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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac08fa54d51dd2a0a2079a42cf9ac4dfd9dfd42d655c89965d300d33a1e0a7e2
4
- data.tar.gz: 4821962c66ebaf696fbc7421e140fdc9f4d881c6b2c573040a73a18fe23a7f7b
3
+ metadata.gz: fe91bd15ae06f9e9cc9e2d54fce101129acd7fd39f8f9c3d970909e5bf029545
4
+ data.tar.gz: d6e887989f068bfa795ceada359e68d911fbeba3d308d3eb0a52e35b1571c3fd
5
5
  SHA512:
6
- metadata.gz: d58290eca4ea0defdf5de915450c5671b76736e95bf80642e105c7624db06a274062937d2b0b29d5c1596c2a3ed8a70372535769ba6adcd21db170e487eef9b4
7
- data.tar.gz: 8e2f782678f2741780b81ed6fb445923761138217b0c95855c25559c576a6cb3df9fa9ed263e6d7f7c1cccfc544b2955a1ac17605df89e81afc6c98fb3babfe4
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 in the proxy
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.1.5
15
- at least, and [foreman-tasks-core](https://github.com/theforeman/foreman-tasks) as
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
- RolesReader.collections_paths.split(':').each do |path|
38
- variables[role_name] ||= VariablesExtractor
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
- after_activation do
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)
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
- parts = path.split('/')
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/command'
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::Command
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 refresh
32
- return unless super
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 ||= File.basename(Dir["#{@root}/artifacts/*"].first)
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
- { 'action_input' => super['action_input'].slice('name', :task_id) }
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
@@ -2,6 +2,6 @@ module Proxy
2
2
  # Version, this allows the proxy and other plugins know
3
3
  # what version of the Ansible plugin is running
4
4
  module Ansible
5
- VERSION = '3.2.2'
5
+ VERSION = '3.4.0'
6
6
  end
7
7
  end
@@ -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.2.2
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: 2022-02-07 00:00:00.000000000 Z
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.5'
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.5'
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/remote_execution_core/ansible_runner.rb
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/task_launcher/playbook.rb
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.1.2
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