foreman_remote_execution 8.0.0 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/job_invocations_controller.rb +1 -2
  3. data/app/controllers/job_templates_controller.rb +1 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +1 -1
  5. data/app/helpers/job_invocations_helper.rb +0 -7
  6. data/app/helpers/remote_execution_helper.rb +1 -1
  7. data/app/lib/actions/remote_execution/proxy_action.rb +46 -0
  8. data/app/lib/actions/remote_execution/run_host_job.rb +38 -11
  9. data/app/lib/actions/remote_execution/run_hosts_job.rb +7 -6
  10. data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +27 -0
  11. data/app/models/job_invocation.rb +5 -9
  12. data/app/models/job_invocation_composer.rb +4 -0
  13. data/app/models/remote_execution_provider.rb +10 -2
  14. data/app/models/ssh_execution_provider.rb +1 -0
  15. data/app/models/template_invocation.rb +1 -0
  16. data/app/models/template_invocation_event.rb +11 -0
  17. data/app/views/job_invocations/_form.html.erb +4 -0
  18. data/app/views/job_invocations/new.html.erb +5 -0
  19. data/app/views/templates/script/package_action.erb +1 -1
  20. data/config/routes.rb +5 -5
  21. data/db/migrate/20220713095705_create_template_invocation_events.rb +17 -0
  22. data/db/migrate/20220822155946_add_time_to_pickup_to_job_invocation.rb +5 -0
  23. data/extra/cockpit/foreman-cockpit-session +303 -230
  24. data/extra/cockpit/foreman-cockpit.service +1 -0
  25. data/foreman_remote_execution.gemspec +1 -1
  26. data/lib/foreman_remote_execution/engine.rb +12 -7
  27. data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +131 -0
  28. data/lib/foreman_remote_execution/version.rb +1 -1
  29. data/test/unit/remote_execution_provider_test.rb +22 -0
  30. data/webpack/JobWizard/JobWizard.js +53 -18
  31. data/webpack/JobWizard/JobWizard.scss +3 -0
  32. data/webpack/JobWizard/JobWizardConstants.js +1 -1
  33. data/webpack/JobWizard/JobWizardHelpers.js +15 -0
  34. data/webpack/JobWizard/JobWizardPageRerun.js +29 -5
  35. data/webpack/JobWizard/JobWizardSelectors.js +8 -2
  36. data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +5 -0
  37. data/webpack/JobWizard/__tests__/fixtures.js +26 -2
  38. data/webpack/JobWizard/autofill.js +32 -10
  39. data/webpack/JobWizard/index.js +25 -6
  40. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +25 -0
  41. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +12 -1
  42. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +41 -6
  43. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
  44. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
  45. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  46. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +6 -2
  47. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +28 -20
  48. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +32 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/index.js +2 -2
  50. data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -0
  51. data/webpack/JobWizard/steps/form/FormHelpers.js +21 -1
  52. data/webpack/JobWizard/steps/form/Formatter.js +22 -6
  53. data/webpack/JobWizard/steps/form/ResourceSelect.js +97 -10
  54. data/webpack/JobWizard/steps/form/SearchSelect.js +2 -2
  55. data/webpack/JobWizard/steps/form/SelectField.js +4 -0
  56. data/webpack/JobWizard/submit.js +3 -1
  57. data/webpack/JobWizard/validation.js +1 -0
  58. data/webpack/Routes/routes.js +3 -3
  59. data/webpack/react_app/components/FeaturesDropdown/actions.js +23 -2
  60. data/webpack/react_app/components/FeaturesDropdown/index.js +2 -0
  61. data/webpack/react_app/components/HostKebab/KebabItems.js +1 -0
  62. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +5 -0
  63. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +51 -59
  64. data/webpack/react_app/extend/Fills.js +3 -3
  65. metadata +12 -5
@@ -1,302 +1,375 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "logger"
4
- require "json"
5
- require "net/https"
6
- require "yaml"
3
+ require 'logger'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'net/https'
7
+ require 'stringio'
8
+ require 'yaml'
9
+ require 'singleton'
7
10
 
8
11
  # Logging
9
-
10
12
  LOG = Logger.new($stderr)
