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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 91afb2738282d322802d7b56291b5240cb62b478
4
- data.tar.gz: b42e795f671e0165e60fe821ca12de4c70884fd1
3
+ metadata.gz: f230840d2fd18a938ceef7c2258a7d9e5aa39531
4
+ data.tar.gz: 4ceec21bcf79ac48db17d515ea22c160453d61ae
5
5
  SHA512:
6
- metadata.gz: 17ce48cae4e47ebb886157156f0d5e35271b184f84e07762dff735bbe32ae458a907a6956a20d1f18c686dc24802f48018eff8ec8a341071f1c65a5a6bd7f4ca
7
- data.tar.gz: bb75cd72b3fbb5a28c63c7b28a6067ac02d6f568ed40bd759dcdd3102b4b4cae5af24e4631276a2220dd8b094553fc0c83697bccc6ba597114b8a351ff520e25
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
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.0.13'
4
+ VERSION = '0.1.0'
5
5
  end
6
6
  end
7
7
  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 initialize
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(Ssh::Plugin.settings.ssh_identity_key_file)
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 }
@@ -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 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.13
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-02-16 00:00:00.000000000 Z
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.3
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.3
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: '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