foreman_remote_execution 8.0.0 → 8.1.0

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 (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