11
13
  LOG.formatter = proc { |severity, datetime, progname, msg| "#{severity}: #{msg}\n" }
12
14
 
13
- def safe_log(format_string, data = nil)
14
- if data.is_a? Hash
15
- data = data.dup
16
- data.each do |key, _|
17
- if key.to_s =~ /password|passphrase/
18
- data[key] = '*******'
19
- end
20
- end
15
+ class Settings
16
+ include Singleton
17
+
18
+ def initialize
19
+ @settings = {}
21
20
  end
22
- format_string % [data]
23
- end
24
21
 
25
- # Settings
22
+ def load!
23
+ settings_path = ENV['FOREMAN_COCKPIT_SETTINGS'] || '/etc/foreman-cockpit/settings.yml'
24
+ @settings = YAML.safe_load(File.read(settings_path), [Symbol])
25
+ LOG.level = Logger.const_get(@settings.fetch(:log_level, 'INFO'))
26
+ LOG.info("Running foreman-cockpit-session with settings from #{settings_path}:\n#{@settings.inspect}")
27
+ end
26
28
 
27
- def read_settings
28
- settings_path = ENV["FOREMAN_COCKPIT_SETTINGS"] || "/etc/foreman-cockpit/settings.yml"
29
- settings = YAML.safe_load(File.read(settings_path), [Symbol])
30
- LOG.level = Logger.const_get(settings.fetch(:log_level, "INFO"))
31
- LOG.info("Running foreman-cockpit-session with settings from #{settings_path}:\n#{settings.inspect}")
32
- settings
29
+ def [](key)
30
+ @settings[key]
31
+ end
33
32
  end
34
33
 
35
- # Cockpit protocol, encoding and decoding of control messages.
34
+ class CockpitError < StandardError
35
+ attr_reader :additional
36
36
 
37
- def send_control(msg)
38
- text = JSON.dump(msg)
39
- LOG.debug("Sending control message #{text}")
40
- $stdout.write("#{text.length+1}\n\n#{text}")
41
- $stdout.flush
37
+ def initialize(message, additional = nil)
38
+ @additional = additional
39
+ super message
40
+ end
42
41
  end
43
42
 
44
- def read_control
45
- size = $stdin.readline.chomp.to_i
46
- raise ArgumentError, "Invalid frame: invalid size" if size.zero?
43
+ class AuthenticationError < CockpitError; end
44
+ class AccessDeniedError < CockpitError; end
45
+
46
+ class Cockpit
47
+ class << self
48
+ def encode_message(payload)
49
+ data = JSON.dump(payload)
50
+ "#{data.length + 1}\n\n#{data}"
51
+ end
47
52
 
48
- data = $stdin.read(size)
49
- LOG.debug("Received control message #{data.lstrip}")
50
- raise ArgumentError, "Invalid frame: too short" if data.nil? || data.length < size
53
+ def send_control(io, msg)
54
+ LOG.debug("Sending control message #{msg}")
55
+ io.write(encode_message(msg))
56
+ io.flush
57
+ end
51
58
 
52
- JSON.parse(data)
53
- end
59
+ def read_control(io, fatal: false)
60
+ size = io.readline.chomp.to_i
61
+ raise ArgumentError, 'Invalid frame: invalid size' if size.zero?
54
62
 
55
- # Specific control messages
63
+ data = io.read(size)
64
+ LOG.debug("Received control message #{data.lstrip}")
65
+ raise ArgumentError, 'Invalid frame: too short' if data.nil? || data.length < size
56
66
 
57
- def send_auth_challenge(challenge)
58
- send_control({ "command" => "authorize",
59
- "cookie" => "1234", # must be present, but value doesn't matter
60
- "challenge" => challenge})
67
+ JSON.parse(data)
68
+ rescue JSON::ParserError, ArgumentError => e
69
+ raise e if fatal
70
+ end
71
+ end
61
72
  end
62
73
 
63
- def send_auth_response(response)
64
- send_control({ "command" => "authorize",
65
- "response" => response})
74
+ class Utils
75
+ class << self
76
+ def safe_log(format_string, data = nil)
77
+ if data.is_a? Hash
78
+ data = data.dup
79
+ data.each_key do |key|
80
+ data[key] = '*******' if key.to_s =~ /password|passphrase/
81
+ end
82
+ end
83
+ format_string % [data]
84
+ end
85
+ end
66
86
  end
