smart_proxy_ansible 3.2.2 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
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