smart_proxy_remote_execution_ssh_core 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: