smart_proxy_ansible 3.3.0 → 3.4.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: 4a1934350dedb103c779cdf61196cff80a43751d260fd5f550d26289dc63f388
4
- data.tar.gz: cee72032c8d95fde92021ef8c312e9c4f0408b9ac1d88b5a0ebf9cdd97bc906c
3
+ metadata.gz: 7e690f2c224c01d97752317ca99ae54ac84a01f45fc2d613c727477d0acbcdcf
4
+ data.tar.gz: 0ec2124072f46f08a9820dee2311dce4a72c44980701ed3ab12697788be1e2ca
5
5
  SHA512:
6
- metadata.gz: a473838108c4b9cbcef9da311410a2fda5bc39047a2cd4c623533ef718918583de1c830105b92a52303bfc41e1eeb933f1d4a898380a0cf9b01c32d7fb07cb76
7
- data.tar.gz: cf1aa8ca722850fd39d312cefcbf178e193fcf391ca8058638dd26db01c288f7350ea6c80e5fb004e87cdda664f5200f24b83b61a2dc3226d88006d0efff7cb3
6
+ metadata.gz: 1b9e32a10bec5a241ea8e6d5aa9727f67e31b7cac1f47b6181ab14177942a48d722d23d54dc324a1fdfb19e0f57d442f80126a93e06331551700210628d8b7c8
7
+ data.tar.gz: 83f106b39297d92d521a2472db2c6c8c5a8d60b6391ea88b16a455e2d01070df6acb03ce2138474e8790a6d2495b897a1e118d4954af5115cba6f914a1e0de13
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
@@ -4,17 +4,10 @@ module Proxy::Ansible
4
4
  require 'smart_proxy_dynflow'
5
5
  require 'smart_proxy_dynflow/continuous_output'
6
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
7
  require 'smart_proxy_ansible/runner/ansible_runner'
11
- require 'smart_proxy_ansible/runner/command_creator'
12
- require 'smart_proxy_ansible/runner/playbook'
13
8
 
14
9
  Proxy::Dynflow::TaskLauncherRegistry.register('ansible-runner',
15
10
  TaskLauncher::AnsibleRunner)
16
- Proxy::Dynflow::TaskLauncherRegistry.register('ansible-playbook',
17
- TaskLauncher::Playbook)
18
11
  end
19
12
  end
20
13
  end
@@ -24,7 +24,7 @@ module Proxy
24
24
  name = ReaderHelper.playbook_or_role_full_name(path)
25
25
  {
26
26
  name: name,
27
- playbooks_content: File.readlines(path)
27
+ playbooks_content: File.read(path)
28
28
  } if playbooks_to_import.nil? || playbooks_to_import.include?(name)
29
29
  end.compact
30
30
  rescue Errno::ENOENT, Errno::EACCES => e
@@ -32,7 +32,7 @@ module Proxy
32
32
 
33
33
  def playbook_or_role_full_name(path)
34
34
  parts = path.split('/')
35
- playbook = parts.pop
35
+ playbook = parts.pop.sub(/\.ya?ml/, '')
36
36
  parts.pop
37
37
  collection = parts.pop
38
38
  author = parts.pop
@@ -1,17 +1,17 @@
1
1
  require 'shellwords'
2
2
  require 'yaml'
3
3
 
4
- require 'smart_proxy_dynflow/runner/command'
4
+ require 'smart_proxy_dynflow/runner/process_manager_command'
5
5
  require 'smart_proxy_dynflow/runner/base'
6
6
  require 'smart_proxy_dynflow/runner/parent'
7
7
  module Proxy::Ansible
8
8
  module Runner
9
9
  class AnsibleRunner < ::Proxy::Dynflow::Runner::Parent
10
- include ::Proxy::Dynflow::Runner::Command
11
- attr_reader :execution_timeout_interval, :command_pid
10
+ include ::Proxy::Dynflow::Runner::ProcessManagerCommand
11
+ attr_reader :execution_timeout_interval
12
12
 
13
- def initialize(input, suspended_action:)
14
- super input, :suspended_action => suspended_action
13
+ def initialize(input, suspended_action:, id: nil)
14
+ super input, :suspended_action => suspended_action, :id => id
15
15
  @inventory = rebuild_secrets(rebuild_inventory(input), input)
16
16
  action_input = input.values.first[:input][:action_input]
17
17
  @playbook = action_input[:script]
@@ -34,22 +34,10 @@ module Proxy::Ansible
34
34
  start_ansible_runner
35
35
  end
36
36
 
37
- def refresh
38
- return unless super
39
- @counter ||= 1
40
- @uuid ||= File.basename(Dir["#{@root}/artifacts/*"].first)
41
- job_event_dir = File.join(@root, 'artifacts', @uuid, 'job_events')
42
- loop do
43
- files = Dir["#{job_event_dir}/*.json"].map do |file|
44
- num = File.basename(file)[/\A\d+/].to_i unless file.include?('partial')
45
- [file, num]
46
- end
47
- files_with_nums = files.select { |(_, num)| num && num >= @counter }.sort_by(&:last)
48
- break if files_with_nums.empty?
49
- logger.debug("[foreman_ansible] - processing event files: #{files_with_nums.map(&:first).inspect}}")
50
- files_with_nums.map(&:first).each { |event_file| handle_event_file(event_file) }
51
- @counter = files_with_nums.last.last + 1
52
- end
37
+ def run_refresh_output
38
+ logger.debug('refreshing runner on demand')
39
+ process_artifacts
40
+ generate_updates
53
41
  end
