smart_proxy_ansible 3.2.0 → 3.3.1

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: 9ae7dee62b09f9ea49553d1e2efab2b3f535940be697cf132af28fe0096da30f
4
- data.tar.gz: 1997fbe1d05cedb27ee1a8c05d38b2644a25b84e0fbd11a93f201c8f81647643
3
+ metadata.gz: 1139197a8fd9b944e4bec3a0dcf1ed18951bffb88457663927ba427ca4f1b372
4
+ data.tar.gz: 0b774027ceedca7a36714f7725704261996efdbfe835f08bcc6fbe6a7beabc22
5
5
  SHA512:
6
- metadata.gz: 30b1c1acfd7ca623ab1bc8ccbbd83cfa0b31c9c99516ea8414e500470fa67b9755a579f3f87d77abaca565f9b31ff1503b335224f7664c40bda8d3e7e4707ddd
7
- data.tar.gz: 9135ef0523d5246271da5b559384d7a53b952a9da67301c00b975d3544e365fd73af156b1cdd77989af75ea98990b1d78c33cd8f7ad040dfd0fa62cfb6103645
6
+ metadata.gz: 2e5535a4a9d2100e9d68321be99e23b331100e4bb8b2b1e4d7104ab7216064db9f9361c68ac6989fe7197217441fe574087278606c848be65a2362634001d818
7
+ data.tar.gz: 2704d69243f82a099282d5289de914bb754826a9d32a92a93d1ad0c747f512ebe006ac2869554c26616126e3085233a6a166d0670130b8844d0e52e89f4182f5
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # An inventory script to load the inventory from JSON file.
3
+ #
4
+
5
+ if [ -z "$JSON_INVENTORY_FILE" ]; then
6
+ echo "JSON_INVENTORY_FILE not specified"
7
+ exit 1
8
+ fi
9
+
10
+ cat $JSON_INVENTORY_FILE
@@ -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,19 +21,36 @@ 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
 
37
+ def initialize_command(*command)
38
+ r, w = IO.pipe
39
+
40
+ @command_pid = spawn(*command, :out => w, :err => w, :in => '/dev/null')
41
+ @command_out = r
42
+ w.close
43
+ rescue Errno::ENOENT => e
44
+ publish_exception("Error running command '#{command.join(' ')}'", e)
45
+ end
46
+
31
47
  def refresh
32
- return unless super
48
+ super
49
+ @uuid ||= if (f = Dir["#{@root}/artifacts/*"].first)
50
+ File.basename(f)
51
+ end
52
+ return unless @uuid
33
53
  @counter ||= 1
34
- @uuid ||= File.basename(Dir["#{@root}/artifacts/*"].first)
35
54
  job_event_dir = File.join(@root, 'artifacts', @uuid, 'job_events')
36
55
  loop do
37
56
  files = Dir["#{job_event_dir}/*.json"].map do |file|
@@ -46,9 +65,26 @@ module Proxy::Ansible
46
65
  end
47
66
  end
48
67
 
68
+ def timeout
69
+ logger.debug('job timed out')
70
+ super
71
+ end
72
+
73
+ def timeout_interval
74
+ execution_timeout_interval
75
+ end
76
+
77
+ def kill
78
+ ::Process.kill('SIGTERM', @command_pid)
79
+ publish_exit_status(2)
80
+ @inventory['all']['hosts'].each { |hostname| @exit_statuses[hostname] = 2 }
81
+ broadcast_data('Timeout for execution passed, stopping the job', 'stderr')
82
+ close
83
+ end
84
+
49
85
  def close
50
86
  super
51
- FileUtils.remove_entry(@root) if @tmp_working_dir
87
+ FileUtils.remove_entry(@root) if @tmp_working_dir && Dir.exist?(@root) && @cleanup_working_dirs
52
88
  end
53
89
 
54
90
  private
@@ -85,10 +121,17 @@ module Proxy::Ansible
85
121
  def handle_broadcast_data(event)
86
122
  log_event("broadcast", event)
87
123
  if event['event'] == 'playbook_on_stats'
124
+ failures = event.dig('event_data', 'failures') || {}
125
+ unreachable = event.dig('event_data', 'dark') || {}
88
126
  header, *rows = event['stdout'].strip.lines.map(&:chomp)
89
127
  @outputs.keys.select { |key| key.is_a? String }.each do |host|
