smart_proxy_ansible 3.2.0 → 3.3.1

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: 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