67
87
 
68
- def read_auth_reply
69
- cmd = read_control
70
- response = cmd["response"]
71
- raise ArgumentError, "Did not receive a valid authorize command" if cmd["command"] != "authorize" || !response
88
+ class ProxyBuffer
89
+ attr_reader :src_io, :dst_io, :buffer
72
90
 
73
- response
74
- end
91
+ def initialize(src_io, dst_io)
92
+ @src_io = src_io
93
+ @dst_io = dst_io
94
+ @buffer = ''
95
+ end
75
96
 
76
- def exit_with_problem(problem, message, auth_methods)
77
- LOG.error("#{problem} - #{message}")
78
- send_control({ "command" => "init",
79
- "problem" => problem,
80
- "message" => message,
81
- "auth-method-results" => auth_methods})
82
- exit 1
83
- end
97
+ def close
98
+ @src_io.close unless @src_io.closed?
99
+ @dst_io.close unless @dst_io.closed?
100
+ end
84
101
 
85
- # Talking to Foreman
102
+ def read_available!
103
+ data = ''
104
+ loop { data += @src_io.read_nonblock(4096) }
105
+ rescue IO::WaitReadable
106
+ rescue IO::WaitWritable
107
+ # This might happen with SSL during a renegotiation. Block a
108
+ # bit to get it over with.
109
+ IO.select(nil, [@src_io])
110
+ retry
111
+ rescue EOFError
112
+ @src_io.close unless @src_io.closed?
113
+ ensure
114
+ @buffer += with_data_callback(data)
115
+ end
86
116
 
87
- def get_token_from_auth_data(auth_data)
88
- auth_data.split(" ")[1]
89
- end
117
+ def with_data_callback(data)
118
+ if @data_callback
119
+ @data_callback.call(data)
120
+ else
121
+ data
122
+ end
123
+ end
90
124
 
91
- def foreman_call(path, token)
92
- foreman = SETTINGS[:foreman_url] || "https://localhost/"
93
- uri = URI(foreman + "/" + path)
125
+ def write_available!
126
+ count = @dst_io.write_nonblock(@buffer)
127
+ @buffer = @buffer[count..-1]
128
+ rescue IO::WaitWritable
129
+ 0
130
+ rescue IO::WaitReadable
131
+ # This might happen with SSL during a renegotiation. Block a
132
+ # bit to get it over with.
133
+ IO.select([@dst_io])
134
+ retry
135
+ end
94
136
 
95
- LOG.debug("Foreman request GET #{uri}")
137
+ def flush_pending_writes!
138
+ write_available! until @buffer.empty?
139
+ end
96
140
 
97
- http = Net::HTTP.new(uri.hostname, uri.port)
98
- if uri.scheme == "https"
99
- http.use_ssl = true
100
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
101
- http.ca_file = SETTINGS[:ssl_ca_file]
141
+ def pending_writes?
142
+ !(@buffer.empty? || @dst_io.closed?)
102
143
  end
103
144
 
104
- req = Net::HTTP::Get.new(uri)
105
- req["Cookie"] = "_session_id=#{token}"
106
- res = http.request(req)
145
+ def readable?
146
+ !@src_io.closed?
147
+ end
107
148
 
108
- LOG.debug do
109
- body = JSON.parse(res.body) rescue res.body
110
- safe_log("Foreman response #{res.code} - %s", body)
149
+ def enqueue(data)
150
+ @buffer += data
111
151
  end
112
152
 
113
- case res.code
114
- when "200"
115
- return JSON.parse(res.body)
116
- when "401"
117
- exit_with_problem("authentication-failed",
118
- "Token was not valid",
119
- { "password" => "not-tried", "token" => "denied" })
120
- when "404"
121
- return nil
122
- else
123
- LOG.error("Error talking to foreman: #{res.body}\n")
124
- exit 1
153
+ def on_data(&block)
154
+ @data_callback = block
125
155
  end
126
156
  end
127
157
 
128
- # SSH via the smart proxy
158
+ class Relay
159
+ attr_reader :proxy
129
160
 
