smart_proxy_ansible 3.2.2 → 3.4.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/README.md +3 -3
- data/lib/smart_proxy_ansible/api.rb +11 -3
- data/lib/smart_proxy_ansible/configuration_loader.rb +13 -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 +3 -16
- data/lib/smart_proxy_ansible/reader_helper.rb +44 -0
- data/lib/smart_proxy_ansible/roles_reader.rb +3 -36
- data/lib/smart_proxy_ansible/runner/ansible_runner.rb +78 -17
- data/lib/smart_proxy_ansible/task_launcher/ansible_runner.rb +2 -1
- 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
- metadata +9 -10
- data/lib/smart_proxy_ansible/actions.rb +0 -19
- data/lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb +0 -64
- data/lib/smart_proxy_ansible/runner/command_creator.rb +0 -61
- data/lib/smart_proxy_ansible/runner/playbook.rb +0 -138
- data/lib/smart_proxy_ansible/task_launcher/playbook.rb +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe91bd15ae06f9e9cc9e2d54fce101129acd7fd39f8f9c3d970909e5bf029545
|
|
4
|
+
data.tar.gz: d6e887989f068bfa795ceada359e68d911fbeba3d308d3eb0a52e35b1571c3fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfc145591dd06dad090a054332802674e30f2c5b51ef22809b1dcd3a42e24f587cb4e577c9e41740ff691290fe4621610ba092c888f35eaba4fa3fa73a9f90dc
|
|
7
|
+
data.tar.gz: 53dfb48a0c82802307cb05df916a48d146bce2505148d8eb67162ef4f87bbeab626c02ecfed46e151051d12c6af1efea7c757ac50f24768654ff39933fa2b485
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smart-proxy Ansible plugin
|
|
2
2
|
|
|
3
|
-
Proxy plugin to make [foreman_ansible](https://github.com/theforeman/foreman_ansible) actions run
|
|
3
|
+
Proxy plugin to make [foreman_ansible](https://github.com/theforeman/foreman_ansible) actions run on the proxy.
|
|
4
4
|
|
|
5
5
|
## Compatibility
|
|
6
6
|
|
|
@@ -11,8 +11,8 @@ This plugin requires at least Foreman Proxy 2.3.
|
|
|
11
11
|
### Prerequisites
|
|
12
12
|
|
|
13
13
|
We expect your proxy to also have
|
|
14
|
-
[smart_proxy_dynflow](https://github.com/theforeman/smart_proxy_dynflow) 0.
|
|
15
|
-
at least, and [
|
|
14
|
+
[smart_proxy_dynflow](https://github.com/theforeman/smart_proxy_dynflow) 0.5
|
|
15
|
+
at least, and [smart_proxy_remote_execution_ssh](https://github.com/theforeman/smart_proxy_remote_execution_ssh) 0.4 as
|
|
16
16
|
a gem requirement.
|
|
17
17
|
|
|
18
18
|
### Get the code
|
|
@@ -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,13 @@
|
|
|
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/runner/ansible_runner'
|
|
8
|
+
|
|
9
|
+
Proxy::Dynflow::TaskLauncherRegistry.register('ansible-runner',
|
|
10
|
+
TaskLauncher::AnsibleRunner)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
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.read(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
|
|
@@ -8,22 +8,9 @@ module Proxy
|
|
|
8
8
|
default_settings :ansible_dir => Dir.home
|
|
9
9
|
# :working_dir => nil
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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)
|
|
26
|
-
end
|
|
11
|
+
load_classes ::Proxy::Ansible::ConfigurationLoader
|
|
12
|
+
load_validators :validate_settings => ::Proxy::Ansible::ValidateSettings
|
|
13
|
+
validate :validate!, :validate_settings => nil
|
|
27
14
|
end
|
|
28
15
|
end
|
|
29
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.sub(/\.ya?ml/, '')
|
|
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
|
|
@@ -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
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
require 'shellwords'
|
|
2
|
+
require 'yaml'
|
|
2
3
|
|
|
3
|
-
require 'smart_proxy_dynflow/runner/
|
|
4
|
+
require 'smart_proxy_dynflow/runner/process_manager_command'
|
|
4
5
|
require 'smart_proxy_dynflow/runner/base'
|
|
5
6
|
require 'smart_proxy_dynflow/runner/parent'
|
|
6
7
|
module Proxy::Ansible
|
|
7
8
|
module Runner
|
|
8
9
|
class AnsibleRunner < ::Proxy::Dynflow::Runner::Parent
|
|
9
|
-
include ::Proxy::Dynflow::Runner::
|
|
10
|
+
include ::Proxy::Dynflow::Runner::ProcessManagerCommand
|
|
11
|
+
attr_reader :execution_timeout_interval
|
|
10
12
|
|
|
11
|
-
def initialize(input, suspended_action:)
|
|
12
|
-
super input, :suspended_action => suspended_action
|
|
13
|
+
def initialize(input, suspended_action:, id: nil)
|
|
14
|
+
super input, :suspended_action => suspended_action, :id => id
|
|
13
15
|
@inventory = rebuild_secrets(rebuild_inventory(input), input)
|
|
14
16
|
action_input = input.values.first[:input][:action_input]
|
|
15
17
|
@playbook = action_input[:script]
|
|
@@ -19,19 +21,65 @@ module Proxy::Ansible
|
|
|
19
21
|
@check_mode = action_input[:check_mode]
|
|
20
22
|
@tags = action_input[:tags]
|
|
21
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)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def start
|
|
25
30
|
prepare_directory_structure
|
|
26
31
|
write_inventory
|
|
27
32
|
write_playbook
|
|
33
|
+
write_ssh_key if !@passphrase.nil? && !@passphrase.empty?
|
|
28
34
|
start_ansible_runner
|
|
29
35
|
end
|
|
30
36
|
|
|
31
|
-
def
|
|
32
|
-
|
|
37
|
+
def run_refresh_output
|
|
38
|
+
logger.debug('refreshing runner on demand')
|
|
39
|
+
process_artifacts
|
|
40
|
+
generate_updates
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def timeout
|
|
44
|
+
logger.debug('job timed out')
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def timeout_interval
|
|
49
|
+
execution_timeout_interval
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def kill
|
|
53
|
+
::Process.kill('SIGTERM', @process_manager.pid)
|
|
54
|
+
publish_exit_status(2)
|
|
55
|
+
@inventory['all']['hosts'].each { |hostname| @exit_statuses[hostname] = 2 }
|
|
56
|
+
broadcast_data('Timeout for execution passed, stopping the job', 'stderr')
|
|
57
|
+
close
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def close
|
|
61
|
+
super
|
|
62
|
+
FileUtils.remove_entry(@root) if @tmp_working_dir && Dir.exist?(@root) && @cleanup_working_dirs
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def publish_exit_status(status)
|
|
66
|
+
process_artifacts
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def initialize_command(*command)
|
|
71
|
+
super
|
|
72
|
+
@process_manager.stdin.close unless @process_manager.done?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def process_artifacts
|
|
33
78
|
@counter ||= 1
|
|
34
|
-
@uuid ||=
|
|
79
|
+
@uuid ||= if (f = Dir["#{@root}/artifacts/*"].first)
|
|
80
|
+
File.basename(f)
|
|
81
|
+
end
|
|
82
|
+
return unless @uuid
|
|
35
83
|
job_event_dir = File.join(@root, 'artifacts', @uuid, 'job_events')
|
|
36
84
|
loop do
|
|
37
85
|
files = Dir["#{job_event_dir}/*.json"].map do |file|
|
|
@@ -46,13 +94,6 @@ module Proxy::Ansible
|
|
|
46
94
|
end
|
|
47
95
|
end
|
|
48
96
|
|
|
49
|
-
def close
|
|
50
|
-
super
|
|
51
|
-
FileUtils.remove_entry(@root) if @tmp_working_dir
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
97
|
def handle_event_file(event_file)
|
|
57
98
|
logger.debug("[foreman_ansible] - parsing event file #{event_file}")
|
|
58
99
|
begin
|
|
@@ -85,10 +126,17 @@ module Proxy::Ansible
|
|
|
85
126
|
def handle_broadcast_data(event)
|
|
86
127
|
log_event("broadcast", event)
|
|
87
128
|
if event['event'] == 'playbook_on_stats'
|
|
129
|
+
failures = event.dig('event_data', 'failures') || {}
|
|
130
|
+
unreachable = event.dig('event_data', 'dark') || {}
|
|
88
131
|
header, *rows = event['stdout'].strip.lines.map(&:chomp)
|
|
89
132
|
@outputs.keys.select { |key| key.is_a? String }.each do |host|
|
|
90
133
|
line = rows.find { |row| row =~ /#{host}/ }
|
|
91
134
|
publish_data_for(host, [header, line].join("\n"), 'stdout')
|
|
135
|
+
|
|
136
|
+
# If the task has been rescued, it won't consider a failure
|
|
137
|
+
if @exit_statuses[host].to_i != 0 && failures[host].to_i <= 0 && unreachable[host].to_i <= 0
|
|
138
|
+
publish_exit_status_for(host, 0)
|
|
139
|
+
end
|
|
92
140
|
end
|
|
93
141
|
else
|
|
94
142
|
broadcast_data(event['stdout'] + "\n", 'stdout')
|
|
@@ -111,6 +159,19 @@ module Proxy::Ansible
|
|
|
111
159
|
File.write(File.join(@root, 'project', 'playbook.yml'), @playbook)
|
|
112
160
|
end
|
|
113
161
|
|
|
162
|
+
def write_ssh_key
|
|
163
|
+
key_path = File.join(@root, 'env', 'ssh_key')
|
|
164
|
+
File.symlink(File.expand_path(Proxy::RemoteExecution::Ssh::Plugin.settings[:ssh_identity_key_file]), key_path)
|
|
165
|
+
|
|
166
|
+
passwords_path = File.join(@root, 'env', 'passwords')
|
|
167
|
+
# here we create a secrets file for ansible-runner, which uses the key as regexp
|
|
168
|
+
# to match line asking for password, given the limitation to match only first 100 chars
|
|
169
|
+
# and the fact the line contains dynamically created temp directory, the regexp
|
|
170
|
+
# mentions only things that are always there, such as artifacts directory and the key name
|
|
171
|
+
secrets = YAML.dump({ "for.*/artifacts/.*/ssh_key_data:" => @passphrase })
|
|
172
|
+
File.write(passwords_path, secrets, perm: 0o600)
|
|
173
|
+
end
|
|
174
|
+
|
|
114
175
|
def start_ansible_runner
|
|
115
176
|
env = {}
|
|
116
177
|
env['FOREMAN_CALLBACK_DISABLE'] = '1' if @rex_command
|
|
@@ -133,7 +194,7 @@ module Proxy::Ansible
|
|
|
133
194
|
end
|
|
134
195
|
|
|
135
196
|
def check_cmd
|
|
136
|
-
check_mode? ? '--check' : ''
|
|
197
|
+
check_mode? ? '"--check"' : ''
|
|
137
198
|
end
|
|
138
199
|
|
|
139
200
|
def verbosity
|
|
@@ -145,11 +206,11 @@ module Proxy::Ansible
|
|
|
145
206
|
end
|
|
146
207
|
|
|
147
208
|
def check_mode?
|
|
148
|
-
@check_mode == true
|
|
209
|
+
@check_mode == true && @rex_command == false
|
|
149
210
|
end
|
|
150
211
|
|
|
151
212
|
def prepare_directory_structure
|
|
152
|
-
inner = %w[inventory project].map { |part| File.join(@root, part) }
|
|
213
|
+
inner = %w[inventory project env].map { |part| File.join(@root, part) }
|
|
153
214
|
([@root] + inner).each do |path|
|
|
154
215
|
FileUtils.mkdir_p path
|
|
155
216
|
end
|
|
@@ -24,7 +24,8 @@ module Proxy::Ansible
|
|
|
24
24
|
# Discard everything apart from hostname to be able to tell the actions
|
|
25
25
|
# apart when debugging
|
|
26
26
|
def transform_input(input)
|
|
27
|
-
|
|
27
|
+
action_input = super['action_input']
|
|
28
|
+
{ 'action_input' => { 'name' => action_input['name'], :task_id => action_input[:task_id], :runner_id => action_input[:runner_id] } }
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# def self.input_format
|
|
@@ -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,7 +1,7 @@
|
|
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Nečas
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rake
|
|
@@ -115,14 +115,14 @@ dependencies:
|
|
|
115
115
|
requirements:
|
|
116
116
|
- - "~>"
|
|
117
117
|
- !ruby/object:Gem::Version
|
|
118
|
-
version: '0.
|
|
118
|
+
version: '0.8'
|
|
119
119
|
type: :runtime
|
|
120
120
|
prerelease: false
|
|
121
121
|
version_requirements: !ruby/object:Gem::Requirement
|
|
122
122
|
requirements:
|
|
123
123
|
- - "~>"
|
|
124
124
|
- !ruby/object:Gem::Version
|
|
125
|
-
version: '0.
|
|
125
|
+
version: '0.8'
|
|
126
126
|
- !ruby/object:Gem::Dependency
|
|
127
127
|
name: smart_proxy_remote_execution_ssh
|
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -152,18 +152,17 @@ files:
|
|
|
152
152
|
- bin/json_inventory.sh
|
|
153
153
|
- bundler.d/ansible.rb
|
|
154
154
|
- lib/smart_proxy_ansible.rb
|
|
155
|
-
- lib/smart_proxy_ansible/actions.rb
|
|
156
155
|
- lib/smart_proxy_ansible/api.rb
|
|
156
|
+
- lib/smart_proxy_ansible/configuration_loader.rb
|
|
157
157
|
- lib/smart_proxy_ansible/exception.rb
|
|
158
158
|
- lib/smart_proxy_ansible/http_config.ru
|
|
159
|
+
- lib/smart_proxy_ansible/playbooks_reader.rb
|
|
159
160
|
- lib/smart_proxy_ansible/plugin.rb
|
|
160
|
-
- lib/smart_proxy_ansible/
|
|
161
|
+
- lib/smart_proxy_ansible/reader_helper.rb
|
|
161
162
|
- lib/smart_proxy_ansible/roles_reader.rb
|
|
162
163
|
- lib/smart_proxy_ansible/runner/ansible_runner.rb
|
|
163
|
-
- lib/smart_proxy_ansible/runner/command_creator.rb
|
|
164
|
-
- lib/smart_proxy_ansible/runner/playbook.rb
|
|
165
164
|
- lib/smart_proxy_ansible/task_launcher/ansible_runner.rb
|
|
166
|
-
- lib/smart_proxy_ansible/
|
|
165
|
+
- lib/smart_proxy_ansible/validate_settings.rb
|
|
167
166
|
- lib/smart_proxy_ansible/variables_extractor.rb
|
|
168
167
|
- lib/smart_proxy_ansible/version.rb
|
|
169
168
|
- settings.d/ansible.yml.example
|
|
@@ -186,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
186
185
|
- !ruby/object:Gem::Version
|
|
187
186
|
version: '0'
|
|
188
187
|
requirements: []
|
|
189
|
-
rubygems_version: 3.
|
|
188
|
+
rubygems_version: 3.2.26
|
|
190
189
|
signing_key:
|
|
191
190
|
specification_version: 4
|
|
192
191
|
summary: Smart-Proxy Ansible plugin
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
|
@@ -1,64 +0,0 @@
|
|
|
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
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
|
@@ -1,138 +0,0 @@
|
|
|
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
|
|
@@ -1,25 +0,0 @@
|
|
|
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
|