smart_proxy_salt 3.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/foreman-node +25 -35
- data/bin/salt_python_wrapper +16 -0
- data/lib/smart_proxy_salt/cli.rb +82 -32
- data/lib/smart_proxy_salt/salt.rb +20 -3
- data/lib/smart_proxy_salt/salt_api.rb +23 -3
- data/lib/smart_proxy_salt/salt_runner.rb +55 -0
- data/lib/smart_proxy_salt/salt_task_launcher.rb +27 -0
- data/lib/smart_proxy_salt/version.rb +1 -1
- data/salt/minion_auth/README.md +48 -0
- data/salt/minion_auth/foreman_minion_auth.sls +24 -0
- data/salt/minion_auth/master.snippet +5 -0
- data/salt/minion_auth/srv/salt/_runners/foreman_file.py +22 -0
- data/salt/minion_auth/srv/salt/_runners/foreman_https.py +66 -0
- data/salt/report_upload/README.md +31 -0
- data/salt/report_upload/master.snippet +3 -0
- data/salt/report_upload/srv/salt/_runners/foreman_report_upload.py +106 -0
- data/salt/report_upload/srv/salt/foreman_report_upload.sls +10 -0
- data/sbin/upload-salt-reports +46 -27
- data/settings.d/salt.yml.example +1 -0
- metadata +33 -66
- data/lib/smart_proxy_salt_core.rb +0 -17
- data/lib/smart_proxy_salt_core/salt_runner.rb +0 -51
- data/lib/smart_proxy_salt_core/salt_task_launcher.rb +0 -21
- data/lib/smart_proxy_salt_core/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63743a9b37c7860b60af93db9735f210608221d87675c7d11764eb9f08348caa
|
4
|
+
data.tar.gz: 38fa3a0d210534f63d1f925790d0a062b20a907499dad0ba2bae1125a0bb8190
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '008b29492b0f227e04ba281423df738e9334e040492f2fcf62172e866cccd931a80de5a5eac14d8e5b4c81a17dd26697e11f89aa696ab28670471f55be585e26'
|
7
|
+
data.tar.gz: 05a4afde3202a35328d2b2daa4cc2266808a9c7af01b53dae3876a4a8b033ef898fc07e1d86bfc9a22c05b7ab3bcdb924884c34b1d5546e1a8c93a39ec256623
|
data/bin/foreman-node
CHANGED
@@ -14,6 +14,7 @@ require 'net/http'
|
|
14
14
|
require 'net/https'
|
15
15
|
require 'etc'
|
16
16
|
require 'timeout'
|
17
|
+
require 'msgpack' if SETTINGS[:filecache]
|
17
18
|
|
18
19
|
begin
|
19
20
|
require 'json'
|
@@ -57,48 +58,37 @@ rescue Exception => e
|
|
57
58
|
exit 1
|
58
59
|
end
|
59
60
|
|
61
|
+
def get_grains_from_filecache(minion)
|
62
|
+
# Use the grains from the salt master's filesystem based cache
|
63
|
+
# This requires the following settings in /etc/salt/foreman.yaml:
|
64
|
+
# :filecache: true
|
65
|
+
# :cachedir: "/path/to/master/cache" (default: "/var/cache/salt/master")
|
66
|
+
# Also, the msgpack rubygem needs to be present
|
67
|
+
cachedir = SETTINGS[:cachedir] || '/var/cache/salt/master'
|
68
|
+
content = File.read("#{cachedir}/minions/#{minion}/data.p")
|
69
|
+
data = MessagePack.unpack(content)
|
70
|
+
data['grains']
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_grains_from_saltrun(minion)
|
74
|
+
result = IO.popen(['salt-run', '-l', 'quiet', '--output=json', 'cache.grains', minion], &:read)
|
75
|
+
data = JSON.parse(result)
|
76
|
+
data[minion]
|
77
|
+
end
|
78
|
+
|
60
79
|
def plain_grains(minion)
|
61
80
|
# We have to get the grains from the cache, because the client
|
62
81
|
# is probably running 'state.highstate' right now.
|
63
|
-
#
|
64
|
-
# salt-run doesn't support directly outputting to json,
|
65
|
-
# so we have resort to python to extract the grains.
|
66
|
-
# Based on https://github.com/saltstack/salt/issues/9444
|
67
|
-
|
68
|
-
script = <<-EOF
|
69
|
-
#!/usr/bin/env python
|
70
|
-
import json
|
71
|
-
import os
|
72
|
-
import sys
|
73
|
-
|
74
|
-
import salt.config
|
75
|
-
import salt.runner
|
76
|
-
|
77
|
-
if __name__ == '__main__':
|
78
|
-
__opts__ = salt.config.master_config(
|
79
|
-
os.environ.get('SALT_MASTER_CONFIG', '/etc/salt/master'))
|
80
|
-
runner = salt.runner.Runner(__opts__)
|
81
|
-
|
82
|
-
stdout_bak = sys.stdout
|
83
|
-
with open(os.devnull, 'wb') as f:
|
84
|
-
sys.stdout = f
|
85
|
-
ret = runner.cmd('cache.grains', ['#{minion}'])
|
86
|
-
sys.stdout = stdout_bak
|
87
|
-
|
88
|
-
print json.dumps(ret)
|
89
|
-
EOF
|
90
|
-
|
91
|
-
result = IO.popen('python 2>/dev/null', 'r+') do |python|
|
92
|
-
python.write script
|
93
|
-
python.close_write
|
94
|
-
result = python.read
|
95
|
-
end
|
96
82
|
|
97
|
-
grains =
|
83
|
+
grains = if SETTINGS[:filecache]
|
84
|
+
get_grains_from_filecache(minion)
|
85
|
+
else
|
86
|
+
get_grains_from_saltrun(minion)
|
87
|
+
end
|
98
88
|
|
99
89
|
raise 'No grains received from Salt master' unless grains
|
100
90
|
|
101
|
-
plainify(grains
|
91
|
+
plainify(grains).flatten.inject(&:merge)
|
102
92
|
end
|
103
93
|
|
104
94
|
def plainify(hash, prefix = nil)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
set -u
|
4
|
+
|
5
|
+
for py in 'python3' 'python'; do
|
6
|
+
exe=$(type -p ${py})
|
7
|
+
if [ -n "${exe}" ]; then
|
8
|
+
if ${exe} -c 'import salt.config'; then
|
9
|
+
${exe} "$@"
|
10
|
+
exit $?
|
11
|
+
fi
|
12
|
+
fi
|
13
|
+
done
|
14
|
+
|
15
|
+
echo "No usable python version found, check if python or python3 can import salt.config!" 1>&2
|
16
|
+
exit 1
|
data/lib/smart_proxy_salt/cli.rb
CHANGED
@@ -14,44 +14,43 @@ module Proxy
|
|
14
14
|
Proxy::Salt::Plugin.settings.autosign_file
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
autosign = open(autosign_file, File::RDWR)
|
17
|
+
def autosign_key_file
|
18
|
+
Proxy::Salt::Plugin.settings.autosign_key_file
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
def autosign_create_hostname(hostname)
|
22
|
+
if append_value_to_file(autosign_file, hostname)
|
23
|
+
{ message: 'Added hostname successfully.' }
|
24
|
+
else
|
25
|
+
{ message: 'Failed to add hostname.' \
|
26
|
+
' See smart proxy error log for more information.' }
|
27
|
+
end
|
28
|
+
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
+
def autosign_remove_hostname(hostname)
|
31
|
+
if remove_value_from_file(autosign_file, hostname)
|
32
|
+
{ message: 'Removed hostname successfully.' }
|
33
|
+
else
|
34
|
+
{ message: 'Failed to remove hostname.' \
|
35
|
+
' See smart proxy error log for more information.' }
|
36
|
+
end
|
30
37
|
end
|
31
38
|
|
32
|
-
def
|
33
|
-
|
39
|
+
def autosign_create_key(key)
|
40
|
+
if append_value_to_file(autosign_key_file, key)
|
41
|
+
{ message: 'Added key successfully.' }
|
42
|
+
else
|
43
|
+
{ message: 'Failed to add key.' \
|
44
|
+
' See smart proxy error log for more information.' }
|
45
|
+
end
|
46
|
+
end
|
34
47
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
l
|
39
|
-
else
|
40
|
-
found = true
|
41
|
-
nil
|
42
|
-
end
|
43
|
-
end.uniq.compact
|
44
|
-
if found
|
45
|
-
autosign = open(autosign_file, File::TRUNC | File::RDWR)
|
46
|
-
autosign.write entries.join("\n")
|
47
|
-
autosign.write "\n"
|
48
|
-
autosign.close
|
49
|
-
result = { :message => "Removed #{host} from autosign" }
|
50
|
-
logger.info result[:message]
|
51
|
-
result
|
48
|
+
def autosign_remove_key(key)
|
49
|
+
if remove_value_from_file(autosign_key_file, key)
|
50
|
+
{ message: 'Removed key successfully.' }
|
52
51
|
else
|
53
|
-
|
54
|
-
|
52
|
+
{ message: 'Failed to remove key.' \
|
53
|
+
' See smart proxy error log for more information.' }
|
55
54
|
end
|
56
55
|
end
|
57
56
|
|
@@ -62,6 +61,57 @@ module Proxy
|
|
62
61
|
end.map(&:chomp)
|
63
62
|
end
|
64
63
|
|
64
|
+
def append_value_to_file(filepath, value)
|
65
|
+
result = false
|
66
|
+
begin
|
67
|
+
raise "No such file: #{filepath}" unless File.exist?(filepath)
|
68
|
+
|
69
|
+
file = open(filepath, File::RDWR)
|
70
|
+
found = false
|
71
|
+
file.each_line { |line| found = true if line.chomp == value }
|
72
|
+
file.puts value unless found
|
73
|
+
file.close
|
74
|
+
|
75
|
+
logger.info "Added an entry to '#{filepath}' successfully."
|
76
|
+
result = true
|
77
|
+
rescue SystemCallError => e
|
78
|
+
logger.info "Attempted to add an entry to '#{filepath}', but an exception occurred: #{e}"
|
79
|
+
end
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
def remove_value_from_file(filepath, value)
|
84
|
+
result = false
|
85
|
+
begin
|
86
|
+
raise "No such file: #{filepath}" unless File.exist?(filepath)
|
87
|
+
|
88
|
+
found = false
|
89
|
+
entries = open(filepath, File::RDONLY).readlines.collect do |l|
|
90
|
+
entry = l.chomp
|
91
|
+
if entry == value
|
92
|
+
found = true
|
93
|
+
nil
|
94
|
+
elsif entry == ""
|
95
|
+
nil
|
96
|
+
else
|
97
|
+
l
|
98
|
+
end
|
99
|
+
end.uniq.compact
|
100
|
+
if found
|
101
|
+
file = open(filepath, File::TRUNC | File::RDWR)
|
102
|
+
file.write entries.join()
|
103
|
+
file.close
|
104
|
+
logger.info "Removed an entry from '#{filepath}' successfully."
|
105
|
+
result = true
|
106
|
+
else
|
107
|
+
raise Proxy::Salt::NotFound.new("Attempt to remove non-existent entry.")
|
108
|
+
end
|
109
|
+
rescue SystemCallError => e
|
110
|
+
logger.info "Attempted to remove an entry from '#{filepath}', but an exception occurred: #{e}"
|
111
|
+
end
|
112
|
+
result
|
113
|
+
end
|
114
|
+
|
65
115
|
def highstate(host)
|
66
116
|
find_salt_binaries
|
67
117
|
cmd = [@sudo, '-u', Proxy::Salt::Plugin.settings.salt_command_user, @salt, '--async', escape_for_shell(host), 'state.highstate']
|
@@ -13,11 +13,24 @@ module Proxy
|
|
13
13
|
plugin 'salt', Proxy::Salt::VERSION
|
14
14
|
|
15
15
|
default_settings :autosign_file => '/etc/salt/autosign.conf',
|
16
|
+
:autosign_key_file => '/var/lib/foreman-proxy/salt/grains/autosign_key',
|
16
17
|
:salt_command_user => 'root',
|
17
|
-
:use_api => false
|
18
|
+
:use_api => false,
|
19
|
+
:saltfile => '/etc/foreman-proxy/settings.d/salt.saltfile'
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
+
requires :dynflow, '>= 0.5.0'
|
22
|
+
|
23
|
+
rackup_path File.expand_path('salt_http_config.ru', __dir__)
|
24
|
+
|
25
|
+
load_classes do
|
26
|
+
require 'smart_proxy_dynflow'
|
27
|
+
require 'smart_proxy_salt/salt_runner'
|
28
|
+
require 'smart_proxy_salt/salt_task_launcher'
|
29
|
+
end
|
30
|
+
|
31
|
+
load_dependency_injection_wirings do |_container_instance, _settings|
|
32
|
+
Proxy::Dynflow::TaskLauncherRegistry.register('salt', SaltTaskLauncher)
|
33
|
+
end
|
21
34
|
end
|
22
35
|
|
23
36
|
class << self
|
@@ -33,6 +46,10 @@ module Proxy
|
|
33
46
|
super
|
34
47
|
end
|
35
48
|
end
|
49
|
+
|
50
|
+
def respond_to_missing?(method, include_private = false)
|
51
|
+
Proxy::Salt::Rest.respond_to?(method) || Proxy::Salt::CLI.respond_to?(method) || super
|
52
|
+
end
|
36
53
|
end
|
37
54
|
end
|
38
55
|
end
|
@@ -9,12 +9,32 @@ module Proxy
|
|
9
9
|
class Api < ::Sinatra::Base
|
10
10
|
include ::Proxy::Log
|
11
11
|
helpers ::Proxy::Helpers
|
12
|
-
|
12
|
+
authorize_with_trusted_hosts
|
13
|
+
|
14
|
+
post '/autosign_key/:key' do
|
15
|
+
content_type :json
|
16
|
+
begin
|
17
|
+
Proxy::Salt.autosign_create_key(params[:key]).to_json
|
18
|
+
rescue Exception => e
|
19
|
+
log_halt 406, "Failed to create autosign key #{params[:key]}: #{e}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
delete '/autosign_key/:key' do
|
24
|
+
content_type :json
|
25
|
+
begin
|
26
|
+
Proxy::Salt.autosign_remove_key(params[:key]).to_json
|
27
|
+
rescue Proxy::Salt::NotFound => e
|
28
|
+
log_halt 404, e.to_s
|
29
|
+
rescue Exception => e
|
30
|
+
log_halt 406, "Failed to remove autosign key #{params[:key]}: #{e}"
|
31
|
+
end
|
32
|
+
end
|
13
33
|
|
14
34
|
post '/autosign/:host' do
|
15
35
|
content_type :json
|
16
36
|
begin
|
17
|
-
Proxy::Salt.
|
37
|
+
Proxy::Salt.autosign_create_hostname(params[:host]).to_json
|
18
38
|
rescue Exception => e
|
19
39
|
log_halt 406, "Failed to create autosign for #{params[:host]}: #{e}"
|
20
40
|
end
|
@@ -23,7 +43,7 @@ module Proxy
|
|
23
43
|
delete '/autosign/:host' do
|
24
44
|
content_type :json
|
25
45
|
begin
|
26
|
-
Proxy::Salt.
|
46
|
+
Proxy::Salt.autosign_remove_hostname(params[:host]).to_json
|
27
47
|
rescue Proxy::Salt::NotFound => e
|
28
48
|
log_halt 404, e.to_s
|
29
49
|
rescue Exception => e
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'smart_proxy_dynflow/runner/base'
|
2
|
+
require 'smart_proxy_dynflow/runner/command_runner'
|
3
|
+
|
4
|
+
module Proxy
|
5
|
+
module Salt
|
6
|
+
# Implements the SaltRunner to be used by foreman_remote_execution
|
7
|
+
class SaltRunner < Proxy::Dynflow::Runner::CommandRunner
|
8
|
+
DEFAULT_REFRESH_INTERVAL = 1
|
9
|
+
|
10
|
+
attr_reader :jid
|
11
|
+
|
12
|
+
def initialize(options, suspended_action)
|
13
|
+
super(options, :suspended_action => suspended_action)
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
command = generate_command
|
19
|
+
logger.debug("Running command '#{command.join(' ')}'")
|
20
|
+
initialize_command(*command)
|
21
|
+
end
|
22
|
+
|
23
|
+
def kill
|
24
|
+
publish_data('== TASK ABORTED BY USER ==', 'stdout')
|
25
|
+
publish_exit_status(1)
|
26
|
+
::Process.kill('SIGTERM', @command_pid)
|
27
|
+
end
|
28
|
+
|
29
|
+
def publish_data(data, type)
|
30
|
+
if @jid.nil? && (match = data.match(/jid: ([0-9]+)/))
|
31
|
+
@jid = match[1]
|
32
|
+
end
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def publish_exit_status(status)
|
37
|
+
# If there was no salt job associated with this run, mark the job as failed
|
38
|
+
status = 1 if @jid.nil?
|
39
|
+
super status
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def generate_command
|
45
|
+
saltfile_path = ::Proxy::Salt::Plugin.settings[:saltfile]
|
46
|
+
command = %w[salt --show-jid]
|
47
|
+
command << "--saltfile=#{saltfile_path}" if File.file?(saltfile_path)
|
48
|
+
command << @options['name']
|
49
|
+
command << 'state.template_str'
|
50
|
+
command << @options['script']
|
51
|
+
command
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'smart_proxy_dynflow/task_launcher'
|
2
|
+
|
3
|
+
module Proxy
|
4
|
+
module Salt
|
5
|
+
# Implements the TaskLauncher::Batch for Salt
|
6
|
+
class SaltTaskLauncher < ::Proxy::Dynflow::TaskLauncher::Batch
|
7
|
+
# Implements the Runner::Action for Salt
|
8
|
+
class SaltRunnerAction < ::Proxy::Dynflow::Action::Runner
|
9
|
+
def initiate_runner
|
10
|
+
additional_options = {
|
11
|
+
:step_id => run_step_id,
|
12
|
+
:uuid => execution_plan_id
|
13
|
+
}
|
14
|
+
::Proxy::Salt::SaltRunner.new(
|
15
|
+
input.merge(additional_options),
|
16
|
+
suspended_action
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def child_launcher(parent)
|
22
|
+
::Proxy::Dynflow::TaskLauncher::Single.new(world, callback, :parent => parent,
|
23
|
+
:action_class_override => SaltRunnerAction)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Foreman Salt Minion Authentication
|
2
|
+
|
3
|
+
Currently, there are two possibilites to authenticate a newly deployed minion automatically:
|
4
|
+
1. Use the _/etc/salt/autosign.conf_ file which stores the hostnames of acceptable hosts.
|
5
|
+
2. Use _Salt Autosign Grains_ for a more secure way which relies on a shared secret key.
|
6
|
+
|
7
|
+
This README, handles the second option and how to configure it
|
8
|
+
|
9
|
+
## Setup
|
10
|
+
Add the content of 'master.snippet' to '/etc/salt/master' which configures the grains key file on the master and a reactor. The grains file holds the acceptable keys and will be written by the Smart Proxy when a new minion is deployed. The reactor initiates an interaction with Foreman Salt if a new minion was authenticated successfully.
|
11
|
+
In case there is already a reactor configured, you need to adapt it using the options mentioned in 'master.snippet'. The directories given in 'master.snippet' are the default ones. In case you want your files in a different place, you have to change the paths accordingly.
|
12
|
+
|
13
|
+
If '/srv/salt' is configured as 'file_roots' in your '/etc/salt/master' config, setup the necessary Salt runners:
|
14
|
+
|
15
|
+
```
|
16
|
+
/srv/salt/_runners/foreman_file.py
|
17
|
+
/srv/salt/_runners/foreman_https.py
|
18
|
+
```
|
19
|
+
|
20
|
+
Check if the reactor ('foreman_minion_auth.sls') is at the appropriated location and set the correct smart proxy address in the reactor file:
|
21
|
+
|
22
|
+
```
|
23
|
+
/var/lib/foreman-proxy/salt/reactors/foreman_minion_auth.sls
|
24
|
+
```
|
25
|
+
|
26
|
+
After changing the Salt master, restart the salt-master service:
|
27
|
+
|
28
|
+
```
|
29
|
+
systemctl restart salt-master
|
30
|
+
```
|
31
|
+
|
32
|
+
After checking the reactor and runners, run the following command to make them available in the Salt environment:
|
33
|
+
|
34
|
+
```
|
35
|
+
salt-run saltutil.sync_all
|
36
|
+
```
|
37
|
+
|
38
|
+
## Procedure
|
39
|
+
|
40
|
+
1. A new host, configured as Salt minion, is deployed with Foreman Salt.
|
41
|
+
2. Foreman Salt generates a unique key for that minion and distributes it via the Provisioning Template to the host and via an API call to the Smart Proxy.
|
42
|
+
3. The Smart Proxy makes the key available for the Salt Autosign Grains procedure by adding it to the previously defined file (by default: /var/lib/foreman-proxy/salt/grains/autosign_key).
|
43
|
+
4. The Salt minion is started and uses the configured Salt autosign grain for authentication to the Salt master.
|
44
|
+
5. The Salt master accepts the minion depending on the key and the corresponding auth reactor is triggered on the Salt master.
|
45
|
+
6. The Salt master initiates an API call to Foreman Salt which marks the corresponding host status as _authenticated_.
|
46
|
+
7. Foreman Salt triggers an API call to Smart Proxy Salt which deletes the key from the acceptable keys list of the Salt master (since the minion was authenticated already and shall not be reused).
|
47
|
+
|
48
|
+
The minion was authenticated successfully.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
{% if 'act' in data and 'id' in data %}
|
2
|
+
{% if data['act'] == 'accept' %}
|
3
|
+
{% if salt['saltutil.runner']('foreman_file.check_key', (data['id'], 100)) == True %}
|
4
|
+
{%- do salt.log.info('Minion authenticated successfully, starting HTTPS request to delete autosign key.') -%}
|
5
|
+
remove_autosign_key_custom_runner:
|
6
|
+
runner.foreman_https.query_cert:
|
7
|
+
- method: PUT
|
8
|
+
- host: example.proxy.com # set smart proxy address
|
9
|
+
- path: /salt/api/v2/salt_autosign_auth?name={{ data['id'] }}
|
10
|
+
- cert: /etc/pki/katello/puppet/puppet_client.crt # default cert location
|
11
|
+
- key: /etc/pki/katello/puppet/puppet_client.key # default key location
|
12
|
+
- port: 443
|
13
|
+
# Uncomment the following lines in case you want to use username + password authentication (not recommended)
|
14
|
+
# call_foreman_salt_custom_runner:
|
15
|
+
# runner.foreman_https.query_user:
|
16
|
+
# - method: PUT
|
17
|
+
# - host: example.proxy.com # set smart proxy address
|
18
|
+
# - path: /salt/api/v2/salt_autosign_auth?name={{ data['id'] }}
|
19
|
+
# - username: my_username # set username
|
20
|
+
# - password: my_password # set password
|
21
|
+
# - port: 443
|
22
|
+
{% endif %}
|
23
|
+
{% endif %}
|
24
|
+
{% endif %}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
import os
|
4
|
+
import time
|
5
|
+
|
6
|
+
SALT_KEY_PATH = "/etc/salt/pki/master/minions/"
|
7
|
+
|
8
|
+
def time_secs(path):
|
9
|
+
stat = os.stat(path)
|
10
|
+
return stat.st_mtime
|
11
|
+
|
12
|
+
|
13
|
+
def younger_than_secs(path, seconds):
|
14
|
+
|
15
|
+
now_time = time.time()
|
16
|
+
file_time = time_secs(path)
|
17
|
+
if now_time - file_time <= seconds:
|
18
|
+
return True
|
19
|
+
return False
|
20
|
+
|
21
|
+
def check_key(hostname, seconds):
|
22
|
+
return younger_than_secs(SALT_KEY_PATH + hostname, seconds)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
from http.client import HTTPSConnection
|
4
|
+
import ssl
|
5
|
+
import base64
|
6
|
+
import json
|
7
|
+
|
8
|
+
|
9
|
+
def query_cert(host, path, port, method, cert, key,
|
10
|
+
payload=None, timeout=10):
|
11
|
+
|
12
|
+
headers = {"Accept": "application/json"}
|
13
|
+
|
14
|
+
if payload is not None or method.lower() in ['put', 'post']:
|
15
|
+
headers["Content-Type"] = "application/json"
|
16
|
+
|
17
|
+
ctx = ssl.create_default_context()
|
18
|
+
ctx.load_cert_chain(certfile=cert, keyfile=key)
|
19
|
+
|
20
|
+
connection = HTTPSConnection(host,
|
21
|
+
port=port,
|
22
|
+
context=ctx,
|
23
|
+
timeout=timeout)
|
24
|
+
if payload is None:
|
25
|
+
connection.request(method,
|
26
|
+
path,
|
27
|
+
headers=headers)
|
28
|
+
else:
|
29
|
+
payload_json = json.dumps(payload)
|
30
|
+
connection.request(method=method,
|
31
|
+
url=path,
|
32
|
+
body=payload_json,
|
33
|
+
headers=headers)
|
34
|
+
response = connection.getresponse()
|
35
|
+
response_str = response.read().decode('utf-8')
|
36
|
+
print(response_str)
|
37
|
+
|
38
|
+
|
39
|
+
def query_user(host, path, port, method, username, password,
|
40
|
+
payload=None, timeout=10):
|
41
|
+
|
42
|
+
auth = "{}:{}".format(username, password)
|
43
|
+
token = base64.b64encode(auth.encode('utf-8')).decode('ascii')
|
44
|
+
headers = {"Authorization": "Basic {}".format(token),
|
45
|
+
"Accept": "application/json"}
|
46
|
+
if payload is not None or method.lower() in ["put", "post"]:
|
47
|
+
headers["Content-Type"] = "application/json"
|
48
|
+
|
49
|
+
ctx = ssl._create_unverified_context()
|
50
|
+
connection = HTTPSConnection(host,
|
51
|
+
port=port,
|
52
|
+
context=ctx,
|
53
|
+
timeout=timeout)
|
54
|
+
if payload is None:
|
55
|
+
connection.request(method=method,
|
56
|
+
url=path,
|
57
|
+
headers=headers)
|
58
|
+
else:
|
59
|
+
payload_json = json.dumps(payload)
|
60
|
+
connection.request(method=method,
|
61
|
+
url=path,
|
62
|
+
body=payload_json,
|
63
|
+
headers=headers)
|
64
|
+
response = connection.getresponse()
|
65
|
+
response_str = response.read().decode('utf-8')
|
66
|
+
print(response_str)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Foreman Salt Report Upload
|
2
|
+
|
3
|
+
Currently, there are two possibilites to upload the salt report to Foreman:
|
4
|
+
1. Use /usr/sbin/upload-salt-reports which is called by a cron job every 10 minutes by default
|
5
|
+
2. Upload the report immediately by using a Salt Reactor.
|
6
|
+
|
7
|
+
This README, handles the second option and how to configure it
|
8
|
+
|
9
|
+
## Setup
|
10
|
+
Add the content of 'master.snippet' to '/etc/salt/master' which configures a reactor.
|
11
|
+
In case there is already a reactor configured, you need to adapt it using the options mentioned in 'master.snippet'.
|
12
|
+
|
13
|
+
In case '/srv/salt' is configured as 'file_roots' in your '/etc/salt/master' config, setup the necessary salt state file and Salt runner functions:
|
14
|
+
|
15
|
+
```
|
16
|
+
/srv/salt/foreman_report_upload.sls
|
17
|
+
/srv/salt/_runners/foreman_report_upload.py
|
18
|
+
```
|
19
|
+
|
20
|
+
After changing the salt master run:
|
21
|
+
|
22
|
+
```
|
23
|
+
systemctl restart salt-master
|
24
|
+
```
|
25
|
+
|
26
|
+
After adding the foreman_report_upload.sls and foreman_report_upload.py, run the following:
|
27
|
+
|
28
|
+
```
|
29
|
+
salt-run saltutil.sync_all
|
30
|
+
```
|
31
|
+
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
'''
|
3
|
+
Uploads reports from the Salt job cache to Foreman
|
4
|
+
'''
|
5
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
6
|
+
|
7
|
+
FOREMAN_CONFIG = '/etc/salt/foreman.yaml'
|
8
|
+
|
9
|
+
try:
|
10
|
+
from http.client import HTTPConnection, HTTPSConnection
|
11
|
+
except ImportError:
|
12
|
+
from httplib import HTTPSConnection, HTTPSConnection
|
13
|
+
|
14
|
+
import ssl
|
15
|
+
import json
|
16
|
+
import yaml
|
17
|
+
import os
|
18
|
+
import sys
|
19
|
+
import base64
|
20
|
+
|
21
|
+
# Import python libs
|
22
|
+
import logging
|
23
|
+
|
24
|
+
log = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
def salt_config():
|
28
|
+
with open(FOREMAN_CONFIG, 'r') as f:
|
29
|
+
config = yaml.load(f.read())
|
30
|
+
return config
|
31
|
+
|
32
|
+
|
33
|
+
def upload(report):
|
34
|
+
config = salt_config()
|
35
|
+
headers = {'Accept': 'application/json',
|
36
|
+
'Content-Type': 'application/json'}
|
37
|
+
|
38
|
+
if config[':proto'] == 'https':
|
39
|
+
ctx = ssl.create_default_context()
|
40
|
+
ctx.load_cert_chain(certfile=config[':ssl_cert'], keyfile=config[':ssl_key'])
|
41
|
+
if config[':ssl_ca']:
|
42
|
+
ctx.load_verify_locations(cafile=config[':ssl_ca'])
|
43
|
+
connection = HTTPSConnection(config[':host'],
|
44
|
+
port=config[':port'], context=ctx)
|
45
|
+
else:
|
46
|
+
connection = HTTPConnection(config[':host'],
|
47
|
+
port=config[':port'])
|
48
|
+
if ':username' in config and ':password' in config:
|
49
|
+
token = base64.b64encode('{}:{}'.format(config[':username'],
|
50
|
+
config[':password']))
|
51
|
+
headers['Authorization'] = 'Basic {}'.format(token)
|
52
|
+
|
53
|
+
connection.request('POST', '/salt/api/v2/jobs/upload',
|
54
|
+
json.dumps(report), headers)
|
55
|
+
response = connection.getresponse()
|
56
|
+
|
57
|
+
if response.status == 200:
|
58
|
+
info_msg = 'Success {0}: {1}'.format(report['job']['job_id'], response.read())
|
59
|
+
log.info(info_msg)
|
60
|
+
else:
|
61
|
+
log.error("Unable to upload job - aborting report upload")
|
62
|
+
log.error(response.read())
|
63
|
+
|
64
|
+
|
65
|
+
def create_report(json_str):
|
66
|
+
msg = json.loads(json_str)
|
67
|
+
|
68
|
+
if msg['fun'] == 'state.highstate':
|
69
|
+
return {'job':
|
70
|
+
{
|
71
|
+
'result': {
|
72
|
+
msg['id']: msg['return'],
|
73
|
+
},
|
74
|
+
'function': 'state.highstate',
|
75
|
+
'job_id': msg['jid']
|
76
|
+
}
|
77
|
+
}
|
78
|
+
elif msg['fun'] == 'state.template_str':
|
79
|
+
for key, entry in msg['return'].items():
|
80
|
+
if key.startswith('module_') and entry['__id__'] == 'state.highstate':
|
81
|
+
return {'job':
|
82
|
+
{
|
83
|
+
'result': {
|
84
|
+
msg['id']: entry['changes']['ret'],
|
85
|
+
},
|
86
|
+
'function': 'state.highstate',
|
87
|
+
'job_id': msg['jid']
|
88
|
+
}
|
89
|
+
}
|
90
|
+
raise Exception('No state.highstate found')
|
91
|
+
|
92
|
+
|
93
|
+
def now(highstate):
|
94
|
+
'''
|
95
|
+
Upload a highstate to Foreman
|
96
|
+
highstate :
|
97
|
+
dictionary containing a highstate (generated by a Salt reactor)
|
98
|
+
'''
|
99
|
+
log.debug('Upload highstate to Foreman')
|
100
|
+
|
101
|
+
try:
|
102
|
+
report = create_report(base64.b64decode(highstate))
|
103
|
+
upload(report)
|
104
|
+
except Exception as exc:
|
105
|
+
log.error('Exception encountered: %s', exc)
|
106
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
{% if 'cmd' in data and data['cmd'] == '_return' and 'fun' in data and (
|
2
|
+
data['fun'] == 'state.highstate' or (data['fun'] == 'state.template_str' and
|
3
|
+
'fun_args' in data and
|
4
|
+
data['fun_args'][0].startswith('state.highstate:')
|
5
|
+
)) %}
|
6
|
+
foreman_report_upload:
|
7
|
+
runner.foreman_report_upload.now:
|
8
|
+
- args:
|
9
|
+
- highstate: '{{ data|json|base64_encode }}'
|
10
|
+
{% endif %}
|
data/sbin/upload-salt-reports
CHANGED
@@ -1,38 +1,55 @@
|
|
1
|
-
#!/usr/bin/
|
1
|
+
#!/usr/bin/salt_python_wrapper
|
2
2
|
# Uploads reports from the Salt job cache to Foreman
|
3
3
|
|
4
|
+
from __future__ import print_function
|
5
|
+
|
4
6
|
LAST_UPLOADED = '/etc/salt/last_uploaded'
|
5
7
|
FOREMAN_CONFIG = '/etc/salt/foreman.yaml'
|
6
8
|
LOCK_FILE = '/var/lock/salt-report-upload.lock'
|
7
9
|
|
8
|
-
|
10
|
+
try:
|
11
|
+
from http.client import HTTPConnection, HTTPSConnection
|
12
|
+
except ImportError:
|
13
|
+
from httplib import HTTPSConnection, HTTPSConnection
|
9
14
|
import ssl
|
10
15
|
import json
|
11
16
|
import yaml
|
17
|
+
import io
|
12
18
|
import os
|
13
19
|
import sys
|
14
20
|
import base64
|
15
|
-
|
16
21
|
import traceback
|
17
|
-
|
18
22
|
import salt.config
|
19
23
|
import salt.runner
|
20
24
|
|
25
|
+
if sys.version_info.major == 3:
|
26
|
+
unicode = str
|
27
|
+
|
21
28
|
|
22
29
|
def salt_config():
|
23
|
-
with open(FOREMAN_CONFIG, 'r') as f:
|
30
|
+
with io.open(FOREMAN_CONFIG, 'r') as f:
|
24
31
|
config = yaml.load(f.read())
|
25
32
|
return config
|
26
33
|
|
27
34
|
|
28
35
|
def get_job(job_id):
|
29
36
|
result = run('jobs.lookup_jid', [job_id])
|
30
|
-
|
31
37
|
# If any minion's results are strings, they're exceptions
|
32
38
|
# and should be wrapped in a list like other errors
|
33
|
-
|
34
|
-
|
35
|
-
|
39
|
+
|
40
|
+
for minion, value in result.items():
|
41
|
+
try:
|
42
|
+
if isinstance(value,str):
|
43
|
+
result[minion] = [value]
|
44
|
+
elif isinstance(value,list):
|
45
|
+
result[minion] = value
|
46
|
+
else:
|
47
|
+
for key, entry in value.items():
|
48
|
+
if key.startswith('module_') and '__id__' in entry and entry['__id__'] == 'state.highstate':
|
49
|
+
result[minion] = entry['changes']['ret']
|
50
|
+
break
|
51
|
+
except KeyError:
|
52
|
+
traceback.print_exc()
|
36
53
|
|
37
54
|
return {'job':
|
38
55
|
{
|
@@ -47,7 +64,7 @@ def read_last_uploaded():
|
|
47
64
|
if not os.path.isfile(LAST_UPLOADED):
|
48
65
|
return 0
|
49
66
|
else:
|
50
|
-
with open(LAST_UPLOADED, 'r') as f:
|
67
|
+
with io.open(LAST_UPLOADED, 'r') as f:
|
51
68
|
result = f.read().strip()
|
52
69
|
if len(result) == 20:
|
53
70
|
try:
|
@@ -59,8 +76,8 @@ def read_last_uploaded():
|
|
59
76
|
|
60
77
|
|
61
78
|
def write_last_uploaded(last_uploaded):
|
62
|
-
with open(LAST_UPLOADED, 'w+') as f:
|
63
|
-
f.write(last_uploaded)
|
79
|
+
with io.open(LAST_UPLOADED, 'w+') as f:
|
80
|
+
f.write(unicode(last_uploaded))
|
64
81
|
|
65
82
|
|
66
83
|
def run(*args, **kwargs):
|
@@ -68,7 +85,7 @@ def run(*args, **kwargs):
|
|
68
85
|
os.environ.get('SALT_MASTER_CONFIG', '/etc/salt/master'))
|
69
86
|
|
70
87
|
runner = salt.runner.Runner(__opts__)
|
71
|
-
with open(os.devnull, '
|
88
|
+
with io.open(os.devnull, 'w') as f:
|
72
89
|
stdout_bak, sys.stdout = sys.stdout, f
|
73
90
|
try:
|
74
91
|
ret = runner.cmd(*args, **kwargs)
|
@@ -79,12 +96,11 @@ def run(*args, **kwargs):
|
|
79
96
|
|
80
97
|
def jobs_to_upload():
|
81
98
|
jobs = run('jobs.list_jobs', kwarg={
|
82
|
-
"search_function": "state.highstate",
|
99
|
+
"search_function": ["state.highstate","state.template_str"],
|
83
100
|
})
|
84
101
|
last_uploaded = read_last_uploaded()
|
85
102
|
|
86
|
-
job_ids = [jid for
|
87
|
-
if int(jid) > last_uploaded]
|
103
|
+
job_ids = [jid for jid in jobs.keys() if int(jid) > last_uploaded]
|
88
104
|
|
89
105
|
for job_id in sorted(job_ids):
|
90
106
|
yield job_id, get_job(job_id)
|
@@ -99,15 +115,17 @@ def upload(jobs):
|
|
99
115
|
ctx = ssl.create_default_context()
|
100
116
|
ctx.load_cert_chain(certfile=config[':ssl_cert'], keyfile=config[':ssl_key'])
|
101
117
|
if config[':ssl_ca']:
|
102
|
-
|
103
|
-
connection =
|
104
|
-
|
118
|
+
ctx.load_verify_locations(cafile=config[':ssl_ca'])
|
119
|
+
connection = HTTPSConnection(config[':host'],
|
120
|
+
port=config[':port'], context=ctx)
|
105
121
|
else:
|
106
|
-
connection =
|
107
|
-
|
122
|
+
connection = HTTPConnection(config[':host'],
|
123
|
+
port=config[':port'])
|
108
124
|
if ':username' in config and ':password' in config:
|
109
|
-
|
110
|
-
|
125
|
+
auth = '{}:{}'.format(config[':username'], config[':password'])
|
126
|
+
if not isinstance(auth, bytes):
|
127
|
+
auth = auth.encode('UTF-8')
|
128
|
+
token = base64.b64encode(auth)
|
111
129
|
headers['Authorization'] = 'Basic {}'.format(token)
|
112
130
|
|
113
131
|
for job_id, job in jobs:
|
@@ -121,23 +139,24 @@ def upload(jobs):
|
|
121
139
|
|
122
140
|
if response.status == 200:
|
123
141
|
write_last_uploaded(job_id)
|
124
|
-
print
|
142
|
+
print("Success %s: %s" % (job_id, response.read()))
|
125
143
|
else:
|
126
|
-
print
|
127
|
-
print
|
144
|
+
print("Unable to upload job - aborting report upload")
|
145
|
+
print(response.read())
|
128
146
|
|
129
147
|
|
130
148
|
def get_lock():
|
131
149
|
if os.path.isfile(LOCK_FILE):
|
132
150
|
raise Exception("Unable to obtain lock.")
|
133
151
|
else:
|
134
|
-
open(LOCK_FILE, 'w+').close()
|
152
|
+
io.open(LOCK_FILE, 'w+').close()
|
135
153
|
|
136
154
|
|
137
155
|
def release_lock():
|
138
156
|
if os.path.isfile(LOCK_FILE):
|
139
157
|
os.remove(LOCK_FILE)
|
140
158
|
|
159
|
+
|
141
160
|
if __name__ == '__main__':
|
142
161
|
try:
|
143
162
|
get_lock()
|
data/settings.d/salt.yml.example
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_salt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Moll
|
@@ -9,64 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-08-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: json
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
requirements:
|
18
|
-
- - ">="
|
19
|
-
- !ruby/object:Gem::Version
|
20
|
-
version: '0'
|
21
|
-
type: :runtime
|
22
|
-
prerelease: false
|
23
|
-
version_requirements: !ruby/object:Gem::Requirement
|
24
|
-
requirements:
|
25
|
-
- - ">="
|
26
|
-
- !ruby/object:Gem::Version
|
27
|
-
version: '0'
|
28
|
-
- !ruby/object:Gem::Dependency
|
29
|
-
name: rack
|
30
|
-
requirement: !ruby/object:Gem::Requirement
|
31
|
-
requirements:
|
32
|
-
- - ">="
|
33
|
-
- !ruby/object:Gem::Version
|
34
|
-
version: '1.1'
|
35
|
-
type: :runtime
|
36
|
-
prerelease: false
|
37
|
-
version_requirements: !ruby/object:Gem::Requirement
|
38
|
-
requirements:
|
39
|
-
- - ">="
|
40
|
-
- !ruby/object:Gem::Version
|
41
|
-
version: '1.1'
|
42
|
-
- !ruby/object:Gem::Dependency
|
43
|
-
name: sinatra
|
44
|
-
requirement: !ruby/object:Gem::Requirement
|
45
|
-
requirements:
|
46
|
-
- - ">="
|
47
|
-
- !ruby/object:Gem::Version
|
48
|
-
version: '0'
|
49
|
-
type: :runtime
|
50
|
-
prerelease: false
|
51
|
-
version_requirements: !ruby/object:Gem::Requirement
|
52
|
-
requirements:
|
53
|
-
- - ">="
|
54
|
-
- !ruby/object:Gem::Version
|
55
|
-
version: '0'
|
56
|
-
- !ruby/object:Gem::Dependency
|
57
|
-
name: logging
|
58
|
-
requirement: !ruby/object:Gem::Requirement
|
59
|
-
requirements:
|
60
|
-
- - ">="
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
version: '0'
|
63
|
-
type: :runtime
|
64
|
-
prerelease: false
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
66
|
-
requirements:
|
67
|
-
- - ">="
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
version: '0'
|
70
14
|
- !ruby/object:Gem::Dependency
|
71
15
|
name: test-unit
|
72
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,28 +59,28 @@ dependencies:
|
|
115
59
|
requirements:
|
116
60
|
- - "~>"
|
117
61
|
- !ruby/object:Gem::Version
|
118
|
-
version: '
|
62
|
+
version: '13'
|
119
63
|
type: :development
|
120
64
|
prerelease: false
|
121
65
|
version_requirements: !ruby/object:Gem::Requirement
|
122
66
|
requirements:
|
123
67
|
- - "~>"
|
124
68
|
- !ruby/object:Gem::Version
|
125
|
-
version: '
|
69
|
+
version: '13'
|
126
70
|
- !ruby/object:Gem::Dependency
|
127
71
|
name: rubocop
|
128
72
|
requirement: !ruby/object:Gem::Requirement
|
129
73
|
requirements:
|
130
74
|
- - '='
|
131
75
|
- !ruby/object:Gem::Version
|
132
|
-
version: 0.
|
76
|
+
version: 0.50.0
|
133
77
|
type: :development
|
134
78
|
prerelease: false
|
135
79
|
version_requirements: !ruby/object:Gem::Requirement
|
136
80
|
requirements:
|
137
81
|
- - '='
|
138
82
|
- !ruby/object:Gem::Version
|
139
|
-
version: 0.
|
83
|
+
version: 0.50.0
|
140
84
|
- !ruby/object:Gem::Dependency
|
141
85
|
name: rack-test
|
142
86
|
requirement: !ruby/object:Gem::Requirement
|
@@ -151,10 +95,25 @@ dependencies:
|
|
151
95
|
- - "~>"
|
152
96
|
- !ruby/object:Gem::Version
|
153
97
|
version: '0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: smart_proxy_dynflow
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 0.5.0
|
105
|
+
type: :runtime
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 0.5.0
|
154
112
|
description: SaltStack Plug-In for Foreman's Smart Proxy
|
155
113
|
email: foreman-dev@googlegroups.com
|
156
114
|
executables:
|
157
115
|
- foreman-node
|
116
|
+
- salt_python_wrapper
|
158
117
|
extensions: []
|
159
118
|
extra_rdoc_files:
|
160
119
|
- README.md
|
@@ -163,6 +122,7 @@ files:
|
|
163
122
|
- LICENSE
|
164
123
|
- README.md
|
165
124
|
- bin/foreman-node
|
125
|
+
- bin/salt_python_wrapper
|
166
126
|
- bundler.d/salt.rb
|
167
127
|
- cron/smart_proxy_salt
|
168
128
|
- etc/foreman.yaml.example
|
@@ -173,11 +133,18 @@ files:
|
|
173
133
|
- lib/smart_proxy_salt/salt.rb
|
174
134
|
- lib/smart_proxy_salt/salt_api.rb
|
175
135
|
- lib/smart_proxy_salt/salt_http_config.ru
|
136
|
+
- lib/smart_proxy_salt/salt_runner.rb
|
137
|
+
- lib/smart_proxy_salt/salt_task_launcher.rb
|
176
138
|
- lib/smart_proxy_salt/version.rb
|
177
|
-
-
|
178
|
-
-
|
179
|
-
-
|
180
|
-
-
|
139
|
+
- salt/minion_auth/README.md
|
140
|
+
- salt/minion_auth/foreman_minion_auth.sls
|
141
|
+
- salt/minion_auth/master.snippet
|
142
|
+
- salt/minion_auth/srv/salt/_runners/foreman_file.py
|
143
|
+
- salt/minion_auth/srv/salt/_runners/foreman_https.py
|
144
|
+
- salt/report_upload/README.md
|
145
|
+
- salt/report_upload/master.snippet
|
146
|
+
- salt/report_upload/srv/salt/_runners/foreman_report_upload.py
|
147
|
+
- salt/report_upload/srv/salt/foreman_report_upload.sls
|
181
148
|
- sbin/upload-salt-reports
|
182
149
|
- settings.d/salt.saltfile.example
|
183
150
|
- settings.d/salt.yml.example
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'foreman_tasks_core'
|
2
|
-
require 'foreman_remote_execution_core'
|
3
|
-
|
4
|
-
module SmartProxySaltCore
|
5
|
-
extend ForemanTasksCore::SettingsLoader
|
6
|
-
register_settings(:salt,
|
7
|
-
:saltfile => '/etc/foreman-proxy/settings.d/salt.saltfile')
|
8
|
-
|
9
|
-
if ForemanTasksCore.dynflow_present?
|
10
|
-
require 'smart_proxy_salt_core/salt_runner'
|
11
|
-
require 'smart_proxy_salt_core/salt_task_launcher'
|
12
|
-
|
13
|
-
if defined?(SmartProxyDynflowCore)
|
14
|
-
SmartProxyDynflowCore::TaskLauncherRegistry.register('salt', SaltTaskLauncher)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
require 'foreman_tasks_core/runner/command_runner'
|
2
|
-
|
3
|
-
module SmartProxySaltCore
|
4
|
-
class SaltRunner < ForemanTasksCore::Runner::CommandRunner
|
5
|
-
DEFAULT_REFRESH_INTERVAL = 1
|
6
|
-
|
7
|
-
attr_reader :jid
|
8
|
-
|
9
|
-
def initialize(options, suspended_action:)
|
10
|
-
super(options, :suspended_action => suspended_action)
|
11
|
-
@options = options
|
12
|
-
end
|
13
|
-
|
14
|
-
def start
|
15
|
-
command = generate_command
|
16
|
-
logger.debug("Running command '#{command.join(' ')}'")
|
17
|
-
initialize_command(*command)
|
18
|
-
end
|
19
|
-
|
20
|
-
def kill
|
21
|
-
publish_data('== TASK ABORTED BY USER ==', 'stdout')
|
22
|
-
publish_exit_status(1)
|
23
|
-
::Process.kill('SIGTERM', @command_pid)
|
24
|
-
end
|
25
|
-
|
26
|
-
def publish_data(data, type)
|
27
|
-
if @jid.nil? && (match = data.match(/jid: ([0-9]+)/))
|
28
|
-
@jid = match[1]
|
29
|
-
end
|
30
|
-
super
|
31
|
-
end
|
32
|
-
|
33
|
-
def publish_exit_status(status)
|
34
|
-
# If there was no salt job associated with this run, mark the job as failed
|
35
|
-
status = 1 if @jid.nil?
|
36
|
-
super status
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def generate_command
|
42
|
-
saltfile_path = SmartProxySaltCore.settings[:saltfile]
|
43
|
-
command = %w(salt --show-jid)
|
44
|
-
command << "--saltfile=#{saltfile_path}" if File.file?(saltfile_path)
|
45
|
-
command << @options['name']
|
46
|
-
command << 'state.template_str'
|
47
|
-
command << @options['script']
|
48
|
-
command
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module SmartProxySaltCore
|
2
|
-
class SaltTaskLauncher < ForemanTasksCore::TaskLauncher::Batch
|
3
|
-
class SaltRunnerAction < ForemanTasksCore::Runner::Action
|
4
|
-
def initiate_runner
|
5
|
-
additional_options = {
|
6
|
-
:step_id => run_step_id,
|
7
|
-
:uuid => execution_plan_id
|
8
|
-
}
|
9
|
-
::SmartProxySaltCore::SaltRunner.new(
|
10
|
-
input.merge(additional_options),
|
11
|
-
:suspended_action => suspended_action
|
12
|
-
)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def child_launcher(parent)
|
17
|
-
ForemanTasksCore::TaskLauncher::Single.new(world, callback, :parent => parent,
|
18
|
-
:action_class_override => SaltRunnerAction)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|