jenkins-maestrodev 0.6.9

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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/Changelog.md +58 -0
  3. data/Gemfile +3 -0
  4. data/README.md +162 -0
  5. data/Rakefile +110 -0
  6. data/bin/jenkins +9 -0
  7. data/cucumber.yml +8 -0
  8. data/features/build_details.feature +44 -0
  9. data/features/configure.feature +36 -0
  10. data/features/default_host.feature +26 -0
  11. data/features/development.feature +14 -0
  12. data/features/listing_jobs.feature +34 -0
  13. data/features/manage_jobs.feature +263 -0
  14. data/features/manage_slave_nodes.feature +83 -0
  15. data/features/step_definitions/common_steps.rb +192 -0
  16. data/features/step_definitions/fixture_project_steps.rb +8 -0
  17. data/features/step_definitions/jenkins_steps.rb +116 -0
  18. data/features/step_definitions/scm_steps.rb +12 -0
  19. data/features/support/common.rb +37 -0
  20. data/features/support/env.rb +19 -0
  21. data/features/support/hooks.rb +16 -0
  22. data/features/support/jenkins_helpers.rb +6 -0
  23. data/features/support/matchers.rb +10 -0
  24. data/fixtures/jenkins/envfile.hpi +0 -0
  25. data/fixtures/jenkins/git.hpi +0 -0
  26. data/fixtures/jenkins/github.hpi +0 -0
  27. data/fixtures/jenkins/greenballs.hpi +0 -0
  28. data/fixtures/jenkins/rake.hpi +0 -0
  29. data/fixtures/jenkins/ruby.hpi +0 -0
  30. data/fixtures/projects/erlang/rebar.config +1 -0
  31. data/fixtures/projects/non-bundler/Rakefile +4 -0
  32. data/fixtures/projects/rails-3/.gitignore +4 -0
  33. data/fixtures/projects/rails-3/Gemfile +30 -0
  34. data/fixtures/projects/rails-3/Gemfile.lock +74 -0
  35. data/fixtures/projects/rails-3/README +256 -0
  36. data/fixtures/projects/rails-3/Rakefile +7 -0
  37. data/fixtures/projects/rails-3/app/controllers/application_controller.rb +3 -0
  38. data/fixtures/projects/rails-3/app/helpers/application_helper.rb +2 -0
  39. data/fixtures/projects/rails-3/app/views/layouts/application.html.erb +14 -0
  40. data/fixtures/projects/rails-3/config.ru +4 -0
  41. data/fixtures/projects/rails-3/config/application.rb +42 -0
  42. data/fixtures/projects/rails-3/config/boot.rb +13 -0
  43. data/fixtures/projects/rails-3/config/database.yml +22 -0
  44. data/fixtures/projects/rails-3/config/environment.rb +5 -0
  45. data/fixtures/projects/rails-3/config/environments/development.rb +26 -0
  46. data/fixtures/projects/rails-3/config/environments/production.rb +49 -0
  47. data/fixtures/projects/rails-3/config/environments/test.rb +35 -0
  48. data/fixtures/projects/rails-3/config/initializers/backtrace_silencers.rb +7 -0
  49. data/fixtures/projects/rails-3/config/initializers/inflections.rb +10 -0
  50. data/fixtures/projects/rails-3/config/initializers/mime_types.rb +5 -0
  51. data/fixtures/projects/rails-3/config/initializers/secret_token.rb +7 -0
  52. data/fixtures/projects/rails-3/config/initializers/session_store.rb +8 -0
  53. data/fixtures/projects/rails-3/config/locales/en.yml +5 -0
  54. data/fixtures/projects/rails-3/config/routes.rb +58 -0
  55. data/fixtures/projects/rails-3/db/seeds.rb +7 -0
  56. data/fixtures/projects/rails-3/doc/README_FOR_APP +2 -0
  57. data/fixtures/projects/rails-3/lib/tasks/.gitkeep +0 -0
  58. data/fixtures/projects/rails-3/public/404.html +26 -0
  59. data/fixtures/projects/rails-3/public/422.html +26 -0
  60. data/fixtures/projects/rails-3/public/500.html +26 -0
  61. data/fixtures/projects/rails-3/public/favicon.ico +0 -0
  62. data/fixtures/projects/rails-3/public/images/rails.png +0 -0
  63. data/fixtures/projects/rails-3/public/index.html +239 -0
  64. data/fixtures/projects/rails-3/public/javascripts/application.js +2 -0
  65. data/fixtures/projects/rails-3/public/javascripts/controls.js +965 -0
  66. data/fixtures/projects/rails-3/public/javascripts/dragdrop.js +974 -0
  67. data/fixtures/projects/rails-3/public/javascripts/effects.js +1123 -0
  68. data/fixtures/projects/rails-3/public/javascripts/prototype.js +6001 -0
  69. data/fixtures/projects/rails-3/public/javascripts/rails.js +175 -0
  70. data/fixtures/projects/rails-3/public/robots.txt +5 -0
  71. data/fixtures/projects/rails-3/public/stylesheets/.gitkeep +0 -0
  72. data/fixtures/projects/rails-3/script/rails +6 -0
  73. data/fixtures/projects/rails-3/test/performance/browsing_test.rb +9 -0
  74. data/fixtures/projects/rails-3/test/test_helper.rb +13 -0
  75. data/fixtures/projects/rails-3/vendor/plugins/.gitkeep +0 -0
  76. data/fixtures/projects/ruby/Gemfile +3 -0
  77. data/fixtures/projects/ruby/Gemfile.lock +10 -0
  78. data/fixtures/projects/ruby/Rakefile +4 -0
  79. data/jenkins.gemspec +34 -0
  80. data/lib/jenkins.rb +6 -0
  81. data/lib/jenkins/api.rb +312 -0
  82. data/lib/jenkins/cli.rb +361 -0
  83. data/lib/jenkins/cli/formatting.rb +53 -0
  84. data/lib/jenkins/config.rb +27 -0
  85. data/lib/jenkins/core_ext/hash.rb +9 -0
  86. data/lib/jenkins/core_ext/object/blank.rb +77 -0
  87. data/lib/jenkins/hudson-cli.jar +0 -0
  88. data/lib/jenkins/job_config_builder.rb +417 -0
  89. data/lib/jenkins/project_scm.rb +22 -0
  90. data/lib/jenkins/remote.rb +11 -0
  91. data/lib/jenkins/version.rb +3 -0
  92. data/spec/api_spec.rb +67 -0
  93. data/spec/fixtures/ec2_global.config.xml +103 -0
  94. data/spec/fixtures/erlang.single.config.xml +59 -0
  95. data/spec/fixtures/rails.multi.config.xml +82 -0
  96. data/spec/fixtures/rails.single.config.triggers.xml +84 -0
  97. data/spec/fixtures/rails.single.config.xml +80 -0
  98. data/spec/fixtures/ruby.multi-ruby-multi-labels.config.xml +84 -0
  99. data/spec/fixtures/ruby.multi.config.xml +77 -0
  100. data/spec/fixtures/ruby.single.config.xml +58 -0
  101. data/spec/fixtures/ruby.user-defined-axis.config.xml +69 -0
  102. data/spec/fixtures/therubyracer.config.xml +77 -0
  103. data/spec/hash_key_cleaner_spec.rb +25 -0
  104. data/spec/job_config_builder_spec.rb +233 -0
  105. data/spec/spec_helper.rb +15 -0
  106. metadata +291 -0
