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 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