smart_proxy_remote_execution_ssh 0.0.13 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bundler.d/Gemfile.local.rb +1 -0
- data/lib/smart_proxy_remote_execution_ssh/plugin.rb +7 -4
- data/lib/smart_proxy_remote_execution_ssh/version.rb +1 -1
- data/lib/smart_proxy_remote_execution_ssh.rb +2 -12
- data/settings.d/smart_proxy_remote_execution_ssh.yml.example +5 -0
- metadata +24 -55
- data/lib/smart_proxy_remote_execution_ssh/command_action.rb +0 -82
- data/lib/smart_proxy_remote_execution_ssh/command_update.rb +0 -74
- data/lib/smart_proxy_remote_execution_ssh/connector.rb +0 -147
- data/lib/smart_proxy_remote_execution_ssh/dispatcher.rb +0 -93
- data/lib/smart_proxy_remote_execution_ssh/session.rb +0 -213
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f230840d2fd18a938ceef7c2258a7d9e5aa39531
|
4
|
+
data.tar.gz: 4ceec21bcf79ac48db17d515ea22c160453d61ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e451301babf18b38cfa04af50849e8f07ab90fb6ca24a31a775670165e71b4631ce259524b701b96e77c0b241e70b7a84fe04f7a61ae25ad91e69b574eb5fe7
|
7
|
+
data.tar.gz: 5585a0c4eff8a6d77131cd46bf1312d6cf8ca3e676d204772537246033da031aac73c21e20c46e5906a85edf64df0ff3d386914a05cb48d7861a323e5fa3fc0b
|
@@ -0,0 +1 @@
|
|
1
|
+
gem 'pry'
|
@@ -13,11 +13,14 @@ module Proxy::RemoteExecution::Ssh
|
|
13
13
|
after_activation do
|
14
14
|
require 'smart_proxy_dynflow'
|
15
15
|
require 'smart_proxy_remote_execution_ssh/version'
|
16
|
-
require 'smart_proxy_remote_execution_ssh/connector'
|
17
|
-
require 'smart_proxy_remote_execution_ssh/command_update'
|
18
|
-
require 'smart_proxy_remote_execution_ssh/dispatcher'
|
19
|
-
require 'smart_proxy_remote_execution_ssh/command_action'
|
20
16
|
require 'smart_proxy_remote_execution_ssh/api'
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'smart_proxy_remote_execution_ssh_core'
|
20
|
+
rescue LoadError
|
21
|
+
end
|
22
|
+
|
23
|
+
Proxy::RemoteExecution::Ssh.validate!
|
21
24
|
end
|
22
25
|
end
|
23
26
|
end
|
@@ -5,7 +5,7 @@ require 'smart_proxy_remote_execution_ssh/plugin'
|
|
5
5
|
module Proxy::RemoteExecution
|
6
6
|
module Ssh
|
7
7
|
class << self
|
8
|
-
def
|
8
|
+
def validate!
|
9
9
|
unless private_key_file
|
10
10
|
raise "settings for `ssh_identity_key` not set"
|
11
11
|
end
|
@@ -18,18 +18,10 @@ module Proxy::RemoteExecution
|
|
18
18
|
unless File.exist?(public_key_file)
|
19
19
|
raise "Ssh public key file #{public_key_file} doesn't exist"
|
20
20
|
end
|
21
|
-
|
22
|
-
@dispatcher = Proxy::RemoteExecution::Ssh::Dispatcher.spawn('proxy-ssh-dispatcher',
|
23
|
-
:clock => Proxy::Dynflow.instance.world.clock,
|
24
|
-
:logger => Proxy::Dynflow.instance.world.logger)
|
25
|
-
end
|
26
|
-
|
27
|
-
def dispatcher
|
28
|
-
@dispatcher || initialize
|
29
21
|
end
|
30
22
|
|
31
23
|
def private_key_file
|
32
|
-
File.expand_path(
|
24
|
+
File.expand_path(Plugin.settings.ssh_identity_key_file)
|
33
25
|
end
|
34
26
|
|
35
27
|
def public_key_file
|
@@ -38,5 +30,3 @@ module Proxy::RemoteExecution
|
|
38
30
|
end
|
39
31
|
end
|
40
32
|
end
|
41
|
-
|
42
|
-
Proxy::Dynflow.after_initialize { Proxy::RemoteExecution::Ssh.initialize }
|
metadata
CHANGED
@@ -1,97 +1,97 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_remote_execution_ssh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nečas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-06-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.7'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.7'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '10.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: minitest
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: mocha
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - ~>
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '1'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - ~>
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: webmock
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - ~>
|
73
|
+
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '1'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - ~>
|
80
|
+
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '1'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rack-test
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - ~>
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - ~>
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
@@ -112,46 +112,17 @@ dependencies:
|
|
112
112
|
name: smart_proxy_dynflow
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
|
-
- - ~>
|
115
|
+
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 0.0
|
117
|
+
version: 0.1.0
|
118
118
|
type: :runtime
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- - ~>
|
122
|
+
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: 0.0
|
125
|
-
-
|
126
|
-
name: net-ssh
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - '>='
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
type: :runtime
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - '>='
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
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: |2
|
154
|
-
Ssh remote execution provider for Foreman Smart-Proxy
|
124
|
+
version: 0.1.0
|
125
|
+
description: " Ssh remote execution provider for Foreman Smart-Proxy\n"
|
155
126
|
email:
|
156
127
|
- inecas@redhat.com
|
157
128
|
executables: []
|
@@ -162,18 +133,15 @@ extra_rdoc_files:
|
|
162
133
|
files:
|
163
134
|
- LICENSE
|
164
135
|
- README.md
|
136
|
+
- bundler.d/Gemfile.local.rb
|
165
137
|
- bundler.d/remote_execution_ssh.rb
|
166
138
|
- lib/smart_proxy_remote_execution_ssh.rb
|
167
139
|
- lib/smart_proxy_remote_execution_ssh/api.rb
|
168
|
-
- lib/smart_proxy_remote_execution_ssh/command_action.rb
|
169
|
-
- lib/smart_proxy_remote_execution_ssh/command_update.rb
|
170
|
-
- lib/smart_proxy_remote_execution_ssh/connector.rb
|
171
|
-
- lib/smart_proxy_remote_execution_ssh/dispatcher.rb
|
172
140
|
- lib/smart_proxy_remote_execution_ssh/http_config.ru
|
173
141
|
- lib/smart_proxy_remote_execution_ssh/plugin.rb
|
174
|
-
- lib/smart_proxy_remote_execution_ssh/session.rb
|
175
142
|
- lib/smart_proxy_remote_execution_ssh/version.rb
|
176
143
|
- settings.d/remote_execution_ssh.yml.example
|
144
|
+
- settings.d/smart_proxy_remote_execution_ssh.yml.example
|
177
145
|
homepage: https://github.com/theforeman/smart_proxy_remote_execution_ssh
|
178
146
|
licenses:
|
179
147
|
- GPLv3
|
@@ -184,12 +152,12 @@ require_paths:
|
|
184
152
|
- lib
|
185
153
|
required_ruby_version: !ruby/object:Gem::Requirement
|
186
154
|
requirements:
|
187
|
-
- -
|
155
|
+
- - ">="
|
188
156
|
- !ruby/object:Gem::Version
|
189
157
|
version: '0'
|
190
158
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
191
159
|
requirements:
|
192
|
-
- -
|
160
|
+
- - ">="
|
193
161
|
- !ruby/object:Gem::Version
|
194
162
|
version: '0'
|
195
163
|
requirements: []
|
@@ -199,3 +167,4 @@ signing_key:
|
|
199
167
|
specification_version: 4
|
200
168
|
summary: Ssh remote execution provider for Foreman Smart-Proxy
|
201
169
|
test_files: []
|
170
|
+
has_rdoc:
|
@@ -1,82 +0,0 @@
|
|
1
|
-
module Proxy::RemoteExecution::Ssh
|
2
|
-
class CommandAction < ::Dynflow::Action
|
3
|
-
include Dynflow::Action::Cancellable
|
4
|
-
include ::Proxy::Dynflow::Callback::PlanHelper
|
5
|
-
|
6
|
-
def plan(input)
|
7
|
-
if callback = input['callback']
|
8
|
-
input[:task_id] = callback['task_id']
|
9
|
-
else
|
10
|
-
input[:task_id] ||= SecureRandom.uuid
|
11
|
-
end
|
12
|
-
plan_with_callback(input)
|
13
|
-
end
|
14
|
-
|
15
|
-
def run(event = nil)
|
16
|
-
case event
|
17
|
-
when nil
|
18
|
-
init_run
|
19
|
-
when CommandUpdate
|
20
|
-
process_update(event)
|
21
|
-
when Dynflow::Action::Cancellable::Cancel
|
22
|
-
kill_run
|
23
|
-
when Dynflow::Action::Skip
|
24
|
-
# do nothing
|
25
|
-
else
|
26
|
-
raise "Unexpected event #{event.inspect}"
|
27
|
-
end
|
28
|
-
rescue => e
|
29
|
-
action_logger.error(e)
|
30
|
-
process_update(CommandUpdate.new(CommandUpdate.encode_exception("Proxy error", e)))
|
31
|
-
end
|
32
|
-
|
33
|
-
def finalize
|
34
|
-
# To mark the task as a whole as failed
|
35
|
-
error! "Script execution failed" if failed_run?
|
36
|
-
end
|
37
|
-
|
38
|
-
def rescue_strategy_for_self
|
39
|
-
Dynflow::Action::Rescue::Skip
|
40
|
-
end
|
41
|
-
|
42
|
-
def command
|
43
|
-
@command ||= Dispatcher::Command.new(:id => input[:task_id],
|
44
|
-
:host => input[:hostname],
|
45
|
-
:ssh_user => input[:ssh_user] || 'root',
|
46
|
-
:effective_user => input[:effective_user],
|
47
|
-
:script => input[:script],
|
48
|
-
:effective_user_method => input[:effective_user_method],
|
49
|
-
:host_public_key => input[:host_public_key],
|
50
|
-
:verify_host => input[:verify_host],
|
51
|
-
:suspended_action => suspended_action)
|
52
|
-
end
|
53
|
-
|
54
|
-
def init_run
|
55
|
-
output[:result] = []
|
56
|
-
Proxy::RemoteExecution::Ssh.dispatcher.tell([:initialize_command, command])
|
57
|
-
suspend
|
58
|
-
end
|
59
|
-
|
60
|
-
def kill_run
|
61
|
-
Proxy::RemoteExecution::Ssh.dispatcher.tell([:kill, command])
|
62
|
-
suspend
|
63
|
-
end
|
64
|
-
|
65
|
-
def finish_run(update)
|
66
|
-
output[:exit_status] = update.exit_status
|
67
|
-
end
|
68
|
-
|
69
|
-
def process_update(update)
|
70
|
-
output[:result].concat(update.buffer_to_hash)
|
71
|
-
if update.exit_status
|
72
|
-
finish_run(update)
|
73
|
-
else
|
74
|
-
suspend
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def failed_run?
|
79
|
-
output[:exit_status] != 0
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
@@ -1,74 +0,0 @@
|
|
1
|
-
module Proxy::RemoteExecution::Ssh
|
2
|
-
# update sent back to the suspended action
|
3
|
-
class CommandUpdate
|
4
|
-
attr_reader :buffer, :exit_status
|
5
|
-
|
6
|
-
def initialize(buffer)
|
7
|
-
@buffer = buffer
|
8
|
-
extract_exit_status
|
9
|
-
end
|
10
|
-
|
11
|
-
def extract_exit_status
|
12
|
-
@buffer.delete_if do |data|
|
13
|
-
if data.is_a? StatusData
|
14
|
-
@exit_status = data.data
|
15
|
-
true
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def buffer_to_hash
|
21
|
-
buffer.map(&:to_hash)
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.encode_exception(description, exception, fatal = true)
|
25
|
-
ret = [DebugData.new("#{description}\n#{exception.class} #{exception.message}")]
|
26
|
-
ret << StatusData.new('EXCEPTION') if fatal
|
27
|
-
return ret
|
28
|
-
end
|
29
|
-
|
30
|
-
class Data
|
31
|
-
attr_reader :data, :timestamp
|
32
|
-
|
33
|
-
def initialize(data, timestamp = Time.now)
|
34
|
-
@data = data
|
35
|
-
@data = @data.force_encoding('UTF-8') if @data.is_a? String
|
36
|
-
@timestamp = timestamp
|
37
|
-
end
|
38
|
-
|
39
|
-
def data_type
|
40
|
-
raise NotImplemented
|
41
|
-
end
|
42
|
-
|
43
|
-
def to_hash
|
44
|
-
{ :output_type => data_type,
|
45
|
-
:output => data,
|
46
|
-
:timestamp => timestamp.to_f }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
class StdoutData < Data
|
51
|
-
def data_type
|
52
|
-
:stdout
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
class StderrData < Data
|
57
|
-
def data_type
|
58
|
-
:stderr
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
class DebugData < Data
|
63
|
-
def data_type
|
64
|
-
:debug
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
class StatusData < Data
|
69
|
-
def data_type
|
70
|
-
:status
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
@@ -1,147 +0,0 @@
|
|
1
|
-
require 'net/ssh'
|
2
|
-
require 'net/scp'
|
3
|
-
|
4
|
-
module Proxy::RemoteExecution::Ssh
|
5
|
-
# Service that handles running external commands for Actions::Command
|
6
|
-
# Dynflow action. It runs just one (actor) thread for all the commands
|
7
|
-
# running in the system and updates the Dynflow actions periodically.
|
8
|
-
class Connector
|
9
|
-
MAX_PROCESS_RETRIES = 3
|
10
|
-
|
11
|
-
def initialize(host, user, options = {})
|
12
|
-
@host = host
|
13
|
-
@user = user
|
14
|
-
@logger = options[:logger] || Logger.new($stderr)
|
15
|
-
@client_private_key_file = options[:client_private_key_file]
|
16
|
-
@known_hosts_file = options[:known_hosts_file]
|
17
|
-
end
|
18
|
-
|
19
|
-
# Initiates run of the remote command and yields the data when
|
20
|
-
# available. The yielding doesn't happen automatically, but as
|
21
|
-
# part of calling the `refresh` method.
|
22
|
-
def async_run(command)
|
23
|
-
started = false
|
24
|
-
session.open_channel do |channel|
|
25
|
-
channel.request_pty
|
26
|
-
|
27
|
-
channel.on_data { |ch, data| yield CommandUpdate::StdoutData.new(data) }
|
28
|
-
|
29
|
-
channel.on_extended_data { |ch, type, data| yield CommandUpdate::StderrData.new(data) }
|
30
|
-
|
31
|
-
# standard exit of the command
|
32
|
-
channel.on_request("exit-status") { |ch, data| yield CommandUpdate::StatusData.new(data.read_long) }
|
33
|
-
|
34
|
-
# on signal: sedning the signal value (such as 'TERM')
|
35
|
-
channel.on_request("exit-signal") do |ch, data|
|
36
|
-
yield(CommandUpdate::StatusData.new(data.read_string))
|
37
|
-
ch.close
|
38
|
-
# wait for the channel to finish so that we know at the end
|
39
|
-
# that the session is inactive
|
40
|
-
ch.wait
|
41
|
-
end
|
42
|
-
|
43
|
-
channel.exec(command) do |ch, success|
|
44
|
-
started = true
|
45
|
-
unless success
|
46
|
-
CommandUpdate.encode_exception("Error initializing command #{command}", e).each do |data|
|
47
|
-
yield data
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
session.process(0) until started
|
53
|
-
return true
|
54
|
-
end
|
55
|
-
|
56
|
-
def run(command)
|
57
|
-
output = ""
|
58
|
-
exit_status = nil
|
59
|
-
channel = session.open_channel do |ch|
|
60
|
-
ch.on_data { |data| output.concat(data) }
|
61
|
-
|
62
|
-
ch.on_extended_data { |_, _, data| output.concat(data) }
|
63
|
-
|
64
|
-
ch.on_request("exit-status") { |_, data| exit_status = data.read_long }
|
65
|
-
|
66
|
-
# on signal: sedning the signal value (such as 'TERM')
|
67
|
-
ch.on_request("exit-signal") do |_, data|
|
68
|
-
exit_status = data.read_string
|
69
|
-
ch.close
|
70
|
-
ch.wait
|
71
|
-
end
|
72
|
-
|
73
|
-
ch.exec command do |_, success|
|
74
|
-
raise "could not execute command" unless success
|
75
|
-
end
|
76
|
-
end
|
77
|
-
channel.wait
|
78
|
-
return exit_status, output
|
79
|
-
end
|
80
|
-
|
81
|
-
# calls the callback registered in the `async_run` when some data
|
82
|
-
# for the session are available
|
83
|
-
def refresh
|
84
|
-
return if @session.nil?
|
85
|
-
tries = 0
|
86
|
-
begin
|
87
|
-
session.process(0)
|
88
|
-
rescue Net::SSH::Disconnect => e
|
89
|
-
session.shutdown!
|
90
|
-
raise e
|
91
|
-
rescue => e
|
92
|
-
@logger.error("Error while processing ssh channel: #{e.class} #{e.message}\n #{e.backtrace.join("\n")}")
|
93
|
-
tries += 1
|
94
|
-
if tries <= MAX_PROCESS_RETRIES
|
95
|
-
retry
|
96
|
-
else
|
97
|
-
raise e
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def upload_file(local_path, remote_path)
|
103
|
-
ensure_remote_directory(File.dirname(remote_path))
|
104
|
-
scp = Net::SCP.new(session)
|
105
|
-
upload_channel = scp.upload(local_path, remote_path)
|
106
|
-
upload_channel.wait
|
107
|
-
ensure
|
108
|
-
if upload_channel
|
109
|
-
upload_channel.close
|
110
|
-
upload_channel.wait
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def ensure_remote_directory(path)
|
115
|
-
exit_code, output = run("mkdir -p #{path}")
|
116
|
-
if exit_code != 0
|
117
|
-
raise "Unable to create directory on remote system #{path}: exit code: #{exit_code}\n #{output}"
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def close
|
122
|
-
@logger.debug("closing session to #{@user}@#{@host}")
|
123
|
-
@session.close unless @session.nil? || @session.closed?
|
124
|
-
end
|
125
|
-
|
126
|
-
private
|
127
|
-
|
128
|
-
def session
|
129
|
-
@session ||= begin
|
130
|
-
@logger.debug("opening session to #{@user}@#{@host}")
|
131
|
-
Net::SSH.start(@host, @user, ssh_options)
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def ssh_options
|
136
|
-
ssh_options = {}
|
137
|
-
ssh_options[:keys] = [@client_private_key_file] if @client_private_key_file
|
138
|
-
ssh_options[:user_known_hosts_file] = @known_hosts_file if @known_hosts_file
|
139
|
-
ssh_options[:keys_only] = true
|
140
|
-
# if the host public key is contained in the known_hosts_file,
|
141
|
-
# verify it, otherwise, if missing, import it and continue
|
142
|
-
ssh_options[:paranoid] = true
|
143
|
-
ssh_options[:auth_methods] = ["publickey"]
|
144
|
-
return ssh_options
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
@@ -1,93 +0,0 @@
|
|
1
|
-
require 'smart_proxy_remote_execution_ssh/session'
|
2
|
-
|
3
|
-
module Proxy::RemoteExecution::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 Dispatcher < ::Dynflow::Actor
|
8
|
-
# command comming from action
|
9
|
-
class Command
|
10
|
-
attr_reader :id, :host, :ssh_user, :effective_user, :effective_user_method, :script, :host_public_key, :suspended_action
|
11
|
-
|
12
|
-
def initialize(data)
|
13
|
-
validate!(data)
|
14
|
-
|
15
|
-
@id = data[:id]
|
16
|
-
@host = data[:host]
|
17
|
-
@ssh_user = data[:ssh_user]
|
18
|
-
@effective_user = data[:effective_user]
|
19
|
-
@effective_user_method = data[:effective_user_method] || 'su'
|
20
|
-
@script = data[:script]
|
21
|
-
@host_public_key = data[:host_public_key]
|
22
|
-
@suspended_action = data[:suspended_action]
|
23
|
-
end
|
24
|
-
|
25
|
-
def validate!(data)
|
26
|
-
required_fields = [:id, :host, :ssh_user, :script, :suspended_action]
|
27
|
-
missing_fields = required_fields.find_all { |f| !data[f] }
|
28
|
-
raise ArgumentError, "Missing fields: #{missing_fields}" unless missing_fields.empty?
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def initialize(options = {})
|
33
|
-
@clock = options[:clock] || Dynflow::Clock.spawn('proxy-dispatcher-clock')
|
34
|
-
@logger = options[:logger] || Logger.new($stderr)
|
35
|
-
|
36
|
-
@session_args = { :logger => @logger,
|
37
|
-
:clock => @clock,
|
38
|
-
:connector_class => options[:connector_class] || Connector,
|
39
|
-
:local_working_dir => options[:local_working_dir] || ::Proxy::RemoteExecution::Ssh::Plugin.settings.local_working_dir,
|
40
|
-
:remote_working_dir => options[:remote_working_dir] || ::Proxy::RemoteExecution::Ssh::Plugin.settings.remote_working_dir,
|
41
|
-
:client_private_key_file => Proxy::RemoteExecution::Ssh.private_key_file,
|
42
|
-
:refresh_interval => options[:refresh_interval] || 1 }
|
43
|
-
|
44
|
-
@sessions = {}
|
45
|
-
end
|
46
|
-
|
47
|
-
def initialize_command(command)
|
48
|
-
@logger.debug("initalizing command [#{command}]")
|
49
|
-
open_session(command)
|
50
|
-
rescue => exception
|
51
|
-
handle_command_exception(command, exception)
|
52
|
-
end
|
53
|
-
|
54
|
-
def kill(command)
|
55
|
-
@logger.debug("killing command [#{command}]")
|
56
|
-
session = @sessions[command.id]
|
57
|
-
session.tell(:kill) if session
|
58
|
-
rescue => exception
|
59
|
-
handle_command_exception(command, exception, false)
|
60
|
-
end
|
61
|
-
|
62
|
-
def finish_command(command)
|
63
|
-
close_session(command)
|
64
|
-
rescue => exception
|
65
|
-
handle_command_exception(command, exception)
|
66
|
-
end
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
def handle_command_exception(command, exception, fatal = true)
|
71
|
-
@logger.error("error while dispatching command #{command} to session:"\
|
72
|
-
"#{exception.class} #{exception.message}:\n #{exception.backtrace.join("\n")}")
|
73
|
-
command_data = CommandUpdate.encode_exception("Failed to dispatch the command", exception, fatal)
|
74
|
-
command.suspended_action << CommandUpdate.new(command_data)
|
75
|
-
close_session(command) if fatal
|
76
|
-
end
|
77
|
-
|
78
|
-
def open_session(command)
|
79
|
-
raise "Session already opened for command #{command}" if @sessions[command.id]
|
80
|
-
options = { :name => "proxy-ssh-session-#{command.host}-#{command.ssh_user}-#{command.id}",
|
81
|
-
:args => [@session_args.merge(:command => command)],
|
82
|
-
:supervise => true }
|
83
|
-
@sessions[command.id] = Proxy::RemoteExecution::Ssh::Session.spawn(options)
|
84
|
-
end
|
85
|
-
|
86
|
-
def close_session(command)
|
87
|
-
session = @sessions.delete(command.id)
|
88
|
-
return unless session
|
89
|
-
@logger.debug("closing session for command [#{command}], #{@sessions.size} session(s) left ")
|
90
|
-
session.tell([:start_termination, Concurrent.future])
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
@@ -1,213 +0,0 @@
|
|
1
|
-
require 'smart_proxy_remote_execution_ssh/session'
|
2
|
-
|
3
|
-
module Proxy::RemoteExecution::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] || ::Proxy::RemoteExecution::Ssh::Plugin.settings.local_working_dir
|
15
|
-
@remote_working_dir = options[:remote_working_dir] || ::Proxy::RemoteExecution::Ssh::Plugin.settings.remote_working_dir
|
16
|
-
@refresh_interval = options[:refresh_interval] || 1
|
17
|
-
@client_private_key_file = Proxy::RemoteExecution::Ssh.private_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
|