smart_proxy_salt 3.1.2 → 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 -6
- 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/srv/salt/_runners/foreman_report_upload.py +1 -1
- data/salt/report_upload/srv/salt/foreman_report_upload.sls +1 -1
- data/sbin/upload-salt-reports +18 -11
- data/settings.d/salt.yml.example +1 -0
- metadata +29 -6
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,19 +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
82
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
83
|
+
grains = if SETTINGS[:filecache]
|
84
|
+
get_grains_from_filecache(minion)
|
85
|
+
else
|
86
|
+
get_grains_from_saltrun(minion)
|
87
|
+
end
|
69
88
|
|
70
89
|
raise 'No grains received from Salt master' unless grains
|
71
90
|
|
72
|
-
plainify(grains
|
91
|
+
plainify(grains).flatten.inject(&:merge)
|
73
92
|
end
|
74
93
|
|
75
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)
|
@@ -99,7 +99,7 @@ def now(highstate):
|
|
99
99
|
log.debug('Upload highstate to Foreman')
|
100
100
|
|
101
101
|
try:
|
102
|
-
report = create_report(base64.
|
102
|
+
report = create_report(base64.b64decode(highstate))
|
103
103
|
upload(report)
|
104
104
|
except Exception as exc:
|
105
105
|
log.error('Exception encountered: %s', exc)
|
data/sbin/upload-salt-reports
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#!/usr/bin/
|
1
|
+
#!/usr/bin/salt_python_wrapper
|
2
2
|
# Uploads reports from the Salt job cache to Foreman
|
3
3
|
|
4
4
|
from __future__ import print_function
|
@@ -34,17 +34,22 @@ def salt_config():
|
|
34
34
|
|
35
35
|
def get_job(job_id):
|
36
36
|
result = run('jobs.lookup_jid', [job_id])
|
37
|
-
|
38
37
|
# If any minion's results are strings, they're exceptions
|
39
38
|
# and should be wrapped in a list like other errors
|
39
|
+
|
40
40
|
for minion, value in result.items():
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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()
|
48
53
|
|
49
54
|
return {'job':
|
50
55
|
{
|
@@ -117,8 +122,10 @@ def upload(jobs):
|
|
117
122
|
connection = HTTPConnection(config[':host'],
|
118
123
|
port=config[':port'])
|
119
124
|
if ':username' in config and ':password' in config:
|
120
|
-
|
121
|
-
|
125
|
+
auth = '{}:{}'.format(config[':username'], config[':password'])
|
126
|
+
if not isinstance(auth, bytes):
|
127
|
+
auth = auth.encode('UTF-8')
|
128
|
+
token = base64.b64encode(auth)
|
122
129
|
headers['Authorization'] = 'Basic {}'.format(token)
|
123
130
|
|
124
131
|
for job_id, job in jobs:
|
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,7 +9,7 @@ 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
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: test-unit
|
@@ -59,28 +59,28 @@ dependencies:
|
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '13'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '13'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rubocop
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
74
|
- - '='
|
75
75
|
- !ruby/object:Gem::Version
|
76
|
-
version: 0.
|
76
|
+
version: 0.50.0
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
81
|
- - '='
|
82
82
|
- !ruby/object:Gem::Version
|
83
|
-
version: 0.
|
83
|
+
version: 0.50.0
|
84
84
|
- !ruby/object:Gem::Dependency
|
85
85
|
name: rack-test
|
86
86
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,10 +95,25 @@ dependencies:
|
|
95
95
|
- - "~>"
|
96
96
|
- !ruby/object:Gem::Version
|
97
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
|
98
112
|
description: SaltStack Plug-In for Foreman's Smart Proxy
|
99
113
|
email: foreman-dev@googlegroups.com
|
100
114
|
executables:
|
101
115
|
- foreman-node
|
116
|
+
- salt_python_wrapper
|
102
117
|
extensions: []
|
103
118
|
extra_rdoc_files:
|
104
119
|
- README.md
|
@@ -107,6 +122,7 @@ files:
|
|
107
122
|
- LICENSE
|
108
123
|
- README.md
|
109
124
|
- bin/foreman-node
|
125
|
+
- bin/salt_python_wrapper
|
110
126
|
- bundler.d/salt.rb
|
111
127
|
- cron/smart_proxy_salt
|
112
128
|
- etc/foreman.yaml.example
|
@@ -117,7 +133,14 @@ files:
|
|
117
133
|
- lib/smart_proxy_salt/salt.rb
|
118
134
|
- lib/smart_proxy_salt/salt_api.rb
|
119
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
|
120
138
|
- lib/smart_proxy_salt/version.rb
|
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
|
121
144
|
- salt/report_upload/README.md
|
122
145
|
- salt/report_upload/master.snippet
|
123
146
|
- salt/report_upload/srv/salt/_runners/foreman_report_upload.py
|