improved_jenkins_client 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,353 @@
1
+ #
2
+ # Copyright (c) 2012-2013 Kannan Manickam <arangamani.kannan@gmail.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
23
+ require 'improved_jenkins_client/urihelper'
24
+
25
+ module JenkinsApi
26
+ class Client
27
+ # This class communicates with Jenkins "/computer" API to obtain details
28
+ # about nodes or slaves connected to the Jenkins.
29
+ #
30
+ class Node
31
+ include JenkinsApi::UriHelper
32
+
33
+ # General attributes of a node.
34
+ # This allows the following methods to be called on this node object.
35
+ # These methods are defined using define_method and are prefixed
36
+ # with get_.
37
+ #
38
+ # def get_busyExecutors
39
+ # def get_displayName
40
+ # def get_totalExecutors
41
+ #
42
+ GENERAL_ATTRIBUTES = [
43
+ "busyExecutors",
44
+ "displayName",
45
+ "totalExecutors"
46
+ ].freeze
47
+
48
+ # Properties of a node.
49
+ # The following methods are defined to be called on the node object
50
+ # and are prefixed with is_ and end with ? as they return true or false.
51
+ #
52
+ # def is_idle?(node_name)
53
+ # def is_jnlpAgent?(node_name)
54
+ # def is_launchSupported?(node_name)
55
+ # def is_manualLaunchAllowed?(node_name)
56
+ # def is_offline?(node_name)
57
+ # def is_temporarilyOffline?(node_name)
58
+ #
59
+ NODE_PROPERTIES = [
60
+ "idle",
61
+ "jnlpAgent",
62
+ "launchSupported",
63
+ "manualLaunchAllowed",
64
+ "offline",
65
+ "temporarilyOffline"
66
+ ].freeze
67
+
68
+ # Node specific attributes.
69
+ # The following methods are defined using define_method.
70
+ # These methods are prefixed with get_node_.
71
+ #
72
+ # def get_node_numExecutors(node_name)
73
+ # def get_node_icon(node_name)
74
+ # def get_node_displayName(node_name)
75
+ # def get_node_loadStatistics(node_name)
76
+ # def get_node_monitorData(node_name)
77
+ # def get_node_offlineCause(node_name)
78
+ # def get_node_oneOffExecutors(node_name)
79
+ #
80
+ NODE_ATTRIBUTES = [
81
+ "numExecutors",
82
+ "icon",
83
+ "displayName",
84
+ "loadStatistics",
85
+ "monitorData",
86
+ "offlineCause",
87
+ "oneOffExecutors"
88
+ ].freeze
89
+
90
+ # Initializes a new node object
91
+ #
92
+ # @param client [Client] the client object
93
+ #
94
+ # @return [Node] the node object
95
+ #
96
+ def initialize(client)
97
+ @client = client
98
+ @logger = @client.logger
99
+ end
100
+
101
+ # Gives the string representation of the Object
102
+ #
103
+ def to_s
104
+ "#<JenkinsApi::Client::Node>"
105
+ end
106
+
107
+ # Creates a new node with the specified parameters
108
+ #
109
+ # @param [Hash] params parameters for creating a dumb slave
110
+ # * +:name+ name of the slave
111
+ # * +:description+ description of the new slave
112
+ # * +:executors+ number of executors
113
+ # * +:remote_fs+ Remote FS root
114
+ # * +:labels+ comma separated list of labels
115
+ # * +:mode+ mode of the slave: normal, exclusive
116
+ # * +:slave_host+ Hostname/IP of the slave
117
+ # * +:slave_port+ Slave port
118
+ # * +:private_key_file+ Private key file of master
119
+ # * +:credentials_id+ Id for credential in Jenkins
120
+ #
121
+ # @example Create a Dumb Slave
122
+ # create_dumb_slave(
123
+ # :name => "slave1",
124
+ # :slave_host => "10.10.10.10",
125
+ # :private_key_file => "/root/.ssh/id_rsa",
126
+ # :executors => 10,
127
+ # :labels => "slave, ruby"
128
+ # )
129
+ #
130
+ def create_dumb_slave(params)
131
+ unless params[:name] && params[:slave_host] && params[:private_key_file]
132
+ raise ArgumentError, "Name, slave host, and private key file are" +
133
+ " required for creating a slave."
134
+ end
135
+
136
+ @logger.info "Creating a dumb slave '#{params[:name]}'"
137
+ @logger.debug "Creating a dumb slave with params: #{params.inspect}"
138
+ default_params = {
139
+ :description => "Automatically created through improved_jenkins_client",
140
+ :executors => 2,
141
+ :remote_fs => "/var/jenkins",
142
+ :labels => params[:name],
143
+ :slave_port => 22,
144
+ :mode => "normal",
145
+ :private_key_file => "",
146
+ :credentials_id => ""
147
+ }
148
+
149
+ params = default_params.merge(params)
150
+ labels = params[:labels].split(/\s*,\s*/).join(" ")
151
+ mode = params[:mode].upcase
152
+
153
+ post_params = {
154
+ "name" => params[:name],
155
+ "type" => "hudson.slaves.DumbSlave$DescriptorImpl",
156
+ "json" => {
157
+ "name" => params[:name],
158
+ "nodeDescription" => params[:description],
159
+ "numExecutors" => params[:executors],
160
+ "remoteFS" => params[:remote_fs],
161
+ "labelString" => labels,
162
+ "mode" => mode,
163
+ "type" => "hudson.slaves.DumbSlave$DescriptorImpl",
164
+ "retentionStrategy" => {
165
+ "stapler-class" => "hudson.slaves.RetentionStrategy$Always"
166
+ },
167
+ "nodeProperties" => {
168
+ "stapler-class-bag" => "true"
169
+ },
170
+ "launcher" => {
171
+ "stapler-class" => "hudson.plugins.sshslaves.SSHLauncher",
172
+ "host" => params[:slave_host],
173
+ "port" => params[:slave_port],
174
+ "username" => params[:slave_user],
175
+ "privatekey" => params[:private_key_file],
176
+ "credentialsId" => params[:credentials_id]
177
+ }
178
+ }.to_json
179
+ }
180
+ @logger.debug "Modified params posted to create slave:" +
181
+ " #{post_params.inspect}"
182
+ @client.api_post_request("/computer/doCreateItem", post_params)
183
+ end
184
+
185
+ def create_dump_slave(params)
186
+ @logger.warn '[DEPRECATED] Please use create_dumb_slave instead.'
187
+ create_dumb_slave(params)
188
+ end
189
+
190
+ # Deletes the specified node
191
+ #
192
+ # @param [String] node_name Name of the node to delete
193
+ #
194
+ def delete(node_name)
195
+ @logger.info "Deleting node '#{node_name}'"
196
+ if list.include?(node_name)
197
+ @client.api_post_request("/computer/#{path_encode node_name}/doDelete")
198
+ else
199
+ raise "The specified node '#{node_name}' doesn't exist in Jenkins."
200
+ end
201
+ end
202
+
203
+ # Deletes all slaves from Jenkins. The master will be the only node alive
204
+ # after the exection of this call.
205
+ #
206
+ # @note This method will remove all slaves from Jenkins. Please use with
207
+ # caution.
208
+ #
209
+ def delete_all!
210
+ @logger.info "Deleting all nodes (except master) from jenkins"
211
+ list.each { |node| delete(node) unless node == "master" }
212
+ end
213
+
214
+ # This method returns two lists 1) nodes online 2) nodes offline
215
+ #
216
+ # @param [String] filter a regex to filter node names
217
+ # @param [Bool] ignorecase whether to be case sensitive or not
218
+ #
219
+ def online_offline_lists(filter = nil, ignorecase = true)
220
+ @logger.info "Obtaining nodes from jenkins matching filter '#{filter}'"
221
+ offline_node_names = []
222
+ online_node_names = []
223
+ response_json = @client.api_get_request("/computer")
224
+ response_json["computer"].each do |computer|
225
+ if computer["displayName"] =~ /#{filter}/i
226
+ if computer["offline"] == true
227
+ offline_node_names << computer["displayName"]
228
+ else
229
+ online_node_names << computer["displayName"]
230
+ end
231
+ end
232
+ end
233
+ return online_node_names, offline_node_names
234
+ end
235
+
236
+ # This method lists all nodes
237
+ #
238
+ # @param [String] filter a regex to filter node names
239
+ # @param [Bool] ignorecase whether to be case sensitive or not
240
+ #
241
+ def list(filter = nil, ignorecase = true, slaveonly = false)
242
+ @logger.info "Obtaining nodes from jenkins matching filter '#{filter}'"
243
+ node_names = []
244
+ response_json = @client.api_get_request("/computer")
245
+ response_json["computer"].each do |computer|
246
+ if computer["displayName"] =~ /#{filter}/i
247
+ unless slaveonly && computer["displayName"] == "master"
248
+ node_names << computer["displayName"]
249
+ end
250
+ end
251
+ end
252
+ node_names
253
+ end
254
+
255
+ # Identifies the index of a node name in the array node nodes
256
+ #
257
+ # @param [String] node_name name of the node
258
+ #
259
+ def index(node_name)
260
+ response_json = @client.api_get_request("/computer")
261
+ response_json["computer"].each_with_index do |computer, index|
262
+ return index if computer["displayName"] == node_name
263
+ end
264
+ end
265
+
266
+ # Defines methods for general node attributes.
267
+ #
268
+ GENERAL_ATTRIBUTES.each do |meth_suffix|
269
+ define_method("get_#{meth_suffix}") do
270
+ @logger.info "Obtaining '#{meth_suffix}' attribute from jenkins"
271
+ response_json = @client.api_get_request("/computer", "tree=#{path_encode meth_suffix}[*[*[*]]]")
272
+ response_json["#{meth_suffix}"]
273
+ end
274
+ end
275
+
276
+ # Defines methods for node properties.
277
+ #
278
+ NODE_PROPERTIES.each do |meth_suffix|
279
+ define_method("is_#{meth_suffix}?") do |node_name|
280
+ @logger.info "Obtaining '#{meth_suffix}' property of '#{node_name}'"
281
+ node_name = "(master)" if node_name == "master"
282
+ response_json = @client.api_get_request("/computer/#{path_encode node_name}", "tree=#{path_encode meth_suffix}")
283
+ resp = response_json["#{meth_suffix}"].to_s
284
+ resp =~ /False/i ? false : true
285
+ end
286
+ end
287
+
288
+ # Defines methods for node specific attributes.
289
+ NODE_ATTRIBUTES.each do |meth_suffix|
290
+ define_method("get_node_#{meth_suffix}") do |node_name|
291
+ @logger.info "Obtaining '#{meth_suffix}' attribute of '#{node_name}'"
292
+ node_name = "(master)" if node_name == "master"
293
+ response_json = @client.api_get_request("/computer/#{path_encode node_name}", "tree=#{path_encode meth_suffix}[*[*[*]]]")
294
+ response_json["#{meth_suffix}"]
295
+ end
296
+ end
297
+
298
+ # Changes the mode of a slave node in Jenkins
299
+ #
300
+ # @param [String] node_name name of the node to change mode for
301
+ # @param [String] mode mode to change to
302
+ #
303
+ def change_mode(node_name, mode)
304
+ @logger.info "Changing the mode of '#{node_name}' to '#{mode}'"
305
+ mode = mode.upcase
306
+ xml = get_config(node_name)
307
+ n_xml = Nokogiri::XML(xml)
308
+ desc = n_xml.xpath("//mode").first
309
+ desc.content = "#{mode.upcase}"
310
+ xml_modified = n_xml.to_xml
311
+ post_config(node_name, xml_modified)
312
+ end
313
+
314
+ # Obtains the configuration of node from Jenkins server
315
+ #
316
+ # @param [String] node_name name of the node
317
+ #
318
+ def get_config(node_name)
319
+ @logger.info "Obtaining the config.xml of node '#{node_name}'"
320
+ node_name = "(master)" if node_name == "master"
321
+ @client.get_config("/computer/#{path_encode node_name}")
322
+ end
323
+
324
+ # Posts the given config.xml to the Jenkins node
325
+ #
326
+ # @param [String] node_name name of the node
327
+ # @param [String] xml Config.xml of the node
328
+ #
329
+ def post_config(node_name, xml)
330
+ @logger.info "Posting the config.xml of node '#{node_name}'"
331
+ node_name = "(master)" if node_name == "master"
332
+ @client.post_config("/computer/#{path_encode node_name}/config.xml", xml)
333
+ end
334
+
335
+ # Toggles the temporarily offline state of the Jenkins node
336
+ #
337
+ # @param [String] node_name name of the node
338
+ # @param [String] reason Offline reason why the node is offline
339
+ #
340
+ def toggle_temporarilyOffline(node_name, reason="")
341
+ @logger.info "Toggling the temporarily offline status of of node '#{node_name}' with reason '#{reason}'"
342
+ node_name = "(master)" if node_name == "master"
343
+ previous_state = is_temporarilyOffline?(node_name)
344
+ @client.api_post_request("/computer/#{path_encode node_name}/toggleOffline?offlineMessage=#{path_encode reason}")
345
+ new_state = is_temporarilyOffline?(node_name)
346
+ if new_state == previous_state
347
+ raise "The specified node '#{node_name}' was unable to change offline state."
348
+ end
349
+ new_state
350
+ end
351
+ end
352
+ end
353
+ end