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