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.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +57 -0
- data/bundler.d/Gemfile.local.rb +1 -0
- data/bundler.d/remote_execution_ssh.rb +1 -0
- data/lib/smart_proxy_remote_execution_ssh_core.rb +49 -0
- data/lib/smart_proxy_remote_execution_ssh_core/command_action.rb +86 -0
- data/lib/smart_proxy_remote_execution_ssh_core/command_update.rb +78 -0
- data/lib/smart_proxy_remote_execution_ssh_core/connector.rb +151 -0
- data/lib/smart_proxy_remote_execution_ssh_core/dispatcher.rb +97 -0
- data/lib/smart_proxy_remote_execution_ssh_core/session.rb +215 -0
- data/lib/smart_proxy_remote_execution_ssh_core/settings.rb +41 -0
- data/lib/smart_proxy_remote_execution_ssh_core/version.rb +9 -0
- data/settings.d/remote_execution_ssh.yml.example +5 -0
- data/settings.d/smart_proxy_remote_execution_ssh.yml.example +5 -0
- metadata +201 -0
@@ -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!
|
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:
|