smart_proxy_ansible 3.2.2 → 3.3.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: 4a1934350dedb103c779cdf61196cff80a43751d260fd5f550d26289dc63f388
4
+ data.tar.gz: cee72032c8d95fde92021ef8c312e9c4f0408b9ac1d88b5a0ebf9cdd97bc906c
5
5
  SHA512:
6
- metadata.gz: d58290eca4ea0defdf5de915450c5671b76736e95bf80642e105c7624db06a274062937d2b0b29d5c1596c2a3ed8a70372535769ba6adcd21db170e487eef9b4
7
- data.tar.gz: 8e2f782678f2741780b81ed6fb445923761138217b0c95855c25559c576a6cb3df9fa9ed263e6d7f7c1cccfc544b2955a1ac17605df89e81afc6c98fb3babfe4
6
+ metadata.gz: a473838108c4b9cbcef9da311410a2fda5bc39047a2cd4c623533ef718918583de1c830105b92a52303bfc41e1eeb933f1d4a898380a0cf9b01c32d7fb07cb76
7
+ data.tar.gz: cf1aa8ca722850fd39d312cefcbf178e193fcf391ca8058638dd26db01c288f7350ea6c80e5fb004e87cdda664f5200f24b83b61a2dc3226d88006d0efff7cb3
@@ -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,20 @@
1
+ module Proxy::Ansible
2
+ class ConfigurationLoader
3
+ def load_classes
4
+ require 'smart_proxy_dynflow'
5
+ require 'smart_proxy_dynflow/continuous_output'
6
+ require 'smart_proxy_ansible/task_launcher/ansible_runner'
7
+ require 'smart_proxy_ansible/task_launcher/playbook'
8
+ require 'smart_proxy_ansible/actions'
9
+ require 'smart_proxy_ansible/remote_execution_core/ansible_runner'
10
+ require 'smart_proxy_ansible/runner/ansible_runner'
11
+ require 'smart_proxy_ansible/runner/command_creator'
12
+ require 'smart_proxy_ansible/runner/playbook'
13
+
14
+ Proxy::Dynflow::TaskLauncherRegistry.register('ansible-runner',
15
+ TaskLauncher::AnsibleRunner)
16
+ Proxy::Dynflow::TaskLauncherRegistry.register('ansible-playbook',
17
+ TaskLauncher::Playbook)
18
+ end
19
+ end
20
+ end
@@ -37,5 +37,8 @@ module Proxy
37
37
  class ReadConfigFileException < Proxy::Ansible::Exception; end
38
38
  class ReadRolesException < Proxy::Ansible::Exception; end
39
39
  class ReadVariablesException < Proxy::Ansible::Exception; end
40
+ class NotExistingWorkingDirException < Proxy::Ansible::Exception; end
41
+ class ReadPlaybooksNamesException < Proxy::Ansible::Exception; end
42
+ class ReadPlaybooksException < Proxy::Ansible::Exception; end
40
43
  end
41
44
  end
@@ -0,0 +1,37 @@
1
+ module Proxy
2
+ module Ansible
3
+ # Implements the logic needed to read the playbooks and associated information
4
+ class PlaybooksReader
5
+ class << self
6
+ def playbooks_names
7
+ ReaderHelper.collections_paths.split(':').flat_map { |path| get_playbooks_names(path) }
8
+ end
9
+
10
+ def playbooks(playbooks_to_import)
11
+ ReaderHelper.collections_paths.split(':').reduce([]) do |playbooks, path|
12
+ playbooks.concat(read_collection_playbooks(path, playbooks_to_import))
13
+ end
14
+ end
15
+
16
+ def get_playbooks_names(collections_path)
17
+ Dir.glob("#{collections_path}/ansible_collections/*/*/playbooks/*").map do |path|
18
+ ReaderHelper.playbook_or_role_full_name(path)
19
+ end
20
+ end
21
+
22
+ def read_collection_playbooks(collections_path, playbooks_to_import = nil)
23
+ Dir.glob("#{collections_path}/ansible_collections/*/*/playbooks/*").map do |path|
24
+ name = ReaderHelper.playbook_or_role_full_name(path)
25
+ {
26
+ name: name,
27
+ playbooks_content: File.readlines(path)
28
+ } if playbooks_to_import.nil? || playbooks_to_import.include?(name)
29
+ end.compact
30
+ rescue Errno::ENOENT, Errno::EACCES => e
31
+ message = "Could not read Ansible playbooks #{collections_path} - #{e.message}"
32
+ raise ReadPlaybooksException, message
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -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
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,4 +1,5 @@
1
1
  require 'shellwords'
2
+ require 'yaml'
2
3
 
3
4
  require 'smart_proxy_dynflow/runner/command'
4
5
  require 'smart_proxy_dynflow/runner/base'
@@ -7,6 +8,7 @@ module Proxy::Ansible
7
8
  module Runner
8
9
  class AnsibleRunner < ::Proxy::Dynflow::Runner::Parent
9
10
  include ::Proxy::Dynflow::Runner::Command
11
+ attr_reader :execution_timeout_interval, :command_pid
10
12
 
11
13
  def initialize(input, suspended_action:)
12
14
  super input, :suspended_action => suspended_action
@@ -19,12 +21,16 @@ 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
 
@@ -46,9 +52,26 @@ module Proxy::Ansible
46
52
  end
47
53
  end
48
54
 
55
+ def timeout
56
+ logger.debug('job timed out')
57
+ super
58
+ end
59
+
60
+ def timeout_interval
61
+ execution_timeout_interval
62
+ end
63
+
64
+ def kill
65
+ ::Process.kill('SIGTERM', @command_pid)
66
+ publish_exit_status(2)
67
+ @inventory['all']['hosts'].each { |hostname| @exit_statuses[hostname] = 2 }
68
+ broadcast_data('Timeout for execution passed, stopping the job', 'stderr')
69
+ close
70
+ end
71
+
49
72
  def close