130
- def ssh_write_request_header(url, sock, params)
131
- data = JSON.dump(params) + "\r\n"
132
- sock.write("POST /ssh/session HTTP/1.1\r\nHost: #{url.host}:#{url.port}\r\nConnection: upgrade\r\nUpgrade: raw\r\nContent-Length: #{data.length}\r\n\r\n#{data}")
133
- sock.flush
134
- end
161
+ def self.start(proxy, params)
162
+ new(proxy, params).run
163
+ end
135
164
 
136
- def ssh_read_and_handle_response_header(sock, url, params)
137
- header = ""
138
- loop do
139
- line = sock.readline
140
- break unless line && (line != "\r\n")
165
+ def run
166
+ initialize_proxy_connection!
167
+ proxy_loop
168
+ end
141
169
 
142
- header += line
170
+ def initialize(proxy, params)
171
+ @proxy = proxy
172
+ @params = params
143
173
  end
144
174
 
145
- status_line, headers_text = header.split("\r\n", 2)
146
- status = status_line.split(" ")[1]
147
- if status != "101"
148
- m = /^Content-Length:[ \t]*([0-9]+)\r?$/i.match(headers_text)
149
- expected_len = if m
150
- m[1].to_i
151
- else
152
- -1
153
- end
154
- response = ""
155
- while expected_len < 0 || response.length < expected_len
156
- begin
157
- response += sock.readpartial(4096)
158
- rescue EOFError
159
- break
160
- end
161
- end
162
- if status == "404"
163
- exit_with_problem("access-denied", "The proxy #{url.hostname} does not support web console sessions", nil)
164
- elsif status[0] == "4"
165
- if response.include? "cockpit-bridge: command not found"
166
- exit_with_problem("access-denied", "#{params['hostname']} has no web console", nil)
175
+ def proxy_loop
176
+ proxy1 = ProxyBuffer.new($stdin, @sock)
177
+ proxy2 = ProxyBuffer.new(@sock, $stdout)
178
+ proxy2.on_data do |data|
179
+ message = Cockpit.read_control(StringIO.new(data))
180
+ if message.is_a?(Hash) && message['command'] == 'authorize'
181
+ response = {
182
+ 'command' => 'authorize',
183
+ 'cookie' => message['cookie'],
184
+ 'response' => @params['effective_user_password'],
185
+ }
186
+ proxy1.enqueue(Cockpit.encode_message(response))
187
+ ''
167
188
  else
168
- exit_with_problem("access-denied", response, nil)
189
+ data
169
190
  end
170
- else
171
- LOG.error("Error talking to smart proxy: #{response}\n")
172
- exit 1
173
191
  end
174
- end
175
- end
176
192
 
177
- def ssh_read_sock(sock)
178
- data = ""
179
- begin
180
- loop do
181
- data += sock.read_nonblock(4096)
182
- end
183
- rescue IO::WaitReadable
184
- data
185
- rescue IO::WaitWritable
186
- # This might happen with SSL during a renegotiation. Block a
187
- # bit to get it over with.
188
- IO.select(nil, [sock])
189
- retry
190
- end
191
- data
192
- end
193
+ proxies = [proxy1, proxy2]
193
194
 
194
- def ssh_write_sock(sock, data)
195
- sock.write_nonblock(data)
196
- rescue IO::WaitWritable
197
- 0
198
- rescue IO::WaitReadable
199
- # This might happen with SSL during a renegotiation. Block a
200
- # bit to get it over with.
201
- IO.select([sock])
202
- retry
203
- end
195
+ loop do
196
+ writers = proxies.select(&:pending_writes?)
197
+ readers = proxies.select(&:readable?)
204
198
 
205
- def ssh_with_proxy(proxy, params)
206
- url = URI(proxy)
207
- LOG.debug("Connecting to proxy at #{url}")
208
- raw_sock = TCPSocket.open(url.hostname, url.port)
209
- if url.scheme == 'https'
210
- ssl_context = OpenSSL::SSL::SSLContext.new
211
- ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(SETTINGS[:ssl_certificate]))
212
- ssl_context.key = OpenSSL::PKey.read(File.read(SETTINGS[:ssl_private_key]))
213
- sock = OpenSSL::SSL::SSLSocket.new(raw_sock, ssl_context)
214
- sock.sync_close = true
215
- sock.connect
216
- else
217
- sock = raw_sock
218
- end
199
+ break if readers.empty? && writers.empty?
219
200
 