@@ -0,0 +1,175 @@
1
+ (function() {
2
+ // Technique from Juriy Zaytsev
3
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
4
+ function isEventSupported(eventName) {
5
+ var el = document.createElement('div');
6
+ eventName = 'on' + eventName;
7
+ var isSupported = (eventName in el);
8
+ if (!isSupported) {
9
+ el.setAttribute(eventName, 'return;');
10
+ isSupported = typeof el[eventName] == 'function';
11
+ }
12
+ el = null;
13
+ return isSupported;
14
+ }
15
+
16
+ function isForm(element) {
17
+ return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
18
+ }
19
+
20
+ function isInput(element) {
21
+ if (Object.isElement(element)) {
22
+ var name = element.nodeName.toUpperCase()
23
+ return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
24
+ }
25
+ else return false
26
+ }
27
+
28
+ var submitBubbles = isEventSupported('submit'),
29
+ changeBubbles = isEventSupported('change')
30
+
31
+ if (!submitBubbles || !changeBubbles) {
32
+ // augment the Event.Handler class to observe custom events when needed
33
+ Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
34
+ function(init, element, eventName, selector, callback) {
35
+ init(element, eventName, selector, callback)
36
+ // is the handler being attached to an element that doesn't support this event?
37
+ if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
38
+ (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
39
+ // "submit" => "emulated:submit"
40
+ this.eventName = 'emulated:' + this.eventName
41
+ }
42
+ }
43
+ )
44
+ }
45
+
46
+ if (!submitBubbles) {
47
+ // discover forms on the page by observing focus events which always bubble
48
+ document.on('focusin', 'form', function(focusEvent, form) {
49
+ // special handler for the real "submit" event (one-time operation)
50
+ if (!form.retrieve('emulated:submit')) {
51
+ form.on('submit', function(submitEvent) {
52
+ var emulated = form.fire('emulated:submit', submitEvent, true)
53
+ // if custom event received preventDefault, cancel the real one too
54
+ if (emulated.returnValue === false) submitEvent.preventDefault()
55
+ })
56
+ form.store('emulated:submit', true)
57
+ }
58
+ })
59
+ }
60
+
61
+ if (!changeBubbles) {
62
+ // discover form inputs on the page
63
+ document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
64
+ // special handler for real "change" events
65
+ if (!input.retrieve('emulated:change')) {
66
+ input.on('change', function(changeEvent) {
67
+ input.fire('emulated:change', changeEvent, true)
68
+ })
69
+ input.store('emulated:change', true)
70
+ }
71
+ })
72
+ }
73
+
74
+ function handleRemote(element) {
75
+ var method, url, params;
76
+
77
+ var event = element.fire("ajax:before");
78
+ if (event.stopped) return false;
79
+
80
+ if (element.tagName.toLowerCase() === 'form') {
81
+ method = element.readAttribute('method') || 'post';
82
+ url = element.readAttribute('action');
83
+ params = element.serialize();
84
+ } else {
85
+ method = element.readAttribute('data-method') || 'get';
86
+ url = element.readAttribute('href');
87
+ params = {};
88
+ }
89
+
90
+ new Ajax.Request(url, {
91
+ method: method,
92
+ parameters: params,
93
+ evalScripts: true,
94
+
95
+ onComplete: function(request) { element.fire("ajax:complete", request); },
96
+ onSuccess: function(request) { element.fire("ajax:success", request); },
97
+ onFailure: function(request) { element.fire("ajax:failure", request); }
98
+ });
99
+
100
+ element.fire("ajax:after");
101
+ }
102
+
103
+ function handleMethod(element) {
104
+ var method = element.readAttribute('data-method'),
105
+ url = element.readAttribute('href'),
106
+ csrf_param = $$('meta[name=csrf-param]')[0],
107
+ csrf_token = $$('meta[name=csrf-token]')[0];
108
+
109
+ var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
110
+ element.parentNode.insert(form);
111
+
112
+ if (method !== 'post') {
113
+ var field = new Element('input', { type: 'hidden', name: '_method', value: method });
114
+ form.insert(field);
115
+ }
116
+
117
+ if (csrf_param) {
118
+ var param = csrf_param.readAttribute('content'),
119
+ token = csrf_token.readAttribute('content'),
120
+ field = new Element('input', { type: 'hidden', name: param, value: token });
121
+ form.insert(field);
122
+ }
123
+
124
+ form.submit();
125
+ }
126
+
127
+
128
+ document.on("click", "*[data-confirm]", function(event, element) {
129
+ var message = element.readAttribute('data-confirm');
130
+ if (!confirm(message)) event.stop();
131
+ });
132
+
133
+ document.on("click", "a[data-remote]", function(event, element) {
134
+ if (event.stopped) return;
135
+ handleRemote(element);
136
+ event.stop();
137
+ });
138
+
139
+ document.on("click", "a[data-method]", function(event, element) {
140
+ if (event.stopped) return;
141
+ handleMethod(element);
142
+ event.stop();
143
+ });
144
+
145
+ document.on("submit", function(event) {
146
+ var element = event.findElement(),
147
+ message = element.readAttribute('data-confirm');
148
+ if (message && !confirm(message)) {
149
+ event.stop();
150
+ return false;
151
+ }
152
+
153
+ var inputs = element.select("input[type=submit][data-disable-with]");
154
+ inputs.each(function(input) {
155
+ input.disabled = true;
156
+ input.writeAttribute('data-original-value', input.value);
157
+ input.value = input.readAttribute('data-disable-with');
158
+ });
159
+
160
+ var element = event.findElement("form[data-remote]");
161
+ if (element) {
162
+ handleRemote(element);
163
+ event.stop();
164
+ }
165
+ });
166
+
167
+ document.on("ajax:after", "form", function(event, element) {
168
+ var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
169
+ inputs.each(function(input) {
170
+ input.value = input.readAttribute('data-original-value');
171
+ input.removeAttribute('data-original-value');
172
+ input.disabled = false;
173
+ });
174
+ });
175
+ })();
@@ -0,0 +1,5 @@
1
+ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2
+ #
3
+ # To ban all spiders from the entire site uncomment the next two lines:
4
+ # User-Agent: *
5
+ # Disallow: /
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+ require 'rails/performance_test_help'
3
+
4
+ # Profiling results for each test method are written to tmp/performance.
5
+ class BrowsingTest < ActionDispatch::PerformanceTest
6
+ def test_homepage
7
+ get '/'
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require File.expand_path('../../config/environment', __FILE__)
3
+ require 'rails/test_help'
4
+
5
+ class ActiveSupport::TestCase
6
+ # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
7
+ #
8
+ # Note: You'll currently still have to declare fixtures explicitly in integration tests
9
+ # -- they do not yet inherit this setting
10
+ fixtures :all
11
+
12
+ # Add more helper methods to be used by all tests here...
13
+ end
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rack"
@@ -0,0 +1,10 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ rack (1.2.1)
5
+
6
+ PLATFORMS
7
+ ruby
8
+
9
+ DEPENDENCIES
10
+ rack
@@ -0,0 +1,4 @@
1
+ desc "Default task runs tests"
2
+ task :default do
3
+ puts "Tests ran successfully!"
4
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "jenkins/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "jenkins-maestrodev"
7
+ s.version = Jenkins::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Charles Lowell", "Nic Williams"]
10
+ s.email = ["cowboyd@thefrontside.net", "drnicwilliams@gmail.com"]
11
+ s.homepage = "https://github.com/jenkinsci/jenkins.rb/tree/master/ruby-tools/cli"
12
+ s.summary = %q{Painless Continuous Integration with Jenkins Server}
13
+ s.description = %q{A suite of utilities for bringing continous integration to your projects (not the other way around) with jenkins CI}
14
+
15
+ s.rubyforge_project = "jenkins"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency("term-ansicolor", ">= 1.0.4")
23
+ s.add_dependency("httparty", "~> 0.6.1")
24
+ s.add_dependency("builder", "~> 2.1.2")
25
+ s.add_dependency("thor", "~> 0.15.0")
26
+ s.add_dependency("hpricot")
27
+ s.add_dependency("json_pure", ">= 1.5.1")
28
+
29
+ s.add_development_dependency "jenkins-war", ">= 1.396"
30
+ s.add_development_dependency "rake"
31
+ s.add_development_dependency "cucumber", "~> 1.0"
32
+ s.add_development_dependency "rspec", "~> 2.0"
33
+ s.add_development_dependency "awesome_print"
34
+ end
@@ -0,0 +1,6 @@
1
+ module Jenkins
2
+ require 'jenkins/version'
3
+ require 'jenkins/api'
4
+ require 'jenkins/job_config_builder'
5
+ require 'jenkins/project_scm'
6
+ end
@@ -0,0 +1,312 @@
1
+ require 'httparty'
2
+ require 'cgi'
3
+ require 'uri'
4
+ require 'json'
5
+ require 'yaml'
6
+
7
+ require 'jenkins/core_ext/hash'
8
+ require 'jenkins/config'
9
+
10
+ YAML::ENGINE.yamler = "syck" if RUBY_VERSION >= '1.9'
11
+
12
+ module Jenkins
13
+ module Api
14
+ include HTTParty
15
+
16
+ headers 'content-type' => 'application/json'
17
+ format :json
18
+ # http_proxy 'localhost', '8888'
19
+
20
+ JobAlreadyExistsError = Class.new(Exception)
21
+
22
+ def self.setup_base_url(options = {})
23
+ # Handle single strings
24
+ options = { :host => options } if options.is_a? String
25
+ options = options.with_clean_keys
26
+ # Thor's HashWithIndifferentAccess is based on string keys which URI::HTTP.build ignores
27
+ options = options.inject({}) { |mem, (key, val)| mem[key.to_sym] = val; mem }
28
+
29
+ options = setup_authentication(options)
30
+
31
+ # Handle URL style hosts by parsing the URL
32
+ if options.keys.length == 1 && options.key?(:host)
33
+ parsed_uri = URI::parse(options[:host])
34
+ options = {
35
+ :host => parsed_uri.host,
36
+ :port => parsed_uri.port,
37
+ :path => parsed_uri.path,
38
+ :ssl => parsed_uri.scheme == 'https'
39
+ }
40
+ if parsed_uri.user && parsed_uri.password
41
+ basic_auth parsed_uri.user, parsed_uri.password
42
+ end
43
+ end
44
+
45
+ options[:host] ||= ENV['JENKINS_HOST']
46
+ options[:port] ||= ENV['JENKINS_PORT']
47
+ options[:port] &&= options[:port].to_i
48
+
49
+ if options[:host]
50
+ uri_class = options.delete(:ssl) ? URI::HTTPS : URI::HTTP
51
+ uri = uri_class.build(options)
52
+ else
53
+ if Jenkins::Config.config["base_uri"]
54
+ uri = Jenkins::Config.config["base_uri"]
55
+ else
56
+ return false # Nothing to work with.
57
+ end
58
+ end
59
+ base_uri uri.to_s
60
+ uri
61
+ end
62
+
63
+ # returns true if successfully create a new job on Jenkins
64
+ # +job_config+ is a Jenkins::JobConfigBuilder instance
65
+ # +options+ are:
66
+ # :override - true, will delete any existing job with same name, else error
67
+ #
68
+ # returns true if successful, else false
69
+ #
70
+ # TODO Exceptions?
71
+ def self.create_job(name, job_config, options = {})
72
+ options = options.with_clean_keys
73
+ delete_job(name) if options[:override]
74
+ begin
75
+ res = post "/createItem/api/xml?name=#{CGI.escape(name)}", {
76
+ :body => job_config.to_xml, :format => :xml, :headers => { 'content-type' => 'application/xml' }
77
+ }
78
+ if res.code.to_i == 200
79
+ cache_configuration!
80
+ true
81
+ else
82
+ show_me_the_error(res)
83
+ false
84
+ end
85
+ rescue REXML::ParseException => e
86
+ # For some reason, if the job exists we get back half a page of HTML
87
+ raise JobAlreadyExistsError.new(name)
88
+ end
89
+ end
90
+
91
+ # returns true if successfully updated a job on Jenkins
92
+ # +job_config+ is a Jenkins::JobConfigBuilder instance
93
+ #
94
+ # returns true if successful, else false
95
+ #
96
+ # TODO Exceptions?
97
+ def self.update_job(name, job_config)
98
+ res = post "#{job_url name}/config.xml", {
99
+ :body => job_config.to_xml, :format => :xml, :headers => { 'content-type' => 'application/xml' }
100
+ }
101
+ if res.code.to_i == 200
102
+ cache_configuration!
103
+ true
104
+ else
105
+ show_me_the_error(res)
106
+ false
107
+ end
108
+ end
109
+
110
+ # Attempts to delete a job +name+
111
+ def self.delete_job(name)
112
+ res = post_plain "#{job_url name}/doDelete"
113
+ res.code.to_i == 302
114
+ end
115
+
116
+ def self.build_job(name)
117
+ res = get_plain "/job/#{name}/build"
118
+ res.code.to_i == 302
119
+ end
120
+
121
+ def self.summary
122
+ json = get "/api/json"
123
+ cache_configuration!
124
+ json
125
+ end
126
+
127
+ def self.job_names
128
+ summary["jobs"].map {|job| job["name"]}
129
+ end
130
+
131
+ # Return hash of job statuses
132
+ def self.job(name)
133
+ begin
134
+ json = get "/job/#{name}/api/json"
135
+ cache_configuration!
136
+ json
137
+ rescue Crack::ParseError
138
+ false
139
+ end
140
+ end
141
+
142
+ # Return a hash of information about a build.
143
+ def self.build_details(job_name, build_number)
144
+ begin
145
+ json = get "/job/#{job_name}/#{build_number}/api/json"
146
+ cache_configuration!
147
+ json
148
+ rescue Crack::ParseError
149
+ false
150
+ end
151
+ end
152
+
153
+ # Return the console log information about a build.
154
+ def self.console(job_name, axe, build_number)
155
+ path = "/job/#{job_name}/#{build_number}/"
156
+ path << "#{axe}/" if axe
157
+ path << "consoleText"
158
+ log = get_plain path
159
+ cache_configuration!
160
+ log.body
161
+ end
162
+
163
+ def self.nodes
164
+ json = get "/computer/api/json"
165
+ cache_configuration!
166
+ json
167
+ end
168
+
169
+ # Adds SSH nodes only, for now
170
+ def self.add_node(options = {})
171
+ options = options.with_clean_keys
172
+ default_options = Hash.new
173
+ if options[:vagrant]
174
+ default_options.merge!(
175
+ :slave_port => 2222,
176
+ :slave_user => 'vagrant',
177
+ :master_key => "/Library/Ruby/Gems/1.8/gems/vagrant-0.6.7/keys/vagrant", # FIXME - hardcoded master username assumption
178
+ :slave_fs => "/vagrant/tmp/jenkins-slave/",
179
+ :description => "Automatically created by Jenkins.rb",
180
+ :executors => 2,
181
+ :exclusive => true
182
+ )
183
+ else
184
+ default_options.merge!(
185
+ :slave_port => 22,
186
+ :slave_user => 'deploy',
187
+ :master_key => "/home/deploy/.ssh/id_rsa", # FIXME - hardcoded master username assumption
188
+ :slave_fs => "/data/jenkins-slave/",
189
+ :description => "Automatically created by Jenkins.rb",
190
+ :executors => 2,
191
+ :exclusive => true
192
+ )
193
+ end
194
+ options = default_options.merge(options)
195
+
196
+ slave_host = options[:slave_host]
197
+ name = options[:name] || slave_host
198
+ labels = options[:labels].split(/\s*,\s*/).join(' ') if options[:labels]
199
+
200
+ type = "hudson.slaves.DumbSlave$DescriptorImpl"
201
+
202
+ fields = {
203
+ "name" => name,
204
+ "type" => type,
205
+
206
+ "json" => {
207
+ "name" => name,
208
+ "nodeDescription" => options[:description],
209
+ "numExecutors" => options[:executors],
210
+ "remoteFS" => options[:slave_fs],
211
+ "labelString" => labels,
212
+ "mode" => options[:exclusive] ? "EXCLUSIVE" : "NORMAL",
213
+ "type" => type,
214
+ "retentionStrategy" => { "stapler-class" => "hudson.slaves.RetentionStrategy$Always" },
215
+ "nodeProperties" => { "stapler-class-bag" => "true" },
216
+ "launcher" => {
217
+ "stapler-class" => "hudson.plugins.sshslaves.SSHLauncher",
218
+ "host" => slave_host,
219
+ "port" => options[:slave_port],
220
+ "username" => options[:slave_user],
221
+ "privatekey" => options[:master_key],
222
+ }
223
+ }.to_json
224
+ }
225
+
226
+ url = URI.parse("#{base_uri}/computer/doCreateItem")
227
+
228
+ req = Net::HTTP::Post.new(url.path)
229
+ req.set_form_data(fields)
230
+
231
+ http = Net::HTTP.new(url.host, url.port)
232
+
233
+ response = http.request(req)
234
+ case response
235
+ when Net::HTTPFound
236
+ { :name => name, :slave_host => slave_host }
237
+ else
238
+ # error message looks like:
239
+ # <td id="main-panel">
240
+ # <h1>Error</h1><p>Slave called 'localhost' already exists</p>
241
+ require "hpricot"
242
+ error = Hpricot(response.body).search("td#main-panel p").text
243
+ unless error.blank?
244
+ puts error
245
+ else
246
+ puts response.body # so we can find other errors
247
+ end
248
+ false
249
+ end
250
+ end
251
+
252
+ def self.delete_node(name)
253
+ post_plain("#{base_uri}/computer/#{CGI::escape(name).gsub('+', '%20')}/doDelete/api/json")
254
+ end
255
+
256
+ # Helper for POST that don't barf at Jenkins's crappy API responses
257
+ def self.post_plain(path, data = "", options = {})
258
+ options = options.with_clean_keys
259
+ uri = URI.parse base_uri
260
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
261
+ if RUBY_VERSION =~ /1.8/
262
+ http.post(path, options)
263
+ else
264
+ http.post(path, data, options)
265
+ end
266
+ end
267
+ end
268
+
269
+ # Helper for GET that don't barf at Jenkins's crappy API responses
270
+ def self.get_plain(path, options = {})
271
+ options = options.with_clean_keys
272
+ uri = URI.parse base_uri
273
+ res = Net::HTTP.start(uri.host, uri.port) { |http| http.get(path, options) }
274
+ end
275
+
276
+ def self.cache_configuration!
277
+ Jenkins::Config.config["base_uri"] = base_uri
278
+ Jenkins::Config.config["basic_auth"] = default_options[:basic_auth]
279
+ Jenkins::Config.store!
280
+ end
281
+
282
+ private
283
+ def self.setup_authentication(options)
284
+ username, password = options.delete(:username), options.delete(:password)
285
+ if username && password
286
+ basic_auth username, password
287
+ elsif Jenkins::Config.config["basic_auth"]
288
+ basic_auth Jenkins::Config.config["basic_auth"]["username"],
289
+ Jenkins::Config.config["basic_auth"]["password"]
290
+ end
291
+ options
292
+ end
293
+
294
+ def self.job_url(name)
295
+ "#{base_uri}/job/#{URI.escape(name)}"
296
+ end
297
+
298
+ def self.show_me_the_error(response)
299
+ require "hpricot"
300
+ doc = Hpricot(response.body)
301
+ error_msg = doc.search("td#main-panel p")
302
+ unless error_msg.inner_text.blank?
303
+ $stderr.puts error_msg.inner_text
304
+ else
305
+ # TODO - what are the errors we get?
306
+ puts "Server error:"
307
+ p response.code
308
+ puts response.body
309
+ end
310
+ end
311
+ end
312
+ end