smart_proxy_ansible 3.1.1 → 3.2.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/lib/smart_proxy_ansible/actions.rb +19 -0
- data/lib/smart_proxy_ansible/api.rb +2 -2
- data/lib/smart_proxy_ansible/plugin.rb +16 -8
- data/lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb +64 -0
- data/lib/smart_proxy_ansible/runner/ansible_runner.rb +210 -0
- data/lib/smart_proxy_ansible/runner/command_creator.rb +61 -0
- data/lib/smart_proxy_ansible/runner/playbook.rb +138 -0
- data/lib/smart_proxy_ansible/task_launcher/ansible_runner.rb +48 -0
- data/lib/smart_proxy_ansible/task_launcher/playbook.rb +25 -0
- data/lib/smart_proxy_ansible/version.rb +1 -1
- data/settings.d/ansible.yml.example +1 -1
- metadata +42 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ae7dee62b09f9ea49553d1e2efab2b3f535940be697cf132af28fe0096da30f
|
4
|
+
data.tar.gz: 1997fbe1d05cedb27ee1a8c05d38b2644a25b84e0fbd11a93f201c8f81647643
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30b1c1acfd7ca623ab1bc8ccbbd83cfa0b31c9c99516ea8414e500470fa67b9755a579f3f87d77abaca565f9b31ff1503b335224f7664c40bda8d3e7e4707ddd
|
7
|
+
data.tar.gz: 9135ef0523d5246271da5b559384d7a53b952a9da67301c00b975d3544e365fd73af156b1cdd77989af75ea98990b1d78c33cd8f7ad040dfd0fa62cfb6103645
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'smart_proxy_dynflow/action/runner'
|
4
|
+
|
5
|
+
module Proxy::Ansible
|
6
|
+
module Actions
|
7
|
+
# Action that can be run both on Foreman or Foreman proxy side
|
8
|
+
# to execute the playbook run
|
9
|
+
class RunPlaybook < Proxy::Dynflow::Action::Runner
|
10
|
+
def initiate_runner
|
11
|
+
Proxy::Ansible::Runner::Playbook.new(
|
12
|
+
input[:inventory],
|
13
|
+
input[:playbook],
|
14
|
+
input[:options]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -35,8 +35,8 @@ module Proxy
|
|
35
35
|
role_name_parts = role_name.split('.')
|
36
36
|
if role_name_parts.count == 3
|
37
37
|
RolesReader.collections_paths.split(':').each do |path|
|
38
|
-
variables[role_name]
|
39
|
-
.extract_variables("#{path}/ansible_collections/#{role_name_parts[0]}/#{role_name_parts[1]}/roles/#{role_name_parts[2]}")
|
38
|
+
variables[role_name] ||= VariablesExtractor
|
39
|
+
.extract_variables("#{path}/ansible_collections/#{role_name_parts[0]}/#{role_name_parts[1]}/roles/#{role_name_parts[2]}")
|
40
40
|
end
|
41
41
|
else
|
42
42
|
RolesReader.roles_path.split(':').each do |path|
|
@@ -5,16 +5,24 @@ module Proxy
|
|
5
5
|
rackup_path File.expand_path('http_config.ru', __dir__)
|
6
6
|
settings_file 'ansible.yml'
|
7
7
|
plugin :ansible, Proxy::Ansible::VERSION
|
8
|
+
default_settings :ansible_dir => Dir.home
|
9
|
+
# :working_dir => nil
|
8
10
|
|
9
11
|
after_activation do
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
require 'smart_proxy_dynflow'
|
13
|
+
require 'smart_proxy_dynflow/continuous_output'
|
14
|
+
require 'smart_proxy_ansible/task_launcher/ansible_runner'
|
15
|
+
require 'smart_proxy_ansible/task_launcher/playbook'
|
16
|
+
require 'smart_proxy_ansible/actions'
|
17
|
+
require 'smart_proxy_ansible/remote_execution_core/ansible_runner'
|
18
|
+
require 'smart_proxy_ansible/runner/ansible_runner'
|
19
|
+
require 'smart_proxy_ansible/runner/command_creator'
|
20
|
+
require 'smart_proxy_ansible/runner/playbook'
|
21
|
+
|
22
|
+
Proxy::Dynflow::TaskLauncherRegistry.register('ansible-runner',
|
23
|
+
TaskLauncher::AnsibleRunner)
|
24
|
+
Proxy::Dynflow::TaskLauncherRegistry.register('ansible-playbook',
|
25
|
+
TaskLauncher::Playbook)
|
18
26
|
end
|
19
27
|
end
|
20
28
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'smart_proxy_dynflow/runner/command_runner'
|
4
|
+
|
5
|
+
module Proxy::Ansible
|
6
|
+
module RemoteExecutionCore
|
7
|
+
# Takes an inventory and runs it through REXCore CommandRunner
|
8
|
+
class AnsibleRunner < ::Proxy::Dynflow::Runner::CommandRunner
|
9
|
+
DEFAULT_REFRESH_INTERVAL = 1
|
10
|
+
CONNECTION_PROMPT = 'Are you sure you want to continue connecting (yes/no)? '
|
11
|
+
|
12
|
+
def initialize(options, suspended_action:)
|
13
|
+
super(options, :suspended_action => suspended_action)
|
14
|
+
@playbook_runner = Proxy::Ansible::Runner::Playbook.new(
|
15
|
+
options['ansible_inventory'],
|
16
|
+
options['script'],
|
17
|
+
options,
|
18
|
+
:suspended_action => suspended_action
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
@playbook_runner.logger = logger
|
24
|
+
@playbook_runner.start
|
25
|
+
rescue StandardError => e
|
26
|
+
logger.error(
|
27
|
+
'error while initalizing command'\
|
28
|
+
" #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}"
|
29
|
+
)
|
30
|
+
publish_exception('Error initializing command', e)
|
31
|
+
end
|
32
|
+
|
33
|
+
def fill_continuous_output(continuous_output)
|
34
|
+
delegated_output.fetch('result', []).each do |raw_output|
|
35
|
+
continuous_output.add_raw_output(raw_output)
|
36
|
+
end
|
37
|
+
rescue StandardError => e
|
38
|
+
continuous_output.add_exception(_('Error loading data from proxy'), e)
|
39
|
+
end
|
40
|
+
|
41
|
+
def refresh
|
42
|
+
@command_out = @playbook_runner.command_out
|
43
|
+
@command_in = @playbook_runner.command_in
|
44
|
+
@command_pid = @playbook_runner.command_pid
|
45
|
+
super
|
46
|
+
kill if unknown_host_key_fingerprint?
|
47
|
+
end
|
48
|
+
|
49
|
+
def kill
|
50
|
+
publish_exit_status(1)
|
51
|
+
::Process.kill('SIGTERM', @command_pid)
|
52
|
+
close
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def unknown_host_key_fingerprint?
|
58
|
+
last_output = @continuous_output.raw_outputs.last
|
59
|
+
return if last_output.nil?
|
60
|
+
last_output['output']&.lines&.last == CONNECTION_PROMPT
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
require 'smart_proxy_dynflow/runner/command'
|
4
|
+
require 'smart_proxy_dynflow/runner/base'
|
5
|
+
require 'smart_proxy_dynflow/runner/parent'
|
6
|
+
module Proxy::Ansible
|
7
|
+
module Runner
|
8
|
+
class AnsibleRunner < ::Proxy::Dynflow::Runner::Parent
|
9
|
+
include ::Proxy::Dynflow::Runner::Command
|
10
|
+
|
11
|
+
def initialize(input, suspended_action:)
|
12
|
+
super input, :suspended_action => suspended_action
|
13
|
+
@inventory = rebuild_secrets(rebuild_inventory(input), input)
|
14
|
+
action_input = input.values.first[:input][:action_input]
|
15
|
+
@playbook = action_input[:script]
|
16
|
+
@root = working_dir
|
17
|
+
@verbosity_level = action_input[:verbosity_level]
|
18
|
+
@rex_command = action_input[:remote_execution_command]
|
19
|
+
@check_mode = action_input[:check_mode]
|
20
|
+
@tags = action_input[:tags]
|
21
|
+
@tags_flag = action_input[:tags_flag]
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
prepare_directory_structure
|
26
|
+
write_inventory
|
27
|
+
write_playbook
|
28
|
+
start_ansible_runner
|
29
|
+
end
|
30
|
+
|
31
|
+
def refresh
|
32
|
+
return unless super
|
33
|
+
@counter ||= 1
|
34
|
+
@uuid ||= File.basename(Dir["#{@root}/artifacts/*"].first)
|
35
|
+
job_event_dir = File.join(@root, 'artifacts', @uuid, 'job_events')
|
36
|
+
loop do
|
37
|
+
files = Dir["#{job_event_dir}/*.json"].map do |file|
|
38
|
+
num = File.basename(file)[/\A\d+/].to_i unless file.include?('partial')
|
39
|
+
[file, num]
|
40
|
+
end
|
41
|
+
files_with_nums = files.select { |(_, num)| num && num >= @counter }.sort_by(&:last)
|
42
|
+
break if files_with_nums.empty?
|
43
|
+
logger.debug("[foreman_ansible] - processing event files: #{files_with_nums.map(&:first).inspect}}")
|
44
|
+
files_with_nums.map(&:first).each { |event_file| handle_event_file(event_file) }
|
45
|
+
@counter = files_with_nums.last.last + 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def close
|
50
|
+
super
|
51
|
+
FileUtils.remove_entry(@root) if @tmp_working_dir
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def handle_event_file(event_file)
|
57
|
+
logger.debug("[foreman_ansible] - parsing event file #{event_file}")
|
58
|
+
begin
|
59
|
+
event = JSON.parse(File.read(event_file))
|
60
|
+
if (hostname = event.dig('event_data', 'host'))
|
61
|
+
handle_host_event(hostname, event)
|
62
|
+
else
|
63
|
+
handle_broadcast_data(event)
|
64
|
+
end
|
65
|
+
true
|
66
|
+
rescue JSON::ParserError => e
|
67
|
+
logger.error("[foreman_ansible] - Error parsing runner event at #{event_file}: #{e.class}: #{e.message}")
|
68
|
+
logger.debug(e.backtrace.join("\n"))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_host_event(hostname, event)
|
73
|
+
log_event("for host: #{hostname.inspect}", event)
|
74
|
+
publish_data_for(hostname, event['stdout'] + "\n", 'stdout') if event['stdout']
|
75
|
+
case event['event']
|
76
|
+
when 'runner_on_ok'
|
77
|
+
publish_exit_status_for(hostname, 0) if @exit_statuses[hostname].nil?
|
78
|
+
when 'runner_on_unreachable'
|
79
|
+
publish_exit_status_for(hostname, 1)
|
80
|
+
when 'runner_on_failed'
|
81
|
+
publish_exit_status_for(hostname, 2) if event.dig('event_data', 'ignore_errors').nil?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_broadcast_data(event)
|
86
|
+
log_event("broadcast", event)
|
87
|
+
if event['event'] == 'playbook_on_stats'
|
88
|
+
header, *rows = event['stdout'].strip.lines.map(&:chomp)
|
89
|
+
@outputs.keys.select { |key| key.is_a? String }.each do |host|
|
90
|
+
line = rows.find { |row| row =~ /#{host}/ }
|
91
|
+
publish_data_for(host, [header, line].join("\n"), 'stdout')
|
92
|
+
end
|
93
|
+
else
|
94
|
+
broadcast_data(event['stdout'] + "\n", 'stdout')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def write_inventory
|
99
|
+
path = File.join(@root, 'inventory', 'hosts')
|
100
|
+
data_path = File.join(@root, 'data')
|
101
|
+
inventory_script = <<~INVENTORY_SCRIPT
|
102
|
+
#!/bin/sh
|
103
|
+
cat #{::Shellwords.escape data_path}
|
104
|
+
INVENTORY_SCRIPT
|
105
|
+
File.write(path, inventory_script)
|
106
|
+
File.write(data_path, JSON.dump(@inventory))
|
107
|
+
File.chmod(0o0755, path)
|
108
|
+
end
|
109
|
+
|
110
|
+
def write_playbook
|
111
|
+
File.write(File.join(@root, 'project', 'playbook.yml'), @playbook)
|
112
|
+
end
|
113
|
+
|
114
|
+
def start_ansible_runner
|
115
|
+
env = {}
|
116
|
+
env['FOREMAN_CALLBACK_DISABLE'] = '1' if @rex_command
|
117
|
+
command = [env, 'ansible-runner', 'run', @root, '-p', 'playbook.yml']
|
118
|
+
command << '--cmdline' << cmdline unless cmdline.nil?
|
119
|
+
command << verbosity if verbose?
|
120
|
+
initialize_command(*command)
|
121
|
+
logger.debug("[foreman_ansible] - Running command '#{command.join(' ')}'")
|
122
|
+
end
|
123
|
+
|
124
|
+
def cmdline
|
125
|
+
cmd_args = [tags_cmd, check_cmd].reject(&:empty?)
|
126
|
+
return nil unless cmd_args.any?
|
127
|
+
cmd_args.join(' ')
|
128
|
+
end
|
129
|
+
|
130
|
+
def tags_cmd
|
131
|
+
flag = @tags_flag == 'include' ? '--tags' : '--skip-tags'
|
132
|
+
@tags.empty? ? '' : "#{flag} '#{Array(@tags).join(',')}'"
|
133
|
+
end
|
134
|
+
|
135
|
+
def check_cmd
|
136
|
+
check_mode? ? '--check' : ''
|
137
|
+
end
|
138
|
+
|
139
|
+
def verbosity
|
140
|
+
'-' + 'v' * @verbosity_level.to_i
|
141
|
+
end
|
142
|
+
|
143
|
+
def verbose?
|
144
|
+
@verbosity_level.to_i.positive?
|
145
|
+
end
|
146
|
+
|
147
|
+
def check_mode?
|
148
|
+
@check_mode == true
|
149
|
+
end
|
150
|
+
|
151
|
+
def prepare_directory_structure
|
152
|
+
inner = %w[inventory project].map { |part| File.join(@root, part) }
|
153
|
+
([@root] + inner).each do |path|
|
154
|
+
FileUtils.mkdir_p path
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def log_event(description, event)
|
159
|
+
# TODO: replace this ugly code with block variant once https://github.com/Dynflow/dynflow/pull/323
|
160
|
+
# arrives in production
|
161
|
+
logger.debug("[foreman_ansible] - handling event #{description}: #{JSON.pretty_generate(event)}") if logger.level <= ::Logger::DEBUG
|
162
|
+
end
|
163
|
+
|
164
|
+
# Each per-host task has inventory only for itself, we must
|
165
|
+
# collect all the partial inventories into one large inventory
|
166
|
+
# containing all the hosts.
|
167
|
+
def rebuild_inventory(input)
|
168
|
+
action_inputs = input.values.map { |hash| hash[:input][:action_input] }
|
169
|
+
hostnames = action_inputs.map { |hash| hash[:name] }
|
170
|
+
inventories = action_inputs.map { |hash| hash[:ansible_inventory] }
|
171
|
+
host_vars = inventories.map { |i| i['_meta']['hostvars'] }.reduce({}) do |acc, hosts|
|
172
|
+
hosts.reduce(acc) do |inner_acc, (hostname, vars)|
|
173
|
+
vars[:ansible_ssh_private_key_file] ||= Proxy::RemoteExecution::Ssh::Plugin.settings[:ssh_identity_key_file]
|
174
|
+
inner_acc.merge(hostname => vars)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
{ '_meta' => { 'hostvars' => host_vars },
|
179
|
+
'all' => { 'hosts' => hostnames,
|
180
|
+
'vars' => inventories.first['all']['vars'] } }
|
181
|
+
end
|
182
|
+
|
183
|
+
def working_dir
|
184
|
+
return @root if @root
|
185
|
+
dir = Proxy::Ansible::Plugin.settings[:working_dir]
|
186
|
+
@tmp_working_dir = true
|
187
|
+
if dir.nil?
|
188
|
+
Dir.mktmpdir
|
189
|
+
else
|
190
|
+
Dir.mktmpdir(nil, File.expand_path(dir))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def rebuild_secrets(inventory, input)
|
195
|
+
input.each do |host, host_input|
|
196
|
+
secrets = host_input['input']['action_input']['secrets']
|
197
|
+
per_host = secrets['per-host'][host]
|
198
|
+
|
199
|
+
new_secrets = {
|
200
|
+
'ansible_password' => inventory['ssh_password'] || per_host['ansible_password'],
|
201
|
+
'ansible_become_password' => inventory['effective_user_password'] || per_host['ansible_become_password']
|
202
|
+
}
|
203
|
+
inventory['_meta']['hostvars'][host].update(new_secrets)
|
204
|
+
end
|
205
|
+
|
206
|
+
inventory
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proxy::Ansible
|
4
|
+
# Creates the actual command to be passed to smart_proxy_dynflow to run
|
5
|
+
class CommandCreator
|
6
|
+
def initialize(inventory_file, playbook_file, options = {})
|
7
|
+
@options = options
|
8
|
+
@playbook_file = playbook_file
|
9
|
+
@inventory_file = inventory_file
|
10
|
+
command
|
11
|
+
end
|
12
|
+
|
13
|
+
def command
|
14
|
+
parts = [environment_variables]
|
15
|
+
parts << 'ansible-playbook'
|
16
|
+
parts.concat(command_options)
|
17
|
+
parts << @playbook_file
|
18
|
+
parts
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def environment_variables
|
24
|
+
defaults = { 'JSON_INVENTORY_FILE' => @inventory_file }
|
25
|
+
defaults['ANSIBLE_CALLBACK_WHITELIST'] = '' if rex_command?
|
26
|
+
defaults
|
27
|
+
end
|
28
|
+
|
29
|
+
def command_options
|
30
|
+
opts = ['-i', json_inventory_script]
|
31
|
+
opts.concat([setup_verbosity]) if verbose?
|
32
|
+
opts.concat(['-T', @options[:timeout]]) unless @options[:timeout].nil?
|
33
|
+
opts
|
34
|
+
end
|
35
|
+
|
36
|
+
def json_inventory_script
|
37
|
+
File.expand_path('../../../bin/json_inventory.sh', File.dirname(__FILE__))
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_verbosity
|
41
|
+
verbosity_level = @options[:verbosity_level].to_i
|
42
|
+
verbosity = '-'
|
43
|
+
verbosity_level.times do
|
44
|
+
verbosity += 'v'
|
45
|
+
end
|
46
|
+
verbosity
|
47
|
+
end
|
48
|
+
|
49
|
+
def verbose?
|
50
|
+
verbosity_level = @options[:verbosity_level]
|
51
|
+
# rubocop:disable Rails/Present
|
52
|
+
!verbosity_level.nil? && !verbosity_level.empty? &&
|
53
|
+
verbosity_level.to_i.positive?
|
54
|
+
# rubocop:enable Rails/Present
|
55
|
+
end
|
56
|
+
|
57
|
+
def rex_command?
|
58
|
+
@options[:remote_execution_command]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'smart_proxy_dynflow/continuous_output'
|
4
|
+
require 'smart_proxy_dynflow/runner/command_runner'
|
5
|
+
require_relative 'command_creator'
|
6
|
+
require 'tmpdir'
|
7
|
+
require 'net/ssh'
|
8
|
+
|
9
|
+
module Proxy::Ansible
|
10
|
+
module Runner
|
11
|
+
# Implements Proxy::Dynflow::Runner::Base interface for running
|
12
|
+
# Ansible playbooks, used by the Foreman Ansible plugin and Ansible proxy
|
13
|
+
class Playbook < Proxy::Dynflow::Runner::CommandRunner
|
14
|
+
attr_reader :command_out, :command_in, :command_pid
|
15
|
+
|
16
|
+
def initialize(inventory, playbook, options = {}, suspended_action:)
|
17
|
+
super :suspended_action => suspended_action
|
18
|
+
@inventory = rebuild_secrets(inventory, options[:secrets])
|
19
|
+
unknown_hosts.each do |host|
|
20
|
+
add_to_known_hosts(host)
|
21
|
+
end
|
22
|
+
@playbook = playbook
|
23
|
+
@options = options
|
24
|
+
initialize_dirs
|
25
|
+
end
|
26
|
+
|
27
|
+
def start
|
28
|
+
write_inventory
|
29
|
+
write_playbook
|
30
|
+
command = CommandCreator.new(inventory_file,
|
31
|
+
playbook_file,
|
32
|
+
@options).command
|
33
|
+
logger.debug('[foreman_ansible] - Initializing Ansible Runner')
|
34
|
+
Dir.chdir(@ansible_dir) do
|
35
|
+
initialize_command(*command)
|
36
|
+
logger.debug("[foreman_ansible] - Running command '#{command.join(' ')}'")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def kill
|
41
|
+
publish_data('== TASK ABORTED BY USER ==', 'stdout')
|
42
|
+
publish_exit_status(1)
|
43
|
+
::Process.kill('SIGTERM', @command_pid)
|
44
|
+
close
|
45
|
+
end
|
46
|
+
|
47
|
+
def close
|
48
|
+
super
|
49
|
+
FileUtils.remove_entry(@working_dir) if @tmp_working_dir
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def write_inventory
|
55
|
+
ensure_directory(File.dirname(inventory_file))
|
56
|
+
File.write(inventory_file, JSON.dump(@inventory))
|
57
|
+
end
|
58
|
+
|
59
|
+
def write_playbook
|
60
|
+
ensure_directory(File.dirname(playbook_file))
|
61
|
+
File.write(playbook_file, @playbook)
|
62
|
+
end
|
63
|
+
|
64
|
+
def inventory_file
|
65
|
+
File.join(@working_dir, 'foreman-inventories', id)
|
66
|
+
end
|
67
|
+
|
68
|
+
def playbook_file
|
69
|
+
File.join(@working_dir, "foreman-playbook-#{id}.yml")
|
70
|
+
end
|
71
|
+
|
72
|
+
def events_dir
|
73
|
+
File.join(@working_dir, 'events', id.to_s)
|
74
|
+
end
|
75
|
+
|
76
|
+
def ensure_directory(path)
|
77
|
+
if File.exist?(path)
|
78
|
+
raise "#{path} expected to be a directory" unless File.directory?(path)
|
79
|
+
else
|
80
|
+
FileUtils.mkdir_p(path)
|
81
|
+
end
|
82
|
+
path
|
83
|
+
end
|
84
|
+
|
85
|
+
def initialize_dirs
|
86
|
+
settings = Proxy::Ansible::Plugin.settings
|
87
|
+
initialize_working_dir(settings[:working_dir])
|
88
|
+
initialize_ansible_dir(settings[:ansible_dir])
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize_working_dir(working_dir)
|
92
|
+
if working_dir.nil?
|
93
|
+
@working_dir = Dir.mktmpdir
|
94
|
+
@tmp_working_dir = true
|
95
|
+
else
|
96
|
+
@working_dir = File.expand_path(working_dir)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def initialize_ansible_dir(ansible_dir)
|
101
|
+
raise "Ansible dir #{ansible_dir} does not exist" unless
|
102
|
+
!ansible_dir.nil? && File.exist?(ansible_dir)
|
103
|
+
@ansible_dir = ansible_dir
|
104
|
+
end
|
105
|
+
|
106
|
+
def unknown_hosts
|
107
|
+
@inventory['all']['hosts'].select do |host|
|
108
|
+
Net::SSH::KnownHosts.search_for(host).empty?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_to_known_hosts(host)
|
113
|
+
logger.warn("[foreman_ansible] - Host #{host} not found in known_hosts")
|
114
|
+
Net::SSH::Transport::Session.new(host).host_keys.each do |host_key|
|
115
|
+
Net::SSH::KnownHosts.add(host, host_key)
|
116
|
+
end
|
117
|
+
logger.warn("[foreman_ansible] - Added host key #{host} to known_hosts")
|
118
|
+
rescue StandardError => e
|
119
|
+
logger.error('[foreman_ansible] - Failed to save host key for '\
|
120
|
+
"#{host}: #{e}")
|
121
|
+
end
|
122
|
+
|
123
|
+
def rebuild_secrets(inventory, secrets)
|
124
|
+
inventory['all']['hosts'].each do |name|
|
125
|
+
per_host = secrets['per-host'][name]
|
126
|
+
|
127
|
+
new_secrets = {
|
128
|
+
'ansible_password' => inventory['ssh_password'] || per_host['ansible_password'],
|
129
|
+
'ansible_become_password' => inventory['effective_user_password'] || per_host['ansible_become_password']
|
130
|
+
}
|
131
|
+
inventory['_meta']['hostvars'][name].update(new_secrets)
|
132
|
+
end
|
133
|
+
|
134
|
+
inventory
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'smart_proxy_dynflow/task_launcher/abstract'
|
3
|
+
require 'smart_proxy_dynflow/task_launcher/batch'
|
4
|
+
require 'smart_proxy_dynflow/task_launcher/group'
|
5
|
+
require 'smart_proxy_ansible/runner/ansible_runner'
|
6
|
+
|
7
|
+
module Proxy::Ansible
|
8
|
+
module TaskLauncher
|
9
|
+
class AnsibleRunner < Proxy::Dynflow::TaskLauncher::AbstractGroup
|
10
|
+
def runner_input(input)
|
11
|
+
super(input).reduce({}) do |acc, (_id, data)|
|
12
|
+
acc.merge(data[:input]['action_input']['name'] => data)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def operation
|
17
|
+
'ansible-runner'
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.runner_class
|
21
|
+
Runner::AnsibleRunner
|
22
|
+
end
|
23
|
+
|
24
|
+
# Discard everything apart from hostname to be able to tell the actions
|
25
|
+
# apart when debugging
|
26
|
+
def transform_input(input)
|
27
|
+
{ 'action_input' => input['action_input'].slice('name') }
|
28
|
+
end
|
29
|
+
|
30
|
+
# def self.input_format
|
31
|
+
# {
|
32
|
+
# $UUID => {
|
33
|
+
# :execution_plan_id => $EXECUTION_PLAN_UUID,
|
34
|
+
# :run_step_id => Integer,
|
35
|
+
# :input => {
|
36
|
+
# :action_class => Class,
|
37
|
+
# :action_input => {
|
38
|
+
# "ansible_inventory"=> String,
|
39
|
+
# "hostname"=>"127.0.0.1",
|
40
|
+
# "script"=>"---\n- hosts: all\n tasks:\n - shell: |\n true\n register: out\n - debug: var=out"
|
41
|
+
# }
|
42
|
+
# }
|
43
|
+
# }
|
44
|
+
# }
|
45
|
+
# end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'smart_proxy_dynflow/action/runner'
|
2
|
+
|
3
|
+
module Proxy::Ansible
|
4
|
+
module TaskLauncher
|
5
|
+
class Playbook < Proxy::Dynflow::TaskLauncher::Batch
|
6
|
+
class PlaybookRunnerAction < Proxy::Dynflow::Action::Runner
|
7
|
+
def initiate_runner
|
8
|
+
additional_options = {
|
9
|
+
:step_id => run_step_id,
|
10
|
+
:uuid => execution_plan_id
|
11
|
+
}
|
12
|
+
::Proxy::Ansible::RemoteExecutionCore::AnsibleRunner.new(
|
13
|
+
input.merge(additional_options),
|
14
|
+
:suspended_action => suspended_action
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def child_launcher(parent)
|
20
|
+
::Proxy::Dynflow::TaskLauncher::Single.new(world, callback, :parent => parent,
|
21
|
+
:action_class_override => PlaybookRunnerAction)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_ansible
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nečas
|
8
8
|
- Daniel Lobato
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-06-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -95,20 +95,48 @@ dependencies:
|
|
95
95
|
- - ">="
|
96
96
|
- !ruby/object:Gem::Version
|
97
97
|
version: '0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: net-ssh
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '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'
|
98
112
|
- !ruby/object:Gem::Dependency
|
99
113
|
name: smart_proxy_dynflow
|
100
114
|
requirement: !ruby/object:Gem::Requirement
|
101
115
|
requirements:
|
102
116
|
- - "~>"
|
103
117
|
- !ruby/object:Gem::Version
|
104
|
-
version: '0.
|
118
|
+
version: '0.5'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0.5'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: smart_proxy_remote_execution_ssh
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0.4'
|
105
133
|
type: :runtime
|
106
134
|
prerelease: false
|
107
135
|
version_requirements: !ruby/object:Gem::Requirement
|
108
136
|
requirements:
|
109
137
|
- - "~>"
|
110
138
|
- !ruby/object:Gem::Version
|
111
|
-
version: '0.
|
139
|
+
version: '0.4'
|
112
140
|
description: " Smart-Proxy ansible plugin\n"
|
113
141
|
email:
|
114
142
|
- inecas@redhat.com
|
@@ -123,11 +151,18 @@ files:
|
|
123
151
|
- README.md
|
124
152
|
- bundler.d/ansible.rb
|
125
153
|
- lib/smart_proxy_ansible.rb
|
154
|
+
- lib/smart_proxy_ansible/actions.rb
|
126
155
|
- lib/smart_proxy_ansible/api.rb
|
127
156
|
- lib/smart_proxy_ansible/exception.rb
|
128
157
|
- lib/smart_proxy_ansible/http_config.ru
|
129
158
|
- lib/smart_proxy_ansible/plugin.rb
|
159
|
+
- lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb
|
130
160
|
- lib/smart_proxy_ansible/roles_reader.rb
|
161
|
+
- lib/smart_proxy_ansible/runner/ansible_runner.rb
|
162
|
+
- lib/smart_proxy_ansible/runner/command_creator.rb
|
163
|
+
- lib/smart_proxy_ansible/runner/playbook.rb
|
164
|
+
- lib/smart_proxy_ansible/task_launcher/ansible_runner.rb
|
165
|
+
- lib/smart_proxy_ansible/task_launcher/playbook.rb
|
131
166
|
- lib/smart_proxy_ansible/variables_extractor.rb
|
132
167
|
- lib/smart_proxy_ansible/version.rb
|
133
168
|
- settings.d/ansible.yml.example
|
@@ -135,7 +170,7 @@ homepage: https://github.com/theforeman/smart_proxy_ansible
|
|
135
170
|
licenses:
|
136
171
|
- GPL-3.0
|
137
172
|
metadata: {}
|
138
|
-
post_install_message:
|
173
|
+
post_install_message:
|
139
174
|
rdoc_options: []
|
140
175
|
require_paths:
|
141
176
|
- lib
|
@@ -151,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
186
|
version: '0'
|
152
187
|
requirements: []
|
153
188
|
rubygems_version: 3.1.2
|
154
|
-
signing_key:
|
189
|
+
signing_key:
|
155
190
|
specification_version: 4
|
156
191
|
summary: Smart-Proxy Ansible plugin
|
157
192
|
test_files: []
|