smart_proxy_remote_execution_ssh 0.0.13 → 0.1.0
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 +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
|