220
- ssh_write_request_header(url, sock, params)
221
- ssh_read_and_handle_response_header(sock, url, params)
201
+ r, w = select(readers, writers)
222
202
 
223
- inp_buf = ""
224
- out_buf = ssh_read_sock(sock)
203
+ r.each(&:read_available!)
204
+ w.each(&:flush_pending_writes!)
205
+ end
206
+ ensure
207
+ proxies.each(&:close)
208
+ @raw_sock.close
209
+ end
225
210
 
226
- ws_eof = false
227
- bridge_eof = false
211
+ private
228
212
 
229
- loop do
230
- readers = [ ]
231
- writers = [ ]
213
+ def select(readers, writers)
214
+ r_ios, w_ios, = IO.select(readers.map(&:src_io), writers.map(&:dst_io))
232
215
 
233
- readers += [ $stdin ] unless ws_eof
234
- readers += [ sock ] unless bridge_eof
235
- writers += [ $stdout ] unless out_buf == ""
236
- writers += [ sock ] unless inp_buf == ""
216
+ [ r_ios.map { |io| readers.find { |r| r.src_io == io } },
217
+ w_ios.map { |io| writers.find { |w| w.dst_io == io } } ]
218
+ end
237
219
 
238
- break if readers.length + writers.length == 0
220
+ def initialize_proxy_connection!
221
+ url = URI(proxy)
222
+ LOG.debug("Connecting to proxy at #{url}")
223
+ @raw_sock = TCPSocket.open(url.hostname, url.port)
224
+ if url.scheme == 'https'
225
+ ssl_context = OpenSSL::SSL::SSLContext.new
226
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(Settings.instance[:ssl_certificate]))
227
+ ssl_context.key = OpenSSL::PKey.read(File.read(Settings.instance[:ssl_private_key]))
228
+ @sock = OpenSSL::SSL::SSLSocket.new(@raw_sock, ssl_context)
229
+ @sock.sync_close = true
230
+ @sock.connect
231
+ else
232
+ @sock = raw_sock
233
+ end
239
234
 
240
- r, w, x = IO.select(readers, writers)
235
+ upgrade_connection!(url)
236
+ end
241
237
 
242
- if r.include?(sock)
243
- begin
244
- out_buf += ssh_read_sock(sock)
245
- rescue EOFError
246
- bridge_eof = true
247
- break if out_buf == ""
248
- end
238
+ def upgrade_connection!(url)
239
+ data = JSON.dump(@params)
240
+ payload = <<~HTTP
241
+ POST /ssh/session HTTP/1.1
242
+ Host: #{url.host}:#{url.port}
243
+ Connection: upgrade
244
+ Upgrade: raw
245
+ Content-Length: #{data.length + 2}
246
+
247
+ #{data}
248
+ HTTP
249
+
250
+ @sock.write(payload.gsub("\n", "\r\n"))
251
+ @sock.flush
252
+
253
+ buf_io = Net::BufferedIO.new(@sock)
254
+
255
+ # This is ugly, but Net::HTTP doesn't seem to be able to parse upgrade replies properly
256
+ headers = {}
257
+ Net::HTTPResponse.send(:each_response_header, buf_io) { |key, value| headers[key] = value }
258
+
259
+ status = headers['Status'].to_i
260
+ body = buf_io.read(headers['Content-Length'].to_i)
261
+ case status
262
+ when 101
263
+ return
264
+ when 404
265
+ raise AccessDeniedError, "The proxy #{url.hostname} does not support web console sessions"
266
+ when (400..499)
267
+ message = if body.include? 'cockpit-bridge: command not found'
268
+ "#{params['hostname']} has no web console"
269
+ else
270
+ body
271
+ end
272
+ raise AccessDeniedError, message
273
+ else
274
+ raise CockpitError, "Error talking to smart proxy: #{response}"
249
275
  end
276
+ end
277
+ end
250
278
 