90
128
  line = rows.find { |row| row =~ /#{host}/ }
91
129
  publish_data_for(host, [header, line].join("\n"), 'stdout')
130
+
131
+ # If the task has been rescued, it won't consider a failure
132
+ if @exit_statuses[host].to_i != 0 && failures[host].to_i <= 0 && unreachable[host].to_i <= 0
133
+ publish_exit_status_for(host, 0)
134
+ end
92
135
  end
93
136
  else
94
137
  broadcast_data(event['stdout'] + "\n", 'stdout')
@@ -111,6 +154,19 @@ module Proxy::Ansible
111
154
  File.write(File.join(@root, 'project', 'playbook.yml'), @playbook)
112
155
  end
113
156
 
157
+ def write_ssh_key
158
+ key_path = File.join(@root, 'env', 'ssh_key')
159
+ File.symlink(File.expand_path(Proxy::RemoteExecution::Ssh::Plugin.settings[:ssh_identity_key_file]), key_path)
160
+
161
+ passwords_path = File.join(@root, 'env', 'passwords')
162
+ # here we create a secrets file for ansible-runner, which uses the key as regexp
163
+ # to match line asking for password, given the limitation to match only first 100 chars
164
+ # and the fact the line contains dynamically created temp directory, the regexp
165
+ # mentions only things that are always there, such as artifacts directory and the key name
166
+ secrets = YAML.dump({ "for.*/artifacts/.*/ssh_key_data:" => @passphrase })
167
+ File.write(passwords_path, secrets, perm: 0o600)
168
+ end
169
+
114
170
  def start_ansible_runner
115
171
  env = {}
116
172
  env['FOREMAN_CALLBACK_DISABLE'] = '1' if @rex_command
@@ -133,7 +189,7 @@ module Proxy::Ansible
133
189
  end
134
190
 
135
191
  def check_cmd
136
- check_mode? ? '--check' : ''
192
+ check_mode? ? '"--check"' : ''
137
193
  end
138
194
 
139
195
  def verbosity
@@ -145,11 +201,11 @@ module Proxy::Ansible
145
201
  end
146
202
 
147
203
  def check_mode?
148
- @check_mode == true
204
+ @check_mode == true && @rex_command == false
149
205
  end
150
206
 
151
207
  def prepare_directory_structure
152
- inner = %w[inventory project].map { |part| File.join(@root, part) }
208
+ inner = %w[inventory project env].map { |part| File.join(@root, part) }
153
209
  ([@root] + inner).each do |path|
154
210
  FileUtils.mkdir_p path
155
211
  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' => input['action_input'].slice('name') }
27
+ action_input = super['action_input']
28
+ { 'action_input' => { 'name' => action_input['name'], :task_id => action_input[:task_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.0'
5
+ VERSION = '3.3.1'
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.0
4
+ version: 3.3.1
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: 2021-06-23 00:00:00.000000000 Z
12
+ date: 2022-02-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -149,13 +149,17 @@ extra_rdoc_files:
149
149
  files:
150
150
  - LICENSE
151
151
  - README.md
152
+ - bin/json_inventory.sh
152
153
  - bundler.d/ansible.rb
153
154
  - lib/smart_proxy_ansible.rb
154
155
  - lib/smart_proxy_ansible/actions.rb
155
156
  - lib/smart_proxy_ansible/api.rb
157
+ - lib/smart_proxy_ansible/configuration_loader.rb
156
158
  - lib/smart_proxy_ansible/exception.rb
157
159
  - lib/smart_proxy_ansible/http_config.ru
160
+ - lib/smart_proxy_ansible/playbooks_reader.rb
158
161
  - lib/smart_proxy_ansible/plugin.rb
162
+ - lib/smart_proxy_ansible/reader_helper.rb
159
163
  - lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb
160
164
  - lib/smart_proxy_ansible/roles_reader.rb
161
165
  - lib/smart_proxy_ansible/runner/ansible_runner.rb
@@ -163,6 +167,7 @@ files:
163
167
  - lib/smart_proxy_ansible/runner/playbook.rb
164
168
  - lib/smart_proxy_ansible/task_launcher/ansible_runner.rb
165
169
  - lib/smart_proxy_ansible/task_launcher/playbook.rb
170
+ - lib/smart_proxy_ansible/validate_settings.rb
166
171
  - lib/smart_proxy_ansible/variables_extractor.rb
167
172
  - lib/smart_proxy_ansible/version.rb
168
173
  - settings.d/ansible.yml.example
@@ -185,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
190
  - !ruby/object:Gem::Version
186
191
  version: '0'
187
192
  requirements: []
188
- rubygems_version: 3.1.2
193
+ rubygems_version: 3.3.4
189
194
  signing_key:
190
195
  specification_version: 4
191
196
  summary: Smart-Proxy Ansible plugin