foreman_remote_execution 1.7.1 → 1.8.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/cockpit_controller.rb +34 -0
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +9 -1
- data/app/models/setting/remote_execution.rb +7 -2
- data/app/models/ssh_execution_provider.rb +17 -0
- data/app/views/job_templates/edit.html.erb +0 -1
- data/app/views/job_templates/index.html.erb +0 -1
- data/app/views/job_templates/new.html.erb +0 -1
- data/config/routes.rb +5 -0
- data/extra/cockpit/foreman-cockpit-session +303 -0
- data/extra/cockpit/settings.yml.example +8 -0
- data/lib/foreman_remote_execution/engine.rb +1 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/cockpit_controller_test.rb +17 -0
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4869e54ee272ae8778eadddd9c3f6304269d924c9d44c42a5f9253ab611eae3a
|
4
|
+
data.tar.gz: ee709617e65b2cb1490c4881517083c48baa138b680ad2afd080936b6c06f19a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f57be80e767108908fdd729bd05c640f541ef027d97d571a5ce9377d46a93fa9d32f0e326106f770086cd8d394efbfaa3790c486f1d90b34a437ec4a77b08331
|
7
|
+
data.tar.gz: cf423d9c36a3469e627644256f1e0da661cb4ea69393eae8119b15af347b5500fdafb939a94fb9b4ca326359d8578b985b136b7320ea5c367f45f91cea903224
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class CockpitController < ApplicationController
|
2
|
+
before_action :find_resource, :only => [:host_ssh_params]
|
3
|
+
|
4
|
+
def host_ssh_params
|
5
|
+
render :json => SSHExecutionProvider.ssh_params(@host)
|
6
|
+
end
|
7
|
+
|
8
|
+
def redirect
|
9
|
+
return invalid_request unless params[:redirect_uri]
|
10
|
+
redir_url = URI.parse(params[:redirect_uri])
|
11
|
+
|
12
|
+
cockpit_url = SSHExecutionProvider.cockpit_url_for_host('')
|
13
|
+
redir_url.query = if redir_url.hostname == URI.join(Setting[:foreman_url], cockpit_url).hostname
|
14
|
+
"access_token=#{request.session_options[:id]}"
|
15
|
+
else
|
16
|
+
"error_description=Sorry"
|
17
|
+
end
|
18
|
+
redirect_to(redir_url.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def resource_name
|
24
|
+
"host"
|
25
|
+
end
|
26
|
+
|
27
|
+
def controller_permission
|
28
|
+
:hosts
|
29
|
+
end
|
30
|
+
|
31
|
+
def action_permission
|
32
|
+
:cockpit
|
33
|
+
end
|
34
|
+
end
|
@@ -20,9 +20,17 @@ module ForemanRemoteExecution
|
|
20
20
|
link_to(_('Schedule Remote Job'), new_job_invocation_path(:host_ids => [args.first.id]), :id => :run_button, :class => 'btn btn-default')
|
21
21
|
end
|
22
22
|
|
23
|
+
def web_console_button(host, *args)
|
24
|
+
return unless authorized_for(permission: 'cockpit_hosts', auth_object: host)
|
25
|
+
url = SSHExecutionProvider.cockpit_url_for_host(host.name)
|
26
|
+
url ? link_to(_('Web Console'), url, :class => 'btn btn-default') : nil
|
27
|
+
end
|
28
|
+
|
23
29
|
def host_title_actions(*args)
|
24
|
-
title_actions(button_group(schedule_job_multi_button(*args))
|
30
|
+
title_actions(button_group(schedule_job_multi_button(*args)),
|
31
|
+
button_group(web_console_button(*args)))
|
25
32
|
super(*args)
|
26
33
|
end
|
27
34
|
end
|
35
|
+
|
28
36
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class Setting::RemoteExecution < Setting
|
2
2
|
|
3
|
-
::Setting::BLANK_ATTRS.concat %w{remote_execution_ssh_password remote_execution_ssh_key_passphrase remote_execution_sudo_password}
|
3
|
+
::Setting::BLANK_ATTRS.concat %w{remote_execution_ssh_password remote_execution_ssh_key_passphrase remote_execution_sudo_password remote_execution_cockpit_url}
|
4
4
|
|
5
5
|
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
6
6
|
def self.load_defaults
|
@@ -67,7 +67,12 @@ class Setting::RemoteExecution < Setting
|
|
67
67
|
self.set('remote_execution_cleanup_working_dirs',
|
68
68
|
N_('When enabled, working directories will be removed after task completion. You may override this per host by setting a parameter called remote_execution_cleanup_working_dirs.'),
|
69
69
|
true,
|
70
|
-
N_('Cleanup working directories'))
|
70
|
+
N_('Cleanup working directories')),
|
71
|
+
self.set('remote_execution_cockpit_url',
|
72
|
+
N_('Where to find the Cockpit instance for the Web Console button. By default, no button is shown.'),
|
73
|
+
nil,
|
74
|
+
N_('Cockpit URL'),
|
75
|
+
nil)
|
71
76
|
].each { |s| self.create! s.update(:category => 'Setting::RemoteExecution') }
|
72
77
|
end
|
73
78
|
|
@@ -29,6 +29,23 @@ class SSHExecutionProvider < RemoteExecutionProvider
|
|
29
29
|
'ssh'
|
30
30
|
end
|
31
31
|
|
32
|
+
def ssh_params(host)
|
33
|
+
proxy_selector = ::RemoteExecutionProxySelector.new
|
34
|
+
proxy = proxy_selector.determine_proxy(host, 'SSH')
|
35
|
+
{
|
36
|
+
:hostname => find_ip_or_hostname(host),
|
37
|
+
:proxy => proxy.class == Symbol ? proxy : proxy.url,
|
38
|
+
:ssh_user => ssh_user(host),
|
39
|
+
:ssh_port => ssh_port(host),
|
40
|
+
:ssh_password => ssh_password(host),
|
41
|
+
:ssh_key_passphrase => ssh_key_passphrase(host)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def cockpit_url_for_host(host)
|
46
|
+
Setting[:remote_execution_cockpit_url] % { :host => host } if Setting[:remote_execution_cockpit_url].present?
|
47
|
+
end
|
48
|
+
|
32
49
|
private
|
33
50
|
|
34
51
|
def ssh_user(host)
|
data/config/routes.rb
CHANGED
@@ -38,6 +38,11 @@ Rails.application.routes.draw do
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
constraints(:id => %r{[^/]+}) do
|
42
|
+
get 'cockpit/host_ssh_params/:id', to: 'cockpit#host_ssh_params'
|
43
|
+
end
|
44
|
+
get 'cockpit/redirect', to: 'cockpit#redirect'
|
45
|
+
|
41
46
|
namespace :api, :defaults => {:format => 'json'} do
|
42
47
|
scope '(:apiv)', :module => :v2, :defaults => {:apiv => 'v2'}, :apiv => /v1|v2/, :constraints => ApiConstraints.new(:version => 2, :default => true) do
|
43
48
|
resources :job_invocations, :except => [:new, :edit, :update, :destroy] do
|
@@ -0,0 +1,303 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "json"
|
5
|
+
require "net/https"
|
6
|
+
require "yaml"
|
7
|
+
|
8
|
+
# Logging
|
9
|
+
|
10
|
+
LOG = Logger.new($stderr)
|
11
|
+
LOG.formatter = proc { | severity, datetime, progname, msg | "#{severity}: #{msg}\n" }
|
12
|
+
|
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
|
21
|
+
end
|
22
|
+
format_string % [data]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Settings
|
26
|
+
|
27
|
+
def read_settings
|
28
|
+
settings_path = ENV["FOREMAN_COCKPIT_SETTINGS"] || "/etc/foreman-cockpit/settings.yml"
|
29
|
+
settings = YAML.load(File.read(settings_path))
|
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
|
33
|
+
end
|
34
|
+
|
35
|
+
# Cockpit protocol, encoding and decoding of control messages.
|
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
|
42
|
+
end
|
43
|
+
|
44
|
+
def read_control
|
45
|
+
size = $stdin.readline.chomp.to_i
|
46
|
+
raise ArgumentError, "Invalid frame: invalid size" if size.zero?
|
47
|
+
data = $stdin.read(size)
|
48
|
+
LOG.debug("Received control message #{data.lstrip}")
|
49
|
+
raise ArgumentError, "Invalid frame: too short" if data == nil || data.length < size
|
50
|
+
JSON.parse(data)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Specific control messages
|
54
|
+
|
55
|
+
def send_auth_challenge(challenge)
|
56
|
+
send_control({ "command" => "authorize",
|
57
|
+
"cookie" => "1234", # must be present, but value doesn't matter
|
58
|
+
"challenge" => challenge
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
def send_auth_response(response)
|
63
|
+
send_control({ "command" => "authorize",
|
64
|
+
"response" => response
|
65
|
+
})
|
66
|
+
end
|
67
|
+
|
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
|
72
|
+
response
|
73
|
+
end
|
74
|
+
|
75
|
+
def exit_with_problem(problem, message, auth_methods)
|
76
|
+
LOG.error("#{problem} - #{message}")
|
77
|
+
send_control({ "command" => "init",
|
78
|
+
"problem" => problem,
|
79
|
+
"message" => message,
|
80
|
+
"auth-method-results" => auth_methods
|
81
|
+
})
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
|
85
|
+
# Talking to Foreman
|
86
|
+
|
87
|
+
def get_token_from_auth_data(auth_data)
|
88
|
+
auth_data.split(" ")[1]
|
89
|
+
end
|
90
|
+
|
91
|
+
def foreman_call(path, token)
|
92
|
+
foreman = SETTINGS[:foreman_url] || "https://localhost/"
|
93
|
+
uri = URI(foreman + "/" + path)
|
94
|
+
|
95
|
+
LOG.debug("Foreman request GET #{uri}")
|
96
|
+
|
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]
|
102
|
+
end
|
103
|
+
|
104
|
+
req = Net::HTTP::Get.new(uri)
|
105
|
+
req["Cookie"] = "_session_id=#{token}"
|
106
|
+
res = http.request(req)
|
107
|
+
|
108
|
+
LOG.debug do
|
109
|
+
body = JSON.parse(res.body) rescue res.body
|
110
|
+
safe_log("Foreman response #{res.code} - %s", body)
|
111
|
+
end
|
112
|
+
|
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
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# SSH via the smart proxy
|
129
|
+
|
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
|
135
|
+
|
136
|
+
def ssh_read_and_handle_response_header(sock, url, params)
|
137
|
+
header = ""
|
138
|
+
loop do
|
139
|
+
line = sock.readline
|
140
|
+
break unless line and line != "\r\n"
|
141
|
+
header += line
|
142
|
+
end
|
143
|
+
|
144
|
+
status_line, headers_text = header.split("\r\n", 2)
|
145
|
+
status = status_line.split(" ")[1]
|
146
|
+
if status != "101"
|
147
|
+
m = /^Content-Length:[ \t]*([0-9]+)\r?$/i.match(headers_text)
|
148
|
+
if m
|
149
|
+
expected_len = m[1].to_i
|
150
|
+
else
|
151
|
+
expected_len = -1
|
152
|
+
end
|
153
|
+
response = ""
|
154
|
+
while expected_len < 0 || response.length < expected_len
|
155
|
+
begin
|
156
|
+
response += sock.readpartial(4096)
|
157
|
+
rescue EOFError
|
158
|
+
break
|
159
|
+
end
|
160
|
+
end
|
161
|
+
if status == "404"
|
162
|
+
exit_with_problem("access-denied", "The proxy #{url.hostname} does not support web console sessions", nil)
|
163
|
+
elsif status[0] == "4"
|
164
|
+
if response.include? "cockpit-bridge: command not found"
|
165
|
+
exit_with_problem("access-denied", "#{params["hostname"]} has no web console", nil)
|
166
|
+
else
|
167
|
+
exit_with_problem("access-denied", response, nil)
|
168
|
+
end
|
169
|
+
else
|
170
|
+
LOG.error("Error talking to smart proxy: #{response}\n")
|
171
|
+
exit 1
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def ssh_read_sock(sock)
|
177
|
+
data = ""
|
178
|
+
begin
|
179
|
+
loop do
|
180
|
+
data += sock.read_nonblock(4096)
|
181
|
+
end
|
182
|
+
rescue IO::WaitReadable
|
183
|
+
data
|
184
|
+
rescue IO::WaitWritable
|
185
|
+
# This might happen with SSL during a renegotiation. Block a
|
186
|
+
# bit to get it over with.
|
187
|
+
IO.select(nil, [sock])
|
188
|
+
retry
|
189
|
+
end
|
190
|
+
data
|
191
|
+
end
|
192
|
+
|
193
|
+
def ssh_write_sock(sock, data)
|
194
|
+
begin
|
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
|
204
|
+
end
|
205
|
+
|
206
|
+
def ssh_with_proxy(proxy, params)
|
207
|
+
url = URI(proxy)
|
208
|
+
LOG.debug("Connecting to proxy at #{url}")
|
209
|
+
raw_sock = TCPSocket.open(url.hostname, url.port)
|
210
|
+
if url.scheme == 'https'
|
211
|
+
ssl_context = OpenSSL::SSL::SSLContext.new()
|
212
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(SETTINGS[:ssl_certificate]))
|
213
|
+
ssl_context.key = OpenSSL::PKey.read(File.read(SETTINGS[:ssl_private_key]))
|
214
|
+
sock = OpenSSL::SSL::SSLSocket.new(raw_sock, ssl_context)
|
215
|
+
sock.sync_close = true
|
216
|
+
sock.connect
|
217
|
+
else
|
218
|
+
sock = raw_sock
|
219
|
+
end
|
220
|
+
|
221
|
+
ssh_write_request_header(url, sock, params)
|
222
|
+
ssh_read_and_handle_response_header(sock, url, params)
|
223
|
+
|
224
|
+
inp_buf = ""
|
225
|
+
out_buf = ssh_read_sock(sock)
|
226
|
+
|
227
|
+
ws_eof = false
|
228
|
+
bridge_eof = false
|
229
|
+
|
230
|
+
loop do
|
231
|
+
readers = [ ]
|
232
|
+
writers = [ ]
|
233
|
+
|
234
|
+
readers += [ $stdin ] unless ws_eof
|
235
|
+
readers += [ sock ] unless bridge_eof
|
236
|
+
writers += [ $stdout ] unless out_buf == ""
|
237
|
+
writers += [ sock ] unless inp_buf == ""
|
238
|
+
|
239
|
+
break if readers.length + writers.length == 0
|
240
|
+
|
241
|
+
r, w, x = IO.select(readers, writers)
|
242
|
+
|
243
|
+
if r.include?(sock)
|
244
|
+
begin
|
245
|
+
out_buf += ssh_read_sock(sock)
|
246
|
+
rescue EOFError
|
247
|
+
bridge_eof = true
|
248
|
+
break if out_buf == ""
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
if w.include?(sock)
|
253
|
+
begin
|
254
|
+
n = ssh_write_sock(sock, inp_buf)
|
255
|
+
inp_buf = inp_buf[n..-1]
|
256
|
+
raw_sock.close_write() if inp_buf == "" and ws_eof
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
if r.include?($stdin)
|
261
|
+
begin
|
262
|
+
inp_buf += $stdin.readpartial(4096)
|
263
|
+
rescue EOFError
|
264
|
+
ws_eof = true
|
265
|
+
raw_sock.close_write() if inp_buf == ""
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
if w.include?($stdout)
|
270
|
+
n = $stdout.write(out_buf)
|
271
|
+
$stdout.flush()
|
272
|
+
out_buf = out_buf[n..-1]
|
273
|
+
break if out_buf == "" and bridge_eof
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Main
|
280
|
+
|
281
|
+
SETTINGS = read_settings
|
282
|
+
|
283
|
+
host = ARGV[0]
|
284
|
+
|
285
|
+
send_auth_challenge("*")
|
286
|
+
token = get_token_from_auth_data(read_auth_reply())
|
287
|
+
|
288
|
+
params = foreman_call("cockpit/host_ssh_params/#{host}", token)
|
289
|
+
exit_with_problem("access-denied", "Host #{host} is not known", nil) unless params
|
290
|
+
|
291
|
+
LOG.debug(safe_log("SSH parameters %s", params))
|
292
|
+
|
293
|
+
params["command"] = "cockpit-bridge"
|
294
|
+
case params["proxy"]
|
295
|
+
when "not_available"
|
296
|
+
exit_with_problem("access-denied", "A proxy is required to reach #{host} but all of them are down", nil)
|
297
|
+
when "not_defined"
|
298
|
+
exit_with_problem("access-denied", "A proxy is required to reach #{host} but none has been configured", nil)
|
299
|
+
when "direct"
|
300
|
+
exit_with_problem("access-denied", "Web console sessions require a proxy but none has been configured", nil)
|
301
|
+
else
|
302
|
+
ssh_with_proxy(params["proxy"], params)
|
303
|
+
end
|
@@ -66,6 +66,7 @@ module ForemanRemoteExecution
|
|
66
66
|
# this permissions grants user to get auto completion hints when setting up filters
|
67
67
|
permission :filter_autocompletion_for_template_invocation, { :template_invocations => [ :auto_complete_search, :index ] },
|
68
68
|
:resource_type => 'TemplateInvocation'
|
69
|
+
permission :cockpit_hosts, { 'cockpit' => [:redirect, :host_ssh_params] }, :resource_type => 'Host'
|
69
70
|
end
|
70
71
|
|
71
72
|
USER_PERMISSIONS = [
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
class CockpitControllerTest < ActionController::TestCase
|
4
|
+
def setup
|
5
|
+
Setting::RemoteExecution.load_defaults
|
6
|
+
as_admin do
|
7
|
+
@host = FactoryBot.create(:host)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
test "should get host_ssh_params" do
|
12
|
+
get :host_ssh_params, params: { id: @host.id }, session: set_session_user
|
13
|
+
assert_response :success
|
14
|
+
response = ActiveSupport::JSON.decode(@response.body)
|
15
|
+
assert response.key?('ssh_user'), 'ssh_params response must include ssh_user'
|
16
|
+
end
|
17
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -155,6 +155,7 @@ files:
|
|
155
155
|
- app/controllers/api/v2/job_templates_controller.rb
|
156
156
|
- app/controllers/api/v2/remote_execution_features_controller.rb
|
157
157
|
- app/controllers/api/v2/template_invocations_controller.rb
|
158
|
+
- app/controllers/cockpit_controller.rb
|
158
159
|
- app/controllers/concerns/foreman/controller/parameters/foreign_input_set.rb
|
159
160
|
- app/controllers/concerns/foreman/controller/parameters/job_template.rb
|
160
161
|
- app/controllers/concerns/foreman/controller/parameters/remote_execution_feature.rb
|
@@ -329,6 +330,8 @@ files:
|
|
329
330
|
- db/seeds.d/60-ssh_proxy_feature.rb
|
330
331
|
- db/seeds.d/70-job_templates.rb
|
331
332
|
- db/seeds.d/90-bookmarks.rb
|
333
|
+
- extra/cockpit/foreman-cockpit-session
|
334
|
+
- extra/cockpit/settings.yml.example
|
332
335
|
- foreman_remote_execution.gemspec
|
333
336
|
- lib/foreman_remote_execution.rb
|
334
337
|
- lib/foreman_remote_execution/engine.rb
|
@@ -369,6 +372,7 @@ files:
|
|
369
372
|
- test/functional/api/v2/job_templates_controller_test.rb
|
370
373
|
- test/functional/api/v2/remote_execution_features_controller_test.rb
|
371
374
|
- test/functional/api/v2/template_invocations_controller_test.rb
|
375
|
+
- test/functional/cockpit_controller_test.rb
|
372
376
|
- test/functional/job_invocations_controller_test.rb
|
373
377
|
- test/functional/job_templates_controller_test.rb
|
374
378
|
- test/helpers/remote_execution_helper_test.rb
|
@@ -420,7 +424,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
420
424
|
version: '0'
|
421
425
|
requirements: []
|
422
426
|
rubyforge_project:
|
423
|
-
rubygems_version: 2.7.
|
427
|
+
rubygems_version: 2.7.6
|
424
428
|
signing_key:
|
425
429
|
specification_version: 4
|
426
430
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
@@ -434,6 +438,7 @@ test_files:
|
|
434
438
|
- test/functional/api/v2/job_templates_controller_test.rb
|
435
439
|
- test/functional/api/v2/remote_execution_features_controller_test.rb
|
436
440
|
- test/functional/api/v2/template_invocations_controller_test.rb
|
441
|
+
- test/functional/cockpit_controller_test.rb
|
437
442
|
- test/functional/job_invocations_controller_test.rb
|
438
443
|
- test/functional/job_templates_controller_test.rb
|
439
444
|
- test/helpers/remote_execution_helper_test.rb
|