251
- if w.include?(sock)
252
- begin
253
- n = ssh_write_sock(sock, inp_buf)
254
- inp_buf = inp_buf[n..-1]
255
- raw_sock.close_write if (inp_buf == "") && ws_eof
256
- end
257
- end
279
+ class Session
280
+ def initialize(host)
281
+ @host = host
282
+ end
258
283
 
259
- if r.include?($stdin)
260
- begin
261
- inp_buf += $stdin.readpartial(4096)
262
- rescue EOFError
263
- ws_eof = true
264
- raw_sock.close_write if inp_buf == ""
265
- end
284
+ def run
285
+ send_auth_challenge('*')
286
+ token = read_auth_reply.match(/^Bearer (.*)$/)[1]
287
+ params = get_host_params(token)
288
+
289
+ LOG.debug(Utils.safe_log('SSH parameters %s', params))
290
+
291
+ params['command'] = 'cockpit-bridge'
292
+ case params['proxy']
293
+ when 'not_available'
294
+ raise AccessDeniedError, "A proxy is required to reach #{@host} but all of them are down"
295
+ when 'not_defined'
296
+ raise AccessDeniedError, "A proxy is required to reach #{@host} but none has been configured"
297
+ when 'direct'
298
+ raise AccessDeniedError, 'Web console sessions require a proxy but none has been configured'
299
+ else
300
+ Relay.start(params['proxy'], params)
266
301
  end
302
+ rescue CockpitError => e
303
+ exit_with_error(e)
304
+ end
267
305
 
268
- next unless w.include?($stdout)
306
+ def exit_with_error(exception)
307
+ problem = case exception
308
+ when AuthenticationError
309
+ 'authentication-failed'
310
+ when AccessDeniedError
311
+ 'access-denied'
312
+ else
313
+ 'error'
314
+ end
315
+
316
+ Cockpit.send_control($stdout, { 'command' => 'init',
317
+ 'problem' => problem,
318
+ 'message' => exception.message,
319
+ 'auth-method-results' => exception.additional})
320
+ exit 1
321
+ end
269
322
 
270
- n = $stdout.write(out_buf)
271
- $stdout.flush
272
- out_buf = out_buf[n..-1]
273
- break if (out_buf == "") && bridge_eof
323
+ def get_host_params(token)
324
+ foreman = Settings.instance[:foreman_url] || 'https://localhost/'
325
+ uri = URI(foreman + '/' + 'cockpit/host_ssh_params/' + @host)
274
326
 
275
- end
276
- end
327
+ LOG.debug("Foreman request GET #{uri}")
277
328
 
278
- # Main
329
+ http = Net::HTTP.new(uri.hostname, uri.port)
330
+ if uri.scheme == 'https'
331
+ http.use_ssl = true
332
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
333
+ http.ca_file = Settings.instance[:ssl_ca_file]
334
+ end
279
335
 
280
- SETTINGS = read_settings
336
+ req = Net::HTTP::Get.new(uri)
337
+ req['Cookie'] = "_session_id=#{token}"
338
+ res = http.request(req)
281
339
 
282
- host = ARGV[0]
340
+ LOG.debug do
341
+ body = JSON.parse(res.body) rescue res.body
342
+ Utils.safe_log("Foreman response #{res.code} - %s", body)
343
+ end
283
344
 
284
- send_auth_challenge("*")
285
- token = get_token_from_auth_data(read_auth_reply)
345
+ case res.code.to_i
346
+ when 200
347
+ return JSON.parse(res.body)
348
+ when 401
349
+ raise AuthenticationError, 'Token was not valid', { 'password' => 'not-tried', 'token' => 'denied' }
350
+ when 404
351
+ raise AccessDeniedError, "Host #{@host} is not known"
352
+ else
353
+ raise CockpitError, "Error talking to Foreman: #{res.body}"
354
+ end
355
+ end
286
356
 
287
- params = foreman_call("cockpit/host_ssh_params/#{host}", token)
288
- exit_with_problem("access-denied", "Host #{host} is not known", nil) unless params
357
+ # Specific control messages
358
+ def send_auth_challenge(challenge)
359
+ Cockpit.send_control($stdout, { 'command' => 'authorize',
360
+ 'cookie' => '1234', # must be present, but value doesn't matter
361
+ 'challenge' => challenge})
362
+ end
289
363
 