50
73
  super
51
- FileUtils.remove_entry(@root) if @tmp_working_dir
74
+ FileUtils.remove_entry(@root) if @tmp_working_dir && Dir.exist?(@root) && @cleanup_working_dirs
52
75
  end
53
76
 
54
77
  private
@@ -85,10 +108,17 @@ module Proxy::Ansible
85
108
  def handle_broadcast_data(event)
86
109
  log_event("broadcast", event)
87
110
  if event['event'] == 'playbook_on_stats'
111
+ failures = event.dig('event_data', 'failures') || {}
112
+ unreachable = event.dig('event_data', 'dark') || {}
88
113
  header, *rows = event['stdout'].strip.lines.map(&:chomp)
89
114
  @outputs.keys.select { |key| key.is_a? String }.each do |host|
90
115
  line = rows.find { |row| row =~ /#{host}/ }
91
116
  publish_data_for(host, [header, line].join("\n"), 'stdout')
117
+
118
+ # If the task has been rescued, it won't consider a failure
119
+ if @exit_statuses[host].to_i != 0 && failures[host].to_i <= 0 && unreachable[host].to_i <= 0
120
+ publish_exit_status_for(host, 0)
121
+ end
92
122
  end
93
123
  else
94
124
  broadcast_data(event['stdout'] + "\n", 'stdout')
@@ -111,6 +141,19 @@ module Proxy::Ansible
111
141
  File.write(File.join(@root, 'project', 'playbook.yml'), @playbook)
112
142
  end
113
143
 
144
+ def write_ssh_key
145
+ key_path = File.join(@root, 'env', 'ssh_key')
146
+ File.symlink(File.expand_path(Proxy::RemoteExecution::Ssh::Plugin.settings[:ssh_identity_key_file]), key_path)
147
+
148
+ passwords_path = File.join(@root, 'env', 'passwords')
149
+ # here we create a secrets file for ansible-runner, which uses the key as regexp
150
+ # to match line asking for password, given the limitation to match only first 100 chars
151
+ # and the fact the line contains dynamically created temp directory, the regexp
152
+ # mentions only things that are always there, such as artifacts directory and the key name
153
+ secrets = YAML.dump({ "for.*/artifacts/.*/ssh_key_data:" => @passphrase })
154
+ File.write(passwords_path, secrets, perm: 0o600)
155
+ end
156
+
114
157
  def start_ansible_runner
115
158
  env = {}
116
159
  env['FOREMAN_CALLBACK_DISABLE'] = '1' if @rex_command
@@ -133,7 +176,7 @@ module Proxy::Ansible
133
176
  end
134
177
 
135
178
  def check_cmd
136
- check_mode? ? '--check' : ''
179
+ check_mode? ? '"--check"' : ''
137
180
  end
138
181
 
139
182
  def verbosity
@@ -145,11 +188,11 @@ module Proxy::Ansible
145
188
  end
146
189
 
147
190
  def check_mode?
148
- @check_mode == true
191
+ @check_mode == true && @rex_command == false
149
192
  end
150
193
 
151
194
  def prepare_directory_structure
152
- inner = %w[inventory project].map { |part| File.join(@root, part) }
195
+ inner = %w[inventory project env].map { |part| File.join(@root, part) }
153
196
  ([@root] + inner).each do |path|
154
197
  FileUtils.mkdir_p path
155
198
  end
@@ -0,0 +1,7 @@
1
+ module Proxy::Ansible
2
+ class ValidateSettings < ::Proxy::PluginValidators::Base
3
+ def validate!(settings)
4
+ raise NotExistingWorkingDirException.new("Working directory does not exist") unless settings[:working_dir].nil? || File.directory?(File.expand_path(settings[:working_dir]))
5
+ end
6
+ end
7
+ end
@@ -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.3.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.3.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: 2022-01-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -154,9 +154,12 @@ files:
154
154
  - lib/smart_proxy_ansible.rb
155
155
  - lib/smart_proxy_ansible/actions.rb
156
156
  - lib/smart_proxy_ansible/api.rb
157
+ - lib/smart_proxy_ansible/configuration_loader.rb
157
158
  - lib/smart_proxy_ansible/exception.rb
158
159
  - lib/smart_proxy_ansible/http_config.ru
160
+ - lib/smart_proxy_ansible/playbooks_reader.rb
159
161
  - lib/smart_proxy_ansible/plugin.rb
162
+ - lib/smart_proxy_ansible/reader_helper.rb
160
163
  - lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb
161
164
  - lib/smart_proxy_ansible/roles_reader.rb
162
165
  - lib/smart_proxy_ansible/runner/ansible_runner.rb
@@ -164,6 +167,7 @@ files:
164
167
  - lib/smart_proxy_ansible/runner/playbook.rb
165
168
  - lib/smart_proxy_ansible/task_launcher/ansible_runner.rb
166
169
  - lib/smart_proxy_ansible/task_launcher/playbook.rb
170
+ - lib/smart_proxy_ansible/validate_settings.rb
167
171
  - lib/smart_proxy_ansible/variables_extractor.rb
168
172
  - lib/smart_proxy_ansible/version.rb
169
173
  - settings.d/ansible.yml.example
@@ -186,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
190
  - !ruby/object:Gem::Version
187
191
  version: '0'
188
192
  requirements: []
189
- rubygems_version: 3.1.2
193
+ rubygems_version: 3.3.4
190
194
  signing_key:
191
195
  specification_version: 4
192
196
  summary: Smart-Proxy Ansible plugin