foreman_remote_execution 1.7.1 → 1.8.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/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
|