290
- LOG.debug(safe_log("SSH parameters %s", params))
364
+ def read_auth_reply
365
+ cmd = Cockpit.read_control($stdin, fatal: true)
366
+ response = cmd['response']
367
+ raise ArgumentError, 'Did not receive a valid authorize command' if cmd['command'] != 'authorize' || !response
291
368
 
292
- params["command"] = "cockpit-bridge"
293
- case params["proxy"]
294
- when "not_available"
295
- exit_with_problem("access-denied", "A proxy is required to reach #{host} but all of them are down", nil)
296
- when "not_defined"
297
- exit_with_problem("access-denied", "A proxy is required to reach #{host} but none has been configured", nil)
298
- when "direct"
299
- exit_with_problem("access-denied", "Web console sessions require a proxy but none has been configured", nil)
300
- else
301
- ssh_with_proxy(params["proxy"], params)
369
+ response
370
+ end
302
371
  end
372
+
373
+ # Load the settings
374
+ Settings.instance.load!
375
+ Session.new(ARGV[0]).run
@@ -8,6 +8,7 @@ Environment=XDG_CONFIG_DIRS=/etc/foreman/
8
8
  Environment=FOREMAN_COCKPIT_SETTINGS=/etc/foreman/cockpit/foreman-cockpit-session.yml
9
9
  Environment=FOREMAN_COCKPIT_ADDRESS=127.0.0.1
10
10
  Environment=FOREMAN_COCKPIT_PORT=19090
11
+ Environment=COCKPIT_SUPERUSER=any
11
12
  ExecStart=/usr/libexec/cockpit-ws --no-tls --address $FOREMAN_COCKPIT_ADDRESS --port $FOREMAN_COCKPIT_PORT
12
13
  User=foreman
13
14
  Group=foreman
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
 
25
25
  s.add_dependency 'deface'
26
26
  s.add_dependency 'dynflow', '>= 1.0.2', '< 2.0.0'
27
- s.add_dependency 'foreman-tasks', '>= 5.1.0'
27
+ s.add_dependency 'foreman-tasks', '>= 7.1.0'
28
28
 
29
29
  s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
30
30
  s.add_development_dependency 'rdoc'
@@ -155,6 +155,11 @@ module ForemanRemoteExecution
155
155
  default: 'Job - Invocation Report',
156
156
  full_name: N_('Job Invocation Report Template'),
157
157
  collection: proc { ForemanRemoteExecution.job_invocation_report_templates_select }
158
+ setting 'remote_execution_time_to_pickup',
159
+ type: :integer,
160
+ description: N_('Time in seconds within which the host has to pick up a job. If the job is not picked up within this limit, the job will be cancelled. Defaults to 1 day.'),
161
+ default: 24 * 60 * 60,
162
+ full_name: N_('Time to pickup')
158
163
  end
159
164
  end
160
165
 
@@ -238,13 +243,6 @@ module ForemanRemoteExecution
238
243
  parent: :monitor_menu,
239
244
  after: :audits
240
245
 
241
- menu :labs_menu, :job_wizard,
242
- url_hash: { controller: 'job_wizard', action: :index },
243
- caption: N_('Job wizard'),
244
- parent: :lab_features_menu,
245
- url: '/experimental/job_wizard/new',
246
- after: :host_wizard
247
-
248
246
  register_custom_status HostStatus::ExecutionStatus
249
247
  # add dashboard widget
250
248
  # widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
@@ -349,6 +347,13 @@ module ForemanRemoteExecution
349
347
  locale_domain = 'foreman_remote_execution'
350
348
  Foreman::Gettext::Support.add_text_domain locale_domain, locale_dir
351
349
  end
350
+
351
+ rake_tasks do
352
+ %w[explain_proxy_selection.rake].each do |rake_file|
353
+ full_path = File.expand_path("../tasks/#{rake_file}", __FILE__)
354
+ load full_path if File.exist?(full_path)
355
+ end
356
+ end
352
357
  end
353
358
 
354
359
  def self.job_invocation_report_templates_select