54
42
 
55
43
  def timeout
@@ -62,7 +50,7 @@ module Proxy::Ansible
62
50
  end
63
51
 
64
52
  def kill
65
- ::Process.kill('SIGTERM', @command_pid)
53
+ ::Process.kill('SIGTERM', @process_manager.pid)
66
54
  publish_exit_status(2)
67
55
  @inventory['all']['hosts'].each { |hostname| @exit_statuses[hostname] = 2 }
68
56
  broadcast_data('Timeout for execution passed, stopping the job', 'stderr')
@@ -74,13 +62,44 @@ module Proxy::Ansible
74
62
  FileUtils.remove_entry(@root) if @tmp_working_dir && Dir.exist?(@root) && @cleanup_working_dirs
75
63
  end
76
64
 
65
+ def publish_exit_status(status)
66
+ process_artifacts
67
+ super
68
+ @targets.each_key { |host| publish_exit_status_for(host, status) } if status != 0
69
+ end
70
+
71
+ def initialize_command(*command)
72
+ super
73
+ @process_manager.stdin.close unless @process_manager.done?
74
+ end
75
+
77
76
  private
78
77
 
78
+ def process_artifacts
79
+ @counter ||= 1
80
+ @uuid ||= if (f = Dir["#{@root}/artifacts/*"].first)
81
+ File.basename(f)
82
+ end
83
+ return unless @uuid
84
+ job_event_dir = File.join(@root, 'artifacts', @uuid, 'job_events')
85
+ loop do
86
+ files = Dir["#{job_event_dir}/*.json"].map do |file|
87
+ num = File.basename(file)[/\A\d+/].to_i unless file.include?('partial')
88
+ [file, num]
89
+ end
90
+ files_with_nums = files.select { |(_, num)| num && num >= @counter }.sort_by(&:last)
91
+ break if files_with_nums.empty?
92
+ logger.debug("[foreman_ansible] - processing event files: #{files_with_nums.map(&:first).inspect}}")
93
+ files_with_nums.map(&:first).each { |event_file| handle_event_file(event_file) }
94
+ @counter = files_with_nums.last.last + 1
95
+ end
96
+ end
97
+
79
98
  def handle_event_file(event_file)
80
99
  logger.debug("[foreman_ansible] - parsing event file #{event_file}")
81
100
  begin
82
101
  event = JSON.parse(File.read(event_file))
83
- if (hostname = event.dig('event_data', 'host'))
102
+ if (hostname = hostname_for_event(event))
84
103
  handle_host_event(hostname, event)
85
104
  else
86
105
  handle_broadcast_data(event)
@@ -92,6 +111,17 @@ module Proxy::Ansible
92
111
  end
93
112
  end
94
113
 
114
+ def hostname_for_event(event)
115
+ hostname = event.dig('event_data', 'host') || event.dig('event_data', 'remote_addr')
116
+ return nil if hostname.nil? || hostname.empty?
117
+
118
+ unless @targets.key?(hostname)
119
+ logger.warn("handle_host_event: unknown host #{hostname} for event '#{event['event']}', broadcasting")
120
+ return nil
121
+ end
122
+ hostname
123
+ end
124
+
95
125
  def handle_host_event(hostname, event)
96
126
  log_event("for host: #{hostname.inspect}", event)
97
127
  publish_data_for(hostname, event['stdout'] + "\n", 'stdout') if event['stdout']
@@ -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
@@ -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.3.0'
5
+ VERSION = '3.4.1'
6
6
  end
7
7
  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.3.0
4
+ version: 3.4.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: 2022-01-17 00:00:00.000000000 Z
12
+ date: 2022-09-05 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,7 +152,6 @@ 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
157
156
  - lib/smart_proxy_ansible/configuration_loader.rb
158
157
  - lib/smart_proxy_ansible/exception.rb
@@ -160,13 +159,9 @@ files:
160
159
  - lib/smart_proxy_ansible/playbooks_reader.rb
161
160
  - lib/smart_proxy_ansible/plugin.rb
162
161
  - lib/smart_proxy_ansible/reader_helper.rb
163
- - lib/smart_proxy_ansible/remote_execution_core/ansible_runner.rb
164
162
  - lib/smart_proxy_ansible/roles_reader.rb
165
163
  - lib/smart_proxy_ansible/runner/ansible_runner.rb
166
- - lib/smart_proxy_ansible/runner/command_creator.rb
167
- - lib/smart_proxy_ansible/runner/playbook.rb
168
164
  - lib/smart_proxy_ansible/task_launcher/ansible_runner.rb
169
- - lib/smart_proxy_ansible/task_launcher/playbook.rb
170
165
  - lib/smart_proxy_ansible/validate_settings.rb
171
166
  - lib/smart_proxy_ansible/variables_extractor.rb
172
167
  - lib/smart_proxy_ansible/version.rb
@@ -190,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
185
  - !ruby/object:Gem::Version
191
186
  version: '0'
192
187
  requirements: []
193
- rubygems_version: 3.3.4
188
+ rubygems_version: 3.1.6
194
189
  signing_key:
195
190
  specification_version: 4
196
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