smart_proxy_remote_execution_ssh_core 0.0.13

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.
@@ -0,0 +1,215 @@
1
+ module Proxy
2
+ module RemoteExecution
3
+ module Ssh
4
+ # Service that handles running external commands for Actions::Command
5
+ # Dynflow action. It runs just one (actor) thread for all the commands
6
+ # running in the system and updates the Dynflow actions periodically.
7
+ class Session < ::Dynflow::Actor
8
+ EXPECTED_POWER_ACTION_MESSAGES = ["restart host", "shutdown host"]
9
+
10
+ def initialize(options = {})
11
+ @clock = options[:clock] || Dynflow::Clock.spawn('proxy-dispatcher-clock')
12
+ @logger = options[:logger] || Logger.new($stderr)
13
+ @connector_class = options[:connector_class] || Connector
14
+ @local_working_dir = options[:local_working_dir] || Settings.instance.local_working_dir
15
+ @remote_working_dir = options[:remote_working_dir] || Settings.instance.remote_working_dir
16
+ @refresh_interval = options[:refresh_interval] || 1
17
+ @client_private_key_file = Settings.instance.ssh_identity_key_file
18
+ @command = options[:command]
19
+
20
+ @command_buffer = []
21
+ @refresh_planned = false
22
+
23
+ reference.tell(:initialize_command)
24
+ end
25
+
26
+ def initialize_command
27
+ @logger.debug("initalizing command [#{@command}]")
28
+ open_connector
29
+ remote_script = cp_script_to_remote
30
+ output_path = File.join(File.dirname(remote_script), 'output')
31
+
32
+ # pipe the output to tee while capturing the exit code
33
+ script = <<-SCRIPT
34
+ exec 4>&1
35
+ exit_code=`((#{su_prefix}#{remote_script}; echo $?>&3 ) | /usr/bin/tee #{output_path} ) 3>&1 >&4`
36
+ exec 4>&-
37
+ exit $exit_code
38
+ SCRIPT
39
+ @logger.debug("executing script:\n#{script.lines.map { |line| " | #{line}" }.join}")
40
+ @connector.async_run(script) do |data|
41
+ @command_buffer << data
42
+ end
43
+ rescue => e
44
+ @logger.error("error while initalizing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
45
+ @command_buffer.concat(CommandUpdate.encode_exception("Error initializing command #{@command}", e))
46
+ refresh
47
+ ensure
48
+ plan_next_refresh
49
+ end
50
+
51
+ def refresh
52
+ @connector.refresh if @connector
53
+
54
+ unless @command_buffer.empty?
55
+ status = refresh_command_buffer
56
+ if status
57
+ finish_command
58
+ end
59
+ end
60
+ rescue Net::SSH::Disconnect => e
61
+ check_expecting_disconnect
62
+ if @expecting_disconnect
63
+ @command_buffer << CommandUpdate::StatusData.new(0)
64
+ else
65
+ @command_buffer.concat(CommandUpdate.encode_exception("Failed to refresh the connector", e, true))
66
+ end
67
+ refresh_command_buffer
68
+ finish_command
69
+ rescue => e
70
+ @command_buffer.concat(CommandUpdate.encode_exception("Failed to refresh the connector", e, false))
71
+ ensure
72
+ @refresh_planned = false
73
+ plan_next_refresh
74
+ end
75
+
76
+ def refresh_command_buffer
77
+ @logger.debug("command #{@command} got new output: #{@command_buffer.inspect}")
78
+ command_update = CommandUpdate.new(@command_buffer)
79
+ check_expecting_disconnect
80
+ @command.suspended_action << command_update
81
+ @command_buffer = []
82
+ if command_update.exit_status
83
+ @logger.debug("command [#{@command}] finished with status #{command_update.exit_status}")
84
+ return command_update.exit_status
85
+ end
86
+ end
87
+
88
+ def kill
89
+ @logger.debug("killing command [#{@command}]")
90
+ if @connector
91
+ @connector.run("pkill -f #{remote_command_file('script')}")
92
+ else
93
+ @logger.debug("connection closed")
94
+ end
95
+ rescue => e
96
+ @command_buffer.concat(CommandUpdate.encode_exception("Failed to kill the command", e, false))
97
+ plan_next_refresh
98
+ end
99
+
100
+ def finish_command
101
+ close
102
+ dispatcher.tell([:finish_command, @command])
103
+ end
104
+
105
+ def dispatcher
106
+ self.parent
107
+ end
108
+
109
+ def start_termination(*args)
110
+ super
111
+ close
112
+ finish_termination
113
+ end
114
+
115
+ private
116
+
117
+ def su_prefix
118
+ return if @command.effective_user.nil? || @command.effective_user == @command.ssh_user
119
+ case @command.effective_user_method
120
+ when 'sudo'
121
+ "sudo -n -u #{@command.effective_user} "
122
+ when 'su'
123
+ "su - #{@command.effective_user} -c "
124
+ else
125
+ raise "effective_user_method ''#{@command.effective_user_method}'' not supported"
126
+ end
127
+ end
128
+
129
+ def open_connector
130
+ raise 'Connector already opened' if @connector
131
+ options = { :logger => @logger }
132
+ options[:known_hosts_file] = prepare_known_hosts
133
+ options[:client_private_key_file] = @client_private_key_file
134
+ @connector = @connector_class.new(@command.host, @command.ssh_user, options)
135
+ end
136
+
137
+ def local_command_dir
138
+ File.join(@local_working_dir, 'foreman-proxy', "foreman-ssh-cmd-#{@command.id}")
139
+ end
140
+
141
+ def local_command_file(filename)
142
+ File.join(local_command_dir, filename)
143
+ end
144
+
145
+ def remote_command_dir
146
+ File.join(@remote_working_dir, "foreman-ssh-cmd-#{@command.id}")
147
+ end
148
+
149
+ def remote_command_file(filename)
150
+ File.join(remote_command_dir, filename)
151
+ end
152
+
153
+ def ensure_local_directory(path)
154
+ if File.exist?(path)
155
+ raise "#{path} expected to be a directory" unless File.directory?(path)
156
+ else
157
+ FileUtils.mkdir_p(path)
158
+ end
159
+ return path
160
+ end
161
+
162
+ def cp_script_to_remote
163
+ local_script_file = write_command_file_locally('script', sanitize_script(@command.script))
164
+ File.chmod(0777, local_script_file)
165
+ remote_script_file = remote_command_file('script')
166
+ @connector.upload_file(local_script_file, remote_script_file)
167
+ return remote_script_file
168
+ end
169
+
170
+ def sanitize_script(script)
171
+ script.tr("\r", '')
172
+ end
173
+
174
+ def write_command_file_locally(filename, content)
175
+ path = local_command_file(filename)
176
+ ensure_local_directory(File.dirname(path))
177
+ File.write(path, content)
178
+ return path
179
+ end
180
+
181
+ def prepare_known_hosts
182
+ path = local_command_file('known_hosts')
183
+ if @command.host_public_key
184
+ write_command_file_locally('known_hosts', "#{@command.host} #{@command.host_public_key}")
185
+ end
186
+ return path
187
+ end
188
+
189
+ def close
190
+ @connector.close if @connector
191
+ @connector = nil
192
+ end
193
+
194
+ def plan_next_refresh
195
+ if @connector && !@refresh_planned
196
+ @logger.debug("planning to refresh")
197
+ @clock.ping(reference, Time.now + @refresh_interval, :refresh)
198
+ @refresh_planned = true
199
+ end
200
+ end
201
+
202
+ # when a remote server disconnects, it's hard to tell if it was on purpose (when calling reboot)
203
+ # or it's an error. When it's expected, we expect the script to produce 'restart host' as
204
+ # its last command output
205
+ def check_expecting_disconnect
206
+ last_output = @command_buffer.reverse.find { |d| d.is_a? CommandUpdate::StdoutData }
207
+ return unless last_output
208
+ if EXPECTED_POWER_ACTION_MESSAGES.any? { |message| last_output.data =~ /^#{message}/ }
209
+ @expecting_disconnect = true
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,41 @@
1
+ require 'ostruct'
2
+
3
+ module Proxy
4
+ module RemoteExecution
5
+ module Ssh
6
+ class Settings < OpenStruct
7
+
8
+ DEFAULT_SETTINGS = {
9
+ :enabled => true,
10
+ :ssh_identity_key_file => '~/.ssh/id_rsa_foreman_proxy',
11
+ :local_working_dir => '/var/tmp',
12
+ :remote_working_dir => '/var/tmp'
13
+ }
14
+
15
+ def initialize(settings = {})
16
+ super(DEFAULT_SETTINGS.merge(settings))
17
+ end
18
+
19
+ def load_settings_from_proxy
20
+ DEFAULT_SETTINGS.keys.each do |key|
21
+ self.class.instance[key] = Proxy::RemoteExecution::Ssh::Plugin.settings[key]
22
+ end
23
+ end
24
+
25
+ def self.create!(input = {})
26
+ settings = Proxy::RemoteExecution::Ssh::Settings.new input
27
+ self.instance = settings
28
+ end
29
+
30
+ def self.instance
31
+ SmartProxyDynflowCore::SETTINGS.plugins['smart_proxy_remote_execution_ssh_core']
32
+ end
33
+
34
+ def self.instance=(settings)
35
+ SmartProxyDynflowCore::SETTINGS.plugins['smart_proxy_remote_execution_ssh_core'] = settings
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ Proxy::RemoteExecution::Ssh::Settings.create!
@@ -0,0 +1,9 @@
1
+ module Proxy
2
+ module RemoteExecution
3
+ module Ssh
4
+ module Core
5
+ VERSION = '0.0.13'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ ---
2
+ :enabled: true
3
+ :ssh_identity_key_file: '~/.ssh/id_rsa_foreman_proxy'
4
+ :local_working_dir: '/var/tmp'
5
+ :remote_working_dir: '/var/tmp'
@@ -0,0 +1,5 @@
1
+ ---
2
+ :enabled: true
3
+ :ssh_identity_key_file: '~/.ssh/id_rsa_foreman_proxy'
4
+ :local_working_dir: '/var/tmp'
5
+ :remote_working_dir: '/var/tmp'
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smart_proxy_remote_execution_ssh_core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.13
5
+ platform: ruby
6
+ authors:
7
+ - Ivan Nečas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mocha
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rack-test
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 0.32.1
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.32.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: smart_proxy_dynflow_core
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 0.0.7
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 0.0.7
125
+ - !ruby/object:Gem::Dependency
126
+ name: net-ssh
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "<="
130
+ - !ruby/object:Gem::Version
131
+ version: 2.9.4
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "<="
137
+ - !ruby/object:Gem::Version
138
+ version: 2.9.4
139
+ - !ruby/object:Gem::Dependency
140
+ name: net-scp
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: " Ssh remote execution provider for Foreman Smart-Proxy\n"
154
+ email:
155
+ - inecas@redhat.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files:
159
+ - README.md
160
+ - LICENSE
161
+ files:
162
+ - LICENSE
163
+ - README.md
164
+ - bundler.d/Gemfile.local.rb
165
+ - bundler.d/remote_execution_ssh.rb
166
+ - lib/smart_proxy_remote_execution_ssh_core.rb
167
+ - lib/smart_proxy_remote_execution_ssh_core/command_action.rb
168
+ - lib/smart_proxy_remote_execution_ssh_core/command_update.rb
169
+ - lib/smart_proxy_remote_execution_ssh_core/connector.rb
170
+ - lib/smart_proxy_remote_execution_ssh_core/dispatcher.rb
171
+ - lib/smart_proxy_remote_execution_ssh_core/session.rb
172
+ - lib/smart_proxy_remote_execution_ssh_core/settings.rb
173
+ - lib/smart_proxy_remote_execution_ssh_core/version.rb
174
+ - settings.d/remote_execution_ssh.yml.example
175
+ - settings.d/smart_proxy_remote_execution_ssh.yml.example
176
+ homepage: https://github.com/theforeman/smart_proxy_remote_execution_ssh
177
+ licenses:
178
+ - GPLv3
179
+ metadata: {}
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubyforge_project:
196
+ rubygems_version: 2.4.8
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: Ssh remote execution provider for Foreman Smart-Proxy
200
+ test_files: []
201
+ has_rdoc: