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.
- checksums.yaml +4 -4
- data/app/controllers/job_invocations_controller.rb +1 -2
- data/app/controllers/job_templates_controller.rb +1 -1
- data/app/controllers/ui_job_wizard_controller.rb +1 -1
- data/app/helpers/job_invocations_helper.rb +0 -7
- data/app/helpers/remote_execution_helper.rb +1 -1
- data/app/lib/actions/remote_execution/proxy_action.rb +46 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +38 -11
- data/app/lib/actions/remote_execution/run_hosts_job.rb +7 -6
- data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +27 -0
- data/app/models/job_invocation.rb +5 -9
- data/app/models/job_invocation_composer.rb +4 -0
- data/app/models/remote_execution_provider.rb +10 -2
- data/app/models/ssh_execution_provider.rb +1 -0
- data/app/models/template_invocation.rb +1 -0
- data/app/models/template_invocation_event.rb +11 -0
- data/app/views/job_invocations/_form.html.erb +4 -0
- data/app/views/job_invocations/new.html.erb +5 -0
- data/app/views/templates/script/package_action.erb +1 -1
- data/config/routes.rb +5 -5
- data/db/migrate/20220713095705_create_template_invocation_events.rb +17 -0
- data/db/migrate/20220822155946_add_time_to_pickup_to_job_invocation.rb +5 -0
- data/extra/cockpit/foreman-cockpit-session +303 -230
- data/extra/cockpit/foreman-cockpit.service +1 -0
- data/foreman_remote_execution.gemspec +1 -1
- data/lib/foreman_remote_execution/engine.rb +12 -7
- data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +131 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/remote_execution_provider_test.rb +22 -0
- data/webpack/JobWizard/JobWizard.js +53 -18
- data/webpack/JobWizard/JobWizard.scss +3 -0
- data/webpack/JobWizard/JobWizardConstants.js +1 -1
- data/webpack/JobWizard/JobWizardHelpers.js +15 -0
- data/webpack/JobWizard/JobWizardPageRerun.js +29 -5
- data/webpack/JobWizard/JobWizardSelectors.js +8 -2
- data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +5 -0
- data/webpack/JobWizard/__tests__/fixtures.js +26 -2
- data/webpack/JobWizard/autofill.js +32 -10
- data/webpack/JobWizard/index.js +25 -6
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +25 -0
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +12 -1
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +41 -6
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +6 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +28 -20
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +32 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +2 -2
- data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +21 -1
- data/webpack/JobWizard/steps/form/Formatter.js +22 -6
- data/webpack/JobWizard/steps/form/ResourceSelect.js +97 -10
- data/webpack/JobWizard/steps/form/SearchSelect.js +2 -2
- data/webpack/JobWizard/steps/form/SelectField.js +4 -0
- data/webpack/JobWizard/submit.js +3 -1
- data/webpack/JobWizard/validation.js +1 -0
- data/webpack/Routes/routes.js +3 -3
- data/webpack/react_app/components/FeaturesDropdown/actions.js +23 -2
- data/webpack/react_app/components/FeaturesDropdown/index.js +2 -0
- data/webpack/react_app/components/HostKebab/KebabItems.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +5 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +51 -59
- data/webpack/react_app/extend/Fills.js +3 -3
- metadata +12 -5
@@ -1,302 +1,375 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
28
|
-
|
29
|
-
|
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
|
-
|
34
|
+
class CockpitError < StandardError
|
35
|
+
attr_reader :additional
|
36
36
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
$stdout.flush
|
37
|
+
def initialize(message, additional = nil)
|
38
|
+
@additional = additional
|
39
|
+
super message
|
40
|
+
end
|
42
41
|
end
|
43
42
|
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
74
|
-
|
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
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
88
|
-
|
89
|
-
|
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
|
92
|
-
|
93
|
-
|
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
|
-
|
137
|
+
def flush_pending_writes!
|
138
|
+
write_available! until @buffer.empty?
|
139
|
+
end
|
96
140
|
|
97
|
-
|
98
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
145
|
+
def readable?
|
146
|
+
!@src_io.closed?
|
147
|
+
end
|
107
148
|
|
108
|
-
|
109
|
-
|
110
|
-
safe_log("Foreman response #{res.code} - %s", body)
|
149
|
+
def enqueue(data)
|
150
|
+
@buffer += data
|
111
151
|
end
|
112
152
|
|
113
|
-
|
114
|
-
|
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
|
-
|
158
|
+
class Relay
|
159
|
+
attr_reader :proxy
|
129
160
|
|
130
|
-
def
|
131
|
-
|
132
|
-
|
133
|
-
sock.flush
|
134
|
-
end
|
161
|
+
def self.start(proxy, params)
|
162
|
+
new(proxy, params).run
|
163
|
+
end
|
135
164
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
break unless line && (line != "\r\n")
|
165
|
+
def run
|
166
|
+
initialize_proxy_connection!
|
167
|
+
proxy_loop
|
168
|
+
end
|
141
169
|
|
142
|
-
|
170
|
+
def initialize(proxy, params)
|
171
|
+
@proxy = proxy
|
172
|
+
@params = params
|
143
173
|
end
|
144
174
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
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
|
-
|
221
|
-
ssh_read_and_handle_response_header(sock, url, params)
|
201
|
+
r, w = select(readers, writers)
|
222
202
|
|
223
|
-
|
224
|
-
|
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
|
-
|
227
|
-
bridge_eof = false
|
211
|
+
private
|
228
212
|
|
229
|
-
|
230
|
-
|
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
|
234
|
-
|
235
|
-
|
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
|
-
|
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
|
-
|
235
|
+
upgrade_connection!(url)
|
236
|
+
end
|
241
237
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
-
|
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
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
276
|
-
end
|
327
|
+
LOG.debug("Foreman request GET #{uri}")
|
277
328
|
|
278
|
-
|
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
|
-
|
336
|
+
req = Net::HTTP::Get.new(uri)
|
337
|
+
req['Cookie'] = "_session_id=#{token}"
|
338
|
+
res = http.request(req)
|
281
339
|
|
282
|
-
|
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
|
-
|
285
|
-
|
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
|
-
|
288
|
-
|
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
|
-
|
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
|
-
|
293
|
-
|
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', '>=
|
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
|