smart_proxy_ansible 3.1.0 → 3.3.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/bin/json_inventory.sh +10 -0
- data/lib/smart_proxy_ansible/actions.rb +19 -0
- data/lib/smart_proxy_ansible/api.rb +11 -3
- data/lib/smart_proxy_ansible/configuration_loader.rb +20 -0
- data/lib/smart_proxy_ansible/exception.rb +3 -0
- data/lib/smart_proxy_ansible/playbooks_reader.rb +37 -0
- data/lib/smart_proxy_ansible/plugin.rb +5 -10
- data/lib/smart_proxy_ansible/reader_helper.rb +44 -0
- data/lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb +64 -0
- data/lib/smart_proxy_ansible/roles_reader.rb +3 -36
- data/lib/smart_proxy_ansible/runner/ansible_runner.rb +253 -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/validate_settings.rb +7 -0
- data/lib/smart_proxy_ansible/version.rb +1 -1
- data/lib/smart_proxy_ansible.rb +4 -0
- data/settings.d/ansible.yml.example +1 -1
- metadata +48 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a1934350dedb103c779cdf61196cff80a43751d260fd5f550d26289dc63f388
|
4
|
+
data.tar.gz: cee72032c8d95fde92021ef8c312e9c4f0408b9ac1d88b5a0ebf9cdd97bc906c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a473838108c4b9cbcef9da311410a2fda5bc39047a2cd4c623533ef718918583de1c830105b92a52303bfc41e1eeb933f1d4a898380a0cf9b01c32d7fb07cb76
|
7
|
+
data.tar.gz: cf1aa8ca722850fd39d312cefcbf178e193fcf391ca8058638dd26db01c288f7350ea6c80e5fb004e87cdda664f5200f24b83b61a2dc3226d88006d0efff7cb3
|
@@ -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
|
@@ -28,15 +28,23 @@ module Proxy
|
|
28
28
|
{}.to_json
|
29
29
|
end
|
30
30
|
|
31
|
+
get '/playbooks_names' do
|
32
|
+
PlaybooksReader.playbooks_names.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
get '/playbooks/:playbooks_names?' do
|
36
|
+
PlaybooksReader.playbooks(params[:playbooks_names]).to_json
|
37
|
+
end
|
38
|
+
|
31
39
|
private
|
32
40
|
|
33
41
|
def extract_variables(role_name)
|
34
42
|
variables = {}
|
35
43
|
role_name_parts = role_name.split('.')
|
36
44
|
if role_name_parts.count == 3
|
37
|
-
|
38
|
-
variables[role_name]
|
39
|
-
.extract_variables("#{path}/ansible_collections/#{role_name_parts[0]}/#{role_name_parts[1]}/roles/#{role_name_parts[2]}")
|
45
|
+
ReaderHelper.collections_paths.split(':').each do |path|
|
46
|
+
variables[role_name] = VariablesExtractor
|
47
|
+
.extract_variables("#{path}/ansible_collections/#{role_name_parts[0]}/#{role_name_parts[1]}/roles/#{role_name_parts[2]}") if variables[role_name].nil? || variables[role_name].empty?
|
40
48
|
end
|
41
49
|
else
|
42
50
|
RolesReader.roles_path.split(':').each do |path|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Proxy::Ansible
|
2
|
+
class ConfigurationLoader
|
3
|
+
def load_classes
|
4
|
+
require 'smart_proxy_dynflow'
|
5
|
+
require 'smart_proxy_dynflow/continuous_output'
|
6
|
+
require 'smart_proxy_ansible/task_launcher/ansible_runner'
|
7
|
+
require 'smart_proxy_ansible/task_launcher/playbook'
|
8
|
+
require 'smart_proxy_ansible/actions'
|
9
|
+
require 'smart_proxy_ansible/remote_execution_core/ansible_runner'
|
10
|
+
require 'smart_proxy_ansible/runner/ansible_runner'
|
11
|
+
require 'smart_proxy_ansible/runner/command_creator'
|
12
|
+
require 'smart_proxy_ansible/runner/playbook'
|
13
|
+
|
14
|
+
Proxy::Dynflow::TaskLauncherRegistry.register('ansible-runner',
|
15
|
+
TaskLauncher::AnsibleRunner)
|
16
|
+
Proxy::Dynflow::TaskLauncherRegistry.register('ansible-playbook',
|
17
|
+
TaskLauncher::Playbook)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -37,5 +37,8 @@ module Proxy
|
|
37
37
|
class ReadConfigFileException < Proxy::Ansible::Exception; end
|
38
38
|
class ReadRolesException < Proxy::Ansible::Exception; end
|
39
39
|
class ReadVariablesException < Proxy::Ansible::Exception; end
|
40
|
+
class NotExistingWorkingDirException < Proxy::Ansible::Exception; end
|
41
|
+
class ReadPlaybooksNamesException < Proxy::Ansible::Exception; end
|
42
|
+
class ReadPlaybooksException < Proxy::Ansible::Exception; end
|
40
43
|
end
|
41
44
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Proxy
|
2
|
+
module Ansible
|
3
|
+
# Implements the logic needed to read the playbooks and associated information
|
4
|
+
class PlaybooksReader
|
5
|
+
class << self
|
6
|
+
def playbooks_names
|
7
|
+
ReaderHelper.collections_paths.split(':').flat_map { |path| get_playbooks_names(path) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def playbooks(playbooks_to_import)
|
11
|
+
ReaderHelper.collections_paths.split(':').reduce([]) do |playbooks, path|
|
12
|
+
playbooks.concat(read_collection_playbooks(path, playbooks_to_import))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_playbooks_names(collections_path)
|
17
|
+
Dir.glob("#{collections_path}/ansible_collections/*/*/playbooks/*").map do |path|
|
18
|
+
ReaderHelper.playbook_or_role_full_name(path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def read_collection_playbooks(collections_path, playbooks_to_import = nil)
|
23
|
+
Dir.glob("#{collections_path}/ansible_collections/*/*/playbooks/*").map do |path|
|
24
|
+
name = ReaderHelper.playbook_or_role_full_name(path)
|
25
|
+
{
|
26
|
+
name: name,
|
27
|
+
playbooks_content: File.readlines(path)
|
28
|
+
} if playbooks_to_import.nil? || playbooks_to_import.include?(name)
|
29
|
+
end.compact
|
30
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
31
|
+
message = "Could not read Ansible playbooks #{collections_path} - #{e.message}"
|
32
|
+
raise ReadPlaybooksException, message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -5,17 +5,12 @@ 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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
require 'foreman_ansible_core'
|
13
|
-
ForemanAnsibleCore.initialize_settings(Proxy::Ansible::Plugin.settings.to_h)
|
14
|
-
rescue LoadError => _
|
15
|
-
# Dynflow core is not available in the proxy, will be handled
|
16
|
-
# by standalone Dynflow core
|
17
|
-
end
|
18
|
-
end
|
11
|
+
load_classes ::Proxy::Ansible::ConfigurationLoader
|
12
|
+
load_validators :validate_settings => ::Proxy::Ansible::ValidateSettings
|
13
|
+
validate :validate!, :validate_settings => nil
|
19
14
|
end
|
20
15
|
end
|
21
16
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Proxy
|
2
|
+
module Ansible
|
3
|
+
# Helper for Playbooks Reader
|
4
|
+
class ReaderHelper
|
5
|
+
class << self
|
6
|
+
DEFAULT_COLLECTIONS_PATHS = '/etc/ansible/collections:/usr/share/ansible/collections'.freeze
|
7
|
+
DEFAULT_CONFIG_FILE = '/etc/ansible/ansible.cfg'.freeze
|
8
|
+
|
9
|
+
def collections_paths
|
10
|
+
config_path(path_from_config('collections_paths'), DEFAULT_COLLECTIONS_PATHS)
|
11
|
+
end
|
12
|
+
|
13
|
+
def config_path(config_line, default)
|
14
|
+
return default if config_line.empty?
|
15
|
+
|
16
|
+
config_line_key = config_line.first.split('=').first.strip
|
17
|
+
# In case of commented roles_path key "#roles_path" or #collections_paths, return default
|
18
|
+
return default if ['#roles_path', '#collections_paths'].include?(config_line_key)
|
19
|
+
|
20
|
+
config_line.first.split('=').last.strip
|
21
|
+
end
|
22
|
+
|
23
|
+
def path_from_config(config_key)
|
24
|
+
File.readlines(DEFAULT_CONFIG_FILE).select do |line|
|
25
|
+
line =~ /^\s*#{config_key}/
|
26
|
+
end
|
27
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
28
|
+
RolesReader.logger.debug(e.backtrace)
|
29
|
+
message = "Could not read Ansible config file #{DEFAULT_CONFIG_FILE} - #{e.message}"
|
30
|
+
raise ReadConfigFileException.new(message), message
|
31
|
+
end
|
32
|
+
|
33
|
+
def playbook_or_role_full_name(path)
|
34
|
+
parts = path.split('/')
|
35
|
+
playbook = parts.pop
|
36
|
+
parts.pop
|
37
|
+
collection = parts.pop
|
38
|
+
author = parts.pop
|
39
|
+
"#{author}.#{collection}.#{playbook}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
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
|
@@ -5,33 +5,16 @@ module Proxy
|
|
5
5
|
# Implements the logic needed to read the roles and associated information
|
6
6
|
class RolesReader
|
7
7
|
class << self
|
8
|
-
DEFAULT_CONFIG_FILE = '/etc/ansible/ansible.cfg'.freeze
|
9
8
|
DEFAULT_ROLES_PATH = '/etc/ansible/roles:/usr/share/ansible/roles'.freeze
|
10
|
-
DEFAULT_COLLECTIONS_PATHS = '/etc/ansible/collections:/usr/share/ansible/collections'.freeze
|
11
9
|
|
12
10
|
def list_roles
|
13
11
|
roles = roles_path.split(':').map { |path| read_roles(path) }.flatten
|
14
|
-
collection_roles = collections_paths.split(':').map { |path| read_collection_roles(path) }.flatten
|
12
|
+
collection_roles = ReaderHelper.collections_paths.split(':').map { |path| read_collection_roles(path) }.flatten
|
15
13
|
roles + collection_roles
|
16
14
|
end
|
17
15
|
|
18
16
|
def roles_path
|
19
|
-
config_path(path_from_config('roles_path'), DEFAULT_ROLES_PATH)
|
20
|
-
end
|
21
|
-
|
22
|
-
def collections_paths
|
23
|
-
config_path(path_from_config('collections_paths'), DEFAULT_COLLECTIONS_PATHS)
|
24
|
-
end
|
25
|
-
|
26
|
-
def config_path(config_line, default)
|
27
|
-
# Default to /etc/ansible/roles if config_line is empty
|
28
|
-
return default if config_line.empty?
|
29
|
-
|
30
|
-
config_line_key = config_line.first.split('=').first.strip
|
31
|
-
# In case of commented roles_path key "#roles_path" or #collections_paths, return default
|
32
|
-
return default if ['#roles_path', '#collections_paths'].include?(config_line_key)
|
33
|
-
|
34
|
-
config_line.first.split('=').last.strip
|
17
|
+
ReaderHelper.config_path(ReaderHelper.path_from_config('roles_path'), DEFAULT_ROLES_PATH)
|
35
18
|
end
|
36
19
|
|
37
20
|
def logger
|
@@ -58,33 +41,17 @@ module Proxy
|
|
58
41
|
|
59
42
|
def glob_path(path)
|
60
43
|
Dir.glob path
|
61
|
-
|
62
44
|
end
|
63
45
|
|
64
46
|
def read_collection_roles(collections_path)
|
65
47
|
Dir.glob("#{collections_path}/ansible_collections/*/*/roles/*").map do |path|
|
66
|
-
|
67
|
-
role = parts.pop
|
68
|
-
parts.pop
|
69
|
-
collection = parts.pop
|
70
|
-
author = parts.pop
|
71
|
-
"#{author}.#{collection}.#{role}"
|
48
|
+
ReaderHelper.playbook_or_role_full_name(path)
|
72
49
|
end
|
73
50
|
rescue Errno::ENOENT, Errno::EACCES => e
|
74
51
|
logger.debug(e.backtrace)
|
75
52
|
message = "Could not read Ansible roles #{collections_path} - #{e.message}"
|
76
53
|
raise ReadRolesException.new(message), message
|
77
54
|
end
|
78
|
-
|
79
|
-
def path_from_config(config_key)
|
80
|
-
File.readlines(DEFAULT_CONFIG_FILE).select do |line|
|
81
|
-
line =~ /^\s*#{config_key}/
|
82
|
-
end
|
83
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
84
|
-
logger.debug(e.backtrace)
|
85
|
-
message = "Could not read Ansible config file #{DEFAULT_CONFIG_FILE} - #{e.message}"
|
86
|
-
raise ReadConfigFileException.new(message), message
|
87
|
-
end
|
88
55
|
end
|
89
56
|
end
|
90
57
|
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'smart_proxy_dynflow/runner/command'
|
5
|
+
require 'smart_proxy_dynflow/runner/base'
|
6
|
+
require 'smart_proxy_dynflow/runner/parent'
|
7
|
+
module Proxy::Ansible
|
8
|
+
module Runner
|
9
|
+
class AnsibleRunner < ::Proxy::Dynflow::Runner::Parent
|
10
|
+
include ::Proxy::Dynflow::Runner::Command
|
11
|
+
attr_reader :execution_timeout_interval, :command_pid
|
12
|
+
|
13
|
+
def initialize(input, suspended_action:)
|
14
|
+
super input, :suspended_action => suspended_action
|
15
|
+
@inventory = rebuild_secrets(rebuild_inventory(input), input)
|
16
|
+
action_input = input.values.first[:input][:action_input]
|
17
|
+
@playbook = action_input[:script]
|
18
|
+
@root = working_dir
|
19
|
+
@verbosity_level = action_input[:verbosity_level]
|
20
|
+
@rex_command = action_input[:remote_execution_command]
|
21
|
+
@check_mode = action_input[:check_mode]
|
22
|
+
@tags = action_input[:tags]
|
23
|
+
@tags_flag = action_input[:tags_flag]
|
24
|
+
@passphrase = action_input['secrets']['key_passphrase']
|
25
|
+
@execution_timeout_interval = action_input[:execution_timeout_interval]
|
26
|
+
@cleanup_working_dirs = action_input.fetch(:cleanup_working_dirs, true)
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
prepare_directory_structure
|
31
|
+
write_inventory
|
32
|
+
write_playbook
|
33
|
+
write_ssh_key if !@passphrase.nil? && !@passphrase.empty?
|
34
|
+
start_ansible_runner
|
35
|
+
end
|
36
|
+
|
37
|
+
def refresh
|
38
|
+
return unless super
|
39
|
+
@counter ||= 1
|
40
|
+
@uuid ||= File.basename(Dir["#{@root}/artifacts/*"].first)
|
41
|
+
job_event_dir = File.join(@root, 'artifacts', @uuid, 'job_events')
|
42
|
+
loop do
|
43
|
+
files = Dir["#{job_event_dir}/*.json"].map do |file|
|
44
|
+
num = File.basename(file)[/\A\d+/].to_i unless file.include?('partial')
|
45
|
+
[file, num]
|
46
|
+
end
|
47
|
+
files_with_nums = files.select { |(_, num)| num && num >= @counter }.sort_by(&:last)
|
48
|
+
break if files_with_nums.empty?
|
49
|
+
logger.debug("[foreman_ansible] - processing event files: #{files_with_nums.map(&:first).inspect}}")
|
50
|
+
files_with_nums.map(&:first).each { |event_file| handle_event_file(event_file) }
|
51
|
+
@counter = files_with_nums.last.last + 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def timeout
|
56
|
+
logger.debug('job timed out')
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
def timeout_interval
|
61
|
+
execution_timeout_interval
|
62
|
+
end
|
63
|
+
|
64
|
+
def kill
|
65
|
+
::Process.kill('SIGTERM', @command_pid)
|
66
|
+
publish_exit_status(2)
|
67
|
+
@inventory['all']['hosts'].each { |hostname| @exit_statuses[hostname] = 2 }
|
68
|
+
broadcast_data('Timeout for execution passed, stopping the job', 'stderr')
|
69
|
+
close
|
70
|
+
end
|
71
|
+
|
72
|
+
def close
|
73
|
+
super
|
74
|
+
FileUtils.remove_entry(@root) if @tmp_working_dir && Dir.exist?(@root) && @cleanup_working_dirs
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def handle_event_file(event_file)
|
80
|
+
logger.debug("[foreman_ansible] - parsing event file #{event_file}")
|
81
|
+
begin
|
82
|
+
event = JSON.parse(File.read(event_file))
|
83
|
+
if (hostname = event.dig('event_data', 'host'))
|
84
|
+
handle_host_event(hostname, event)
|
85
|
+
else
|
86
|
+
handle_broadcast_data(event)
|
87
|
+
end
|
88
|
+
true
|
89
|
+
rescue JSON::ParserError => e
|
90
|
+
logger.error("[foreman_ansible] - Error parsing runner event at #{event_file}: #{e.class}: #{e.message}")
|
91
|
+
logger.debug(e.backtrace.join("\n"))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def handle_host_event(hostname, event)
|
96
|
+
log_event("for host: #{hostname.inspect}", event)
|
97
|
+
publish_data_for(hostname, event['stdout'] + "\n", 'stdout') if event['stdout']
|
98
|
+
case event['event']
|
99
|
+
when 'runner_on_ok'
|
100
|
+
publish_exit_status_for(hostname, 0) if @exit_statuses[hostname].nil?
|
101
|
+
when 'runner_on_unreachable'
|
102
|
+
publish_exit_status_for(hostname, 1)
|
103
|
+
when 'runner_on_failed'
|
104
|
+
publish_exit_status_for(hostname, 2) if event.dig('event_data', 'ignore_errors').nil?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_broadcast_data(event)
|
109
|
+
log_event("broadcast", event)
|
110
|
+
if event['event'] == 'playbook_on_stats'
|
111
|
+
failures = event.dig('event_data', 'failures') || {}
|
112
|
+
unreachable = event.dig('event_data', 'dark') || {}
|
113
|
+
header, *rows = event['stdout'].strip.lines.map(&:chomp)
|
114
|
+
@outputs.keys.select { |key| key.is_a? String }.each do |host|
|
115
|
+
line = rows.find { |row| row =~ /#{host}/ }
|
116
|
+
publish_data_for(host, [header, line].join("\n"), 'stdout')
|
117
|
+
|
118
|
+
# If the task has been rescued, it won't consider a failure
|
119
|
+
if @exit_statuses[host].to_i != 0 && failures[host].to_i <= 0 && unreachable[host].to_i <= 0
|
120
|
+
publish_exit_status_for(host, 0)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
else
|
124
|
+
broadcast_data(event['stdout'] + "\n", 'stdout')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def write_inventory
|
129
|
+
path = File.join(@root, 'inventory', 'hosts')
|
130
|
+
data_path = File.join(@root, 'data')
|
131
|
+
inventory_script = <<~INVENTORY_SCRIPT
|
132
|
+
#!/bin/sh
|
133
|
+
cat #{::Shellwords.escape data_path}
|
134
|
+
INVENTORY_SCRIPT
|
135
|
+
File.write(path, inventory_script)
|
136
|
+
File.write(data_path, JSON.dump(@inventory))
|
137
|
+
File.chmod(0o0755, path)
|
138
|
+
end
|
139
|
+
|
140
|
+
def write_playbook
|
141
|
+
File.write(File.join(@root, 'project', 'playbook.yml'), @playbook)
|
142
|
+
end
|
143
|
+
|
144
|
+
def write_ssh_key
|
145
|
+
key_path = File.join(@root, 'env', 'ssh_key')
|
146
|
+
File.symlink(File.expand_path(Proxy::RemoteExecution::Ssh::Plugin.settings[:ssh_identity_key_file]), key_path)
|
147
|
+
|
148
|
+
passwords_path = File.join(@root, 'env', 'passwords')
|
149
|
+
# here we create a secrets file for ansible-runner, which uses the key as regexp
|
150
|
+
# to match line asking for password, given the limitation to match only first 100 chars
|
151
|
+
# and the fact the line contains dynamically created temp directory, the regexp
|
152
|
+
# mentions only things that are always there, such as artifacts directory and the key name
|
153
|
+
secrets = YAML.dump({ "for.*/artifacts/.*/ssh_key_data:" => @passphrase })
|
154
|
+
File.write(passwords_path, secrets, perm: 0o600)
|
155
|
+
end
|
156
|
+
|
157
|
+
def start_ansible_runner
|
158
|
+
env = {}
|
159
|
+
env['FOREMAN_CALLBACK_DISABLE'] = '1' if @rex_command
|
160
|
+
command = [env, 'ansible-runner', 'run', @root, '-p', 'playbook.yml']
|
161
|
+
command << '--cmdline' << cmdline unless cmdline.nil?
|
162
|
+
command << verbosity if verbose?
|
163
|
+
initialize_command(*command)
|
164
|
+
logger.debug("[foreman_ansible] - Running command '#{command.join(' ')}'")
|
165
|
+
end
|
166
|
+
|
167
|
+
def cmdline
|
168
|
+
cmd_args = [tags_cmd, check_cmd].reject(&:empty?)
|
169
|
+
return nil unless cmd_args.any?
|
170
|
+
cmd_args.join(' ')
|
171
|
+
end
|
172
|
+
|
173
|
+
def tags_cmd
|
174
|
+
flag = @tags_flag == 'include' ? '--tags' : '--skip-tags'
|
175
|
+
@tags.empty? ? '' : "#{flag} '#{Array(@tags).join(',')}'"
|
176
|
+
end
|
177
|
+
|
178
|
+
def check_cmd
|
179
|
+
check_mode? ? '"--check"' : ''
|
180
|
+
end
|
181
|
+
|
182
|
+
def verbosity
|
183
|
+
'-' + 'v' * @verbosity_level.to_i
|
184
|
+
end
|
185
|
+
|
186
|
+
def verbose?
|
187
|
+
@verbosity_level.to_i.positive?
|
188
|
+
end
|
189
|
+
|
190
|
+
def check_mode?
|
191
|
+
@check_mode == true && @rex_command == false
|
192
|
+
end
|
193
|
+
|
194
|
+
def prepare_directory_structure
|
195
|
+
inner = %w[inventory project env].map { |part| File.join(@root, part) }
|
196
|
+
([@root] + inner).each do |path|
|
197
|
+
FileUtils.mkdir_p path
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def log_event(description, event)
|
202
|
+
# TODO: replace this ugly code with block variant once https://github.com/Dynflow/dynflow/pull/323
|
203
|
+
# arrives in production
|
204
|
+
logger.debug("[foreman_ansible] - handling event #{description}: #{JSON.pretty_generate(event)}") if logger.level <= ::Logger::DEBUG
|
205
|
+
end
|
206
|
+
|
207
|
+
# Each per-host task has inventory only for itself, we must
|
208
|
+
# collect all the partial inventories into one large inventory
|
209
|
+
# containing all the hosts.
|
210
|
+
def rebuild_inventory(input)
|
211
|
+
action_inputs = input.values.map { |hash| hash[:input][:action_input] }
|
212
|
+
hostnames = action_inputs.map { |hash| hash[:name] }
|
213
|
+
inventories = action_inputs.map { |hash| hash[:ansible_inventory] }
|
214
|
+
host_vars = inventories.map { |i| i['_meta']['hostvars'] }.reduce({}) do |acc, hosts|
|
215
|
+
hosts.reduce(acc) do |inner_acc, (hostname, vars)|
|
216
|
+
vars[:ansible_ssh_private_key_file] ||= Proxy::RemoteExecution::Ssh::Plugin.settings[:ssh_identity_key_file]
|
217
|
+
inner_acc.merge(hostname => vars)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
{ '_meta' => { 'hostvars' => host_vars },
|
222
|
+
'all' => { 'hosts' => hostnames,
|
223
|
+
'vars' => inventories.first['all']['vars'] } }
|
224
|
+
end
|
225
|
+
|
226
|
+
def working_dir
|
227
|
+
return @root if @root
|
228
|
+
dir = Proxy::Ansible::Plugin.settings[:working_dir]
|
229
|
+
@tmp_working_dir = true
|
230
|
+
if dir.nil?
|
231
|
+
Dir.mktmpdir
|
232
|
+
else
|
233
|
+
Dir.mktmpdir(nil, File.expand_path(dir))
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def rebuild_secrets(inventory, input)
|
238
|
+
input.each do |host, host_input|
|
239
|
+
secrets = host_input['input']['action_input']['secrets']
|
240
|
+
per_host = secrets['per-host'][host]
|
241
|
+
|
242
|
+
new_secrets = {
|
243
|
+
'ansible_password' => inventory['ssh_password'] || per_host['ansible_password'],
|
244
|
+
'ansible_become_password' => inventory['effective_user_password'] || per_host['ansible_become_password']
|
245
|
+
}
|
246
|
+
inventory['_meta']['hostvars'][host].update(new_secrets)
|
247
|
+
end
|
248
|
+
|
249
|
+
inventory
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
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' => super['action_input'].slice('name', :task_id) }
|
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
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module Proxy::Ansible
|
2
|
+
class ValidateSettings < ::Proxy::PluginValidators::Base
|
3
|
+
def validate!(settings)
|
4
|
+
raise NotExistingWorkingDirException.new("Working directory does not exist") unless settings[:working_dir].nil? || File.directory?(File.expand_path(settings[:working_dir]))
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
data/lib/smart_proxy_ansible.rb
CHANGED
@@ -4,8 +4,12 @@ module Proxy
|
|
4
4
|
# Basic requires for this plugin
|
5
5
|
module Ansible
|
6
6
|
require 'smart_proxy_ansible/version'
|
7
|
+
require 'smart_proxy_ansible/configuration_loader'
|
8
|
+
require 'smart_proxy_ansible/validate_settings'
|
7
9
|
require 'smart_proxy_ansible/plugin'
|
8
10
|
require 'smart_proxy_ansible/roles_reader'
|
11
|
+
require 'smart_proxy_ansible/playbooks_reader'
|
12
|
+
require 'smart_proxy_ansible/reader_helper'
|
9
13
|
require 'smart_proxy_ansible/variables_extractor'
|
10
14
|
end
|
11
15
|
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.3.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:
|
12
|
+
date: 2022-01-17 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
|
@@ -121,13 +149,25 @@ extra_rdoc_files:
|
|
121
149
|
files:
|
122
150
|
- LICENSE
|
123
151
|
- README.md
|
152
|
+
- bin/json_inventory.sh
|
124
153
|
- bundler.d/ansible.rb
|
125
154
|
- lib/smart_proxy_ansible.rb
|
155
|
+
- lib/smart_proxy_ansible/actions.rb
|
126
156
|
- lib/smart_proxy_ansible/api.rb
|
157
|
+
- lib/smart_proxy_ansible/configuration_loader.rb
|
127
158
|
- lib/smart_proxy_ansible/exception.rb
|
128
159
|
- lib/smart_proxy_ansible/http_config.ru
|
160
|
+
- lib/smart_proxy_ansible/playbooks_reader.rb
|
129
161
|
- lib/smart_proxy_ansible/plugin.rb
|
162
|
+
- lib/smart_proxy_ansible/reader_helper.rb
|
163
|
+
- lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb
|
130
164
|
- lib/smart_proxy_ansible/roles_reader.rb
|
165
|
+
- lib/smart_proxy_ansible/runner/ansible_runner.rb
|
166
|
+
- lib/smart_proxy_ansible/runner/command_creator.rb
|
167
|
+
- lib/smart_proxy_ansible/runner/playbook.rb
|
168
|
+
- lib/smart_proxy_ansible/task_launcher/ansible_runner.rb
|
169
|
+
- lib/smart_proxy_ansible/task_launcher/playbook.rb
|
170
|
+
- lib/smart_proxy_ansible/validate_settings.rb
|
131
171
|
- lib/smart_proxy_ansible/variables_extractor.rb
|
132
172
|
- lib/smart_proxy_ansible/version.rb
|
133
173
|
- settings.d/ansible.yml.example
|
@@ -135,7 +175,7 @@ homepage: https://github.com/theforeman/smart_proxy_ansible
|
|
135
175
|
licenses:
|
136
176
|
- GPL-3.0
|
137
177
|
metadata: {}
|
138
|
-
post_install_message:
|
178
|
+
post_install_message:
|
139
179
|
rdoc_options: []
|
140
180
|
require_paths:
|
141
181
|
- lib
|
@@ -150,8 +190,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
150
190
|
- !ruby/object:Gem::Version
|
151
191
|
version: '0'
|
152
192
|
requirements: []
|
153
|
-
rubygems_version: 3.
|
154
|
-
signing_key:
|
193
|
+
rubygems_version: 3.3.4
|
194
|
+
signing_key:
|
155
195
|
specification_version: 4
|
156
196
|
summary: Smart-Proxy Ansible plugin
|
157
197
|
test_files: []
|