Class: Proxy::RemoteExecution::Ssh::Runners::ScriptRunner

Inherits:
Dynflow::Runner::Base
  • Object
show all
Includes:
Dynflow::Runner::ProcessManagerCommand, CommandLogging
Defined in:
lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb

Direct Known Subclasses

PollingScriptRunner

Constant Summary collapse

EXPECTED_POWER_ACTION_MESSAGES =
['restart host', 'shutdown host'].freeze
DEFAULT_REFRESH_INTERVAL =
1
UNSHARE_PREFIX =
'unshare --fork --kill-child'.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from CommandLogging

#log_command, #set_pm_debug_logging

Constructor Details

#initialize(options, user_method, suspended_action: nil) ⇒ ScriptRunner

Returns a new instance of ScriptRunner.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 104

def initialize(options, user_method, suspended_action: nil)
  super suspended_action: suspended_action
  @host = options.fetch(:hostname)
  @script = options.fetch(:script)
  @ssh_user = options.fetch(:ssh_user, 'root')
  @ssh_port = options.fetch(:ssh_port, 22)
  @host_public_key = options.fetch(:host_public_key, nil)
  @execution_timeout_interval = options.fetch(:execution_timeout_interval, nil)

  @client_private_key_file = settings.ssh_identity_key_file
  @local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
  @remote_working_dir = options.fetch(:remote_working_dir, settings.remote_working_dir.shellescape)
  @socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir)
  @cleanup_working_dirs = options.fetch(:cleanup_working_dirs, settings.cleanup_working_dirs)
  @first_execution = options.fetch(:first_execution, false)
  @user_method = user_method
  @options = options
end

Instance Attribute Details

#execution_timeout_intervalObject (readonly)

Returns the value of attribute execution_timeout_interval.



97
98
99
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 97

def execution_timeout_interval
  @execution_timeout_interval
end

Class Method Details

.build(options, suspended_action:) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 123

def self.build(options, suspended_action:)
  effective_user = options.fetch(:effective_user, nil)
  ssh_user = options.fetch(:ssh_user, 'root')
  effective_user_method = options.fetch(:effective_user_method, 'sudo')

  user_method = if effective_user.nil? || effective_user == ssh_user
                  NoopUserMethod.new
                elsif effective_user_method == 'sudo'
                  SudoUserMethod.new(effective_user, ssh_user,
                                     options.fetch(:secrets, {}).fetch(:effective_user_password, nil))
                elsif effective_user_method == 'dzdo'
                  DzdoUserMethod.new(effective_user, ssh_user,
                                     options.fetch(:secrets, {}).fetch(:effective_user_password, nil))
                elsif effective_user_method == 'su'
                  SuUserMethod.new(effective_user, ssh_user,
                                   options.fetch(:secrets, {}).fetch(:effective_user_password, nil))
                else
                  raise "effective_user_method '#{effective_user_method}' not supported"
                end

  new(options, user_method, suspended_action: suspended_action)
end

Instance Method Details

#closeObject



246
247
248
249
250
251
252
253
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 246

def close
  run_sync("rm -rf #{remote_command_dir}") if should_cleanup?
rescue StandardError => e
  publish_exception('Error when removing remote working dir', e, false)
ensure
  close_session if @process_manager
  FileUtils.rm_rf(local_command_dir) if Dir.exist?(local_command_dir) && @cleanup_working_dirs
end

#close_sessionObject



240
241
242
243
244
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 240

def close_session
  raise 'Control socket file does not exist' unless File.exist?(socket_file)
  @logger.debug("Sending exit request for session #{@ssh_user}@#{@host}")
  @connection.disconnect!
end

#initialization_scriptObject

the script that initiates the execution



209
210
211
212
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 209

def initialization_script
  # pipe the output to tee while capturing the exit code in a file
  @remote_script_wrapper
end

#killObject



221
222
223
224
225
226
227
228
229
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 221

def kill
  if @process_manager&.started?
    run_sync("kill $(cat #{@pid_path})")
  else
    logger.debug('connection closed')
  end
rescue StandardError => e
  publish_exception('Unexpected error', e, false)
end

#preflight_checksObject



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 165

def preflight_checks
  script = cp_script_to_remote("#!/bin/sh\nexec true")
  ensure_remote_command(script,
    error: 'Failed to execute script on remote machine, exit code: %{exit_code}.'
  )
  unless @user_method.is_a? NoopUserMethod
    ensure_remote_command("#{@user_method.cli_command_prefix} #{script}",
                          error: 'Failed to change to effective user, exit code: %{exit_code}',
                          tty: true,
                          user_method: @user_method,
                          close_stdin: false)
  end
  # The path should already be escaped
  ensure_remote_command("rm #{script}")
end

#prepare_startObject



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 181

def prepare_start
  @remote_script = cp_script_to_remote
  @output_path = File.join(File.dirname(@remote_script), 'output')
  @exit_code_path = File.join(File.dirname(@remote_script), 'exit_code')
  @pid_path = File.join(File.dirname(@remote_script), 'pid')
  su_method = @user_method.instance_of?(SuUserMethod)
  wrapper = <<~SCRIPT
    if [ "$1" = "inner" ]; then
      echo \$$ > #{@pid_path}
      (
        #{@user_method.cli_command_prefix}#{su_method ? "'exec #{@remote_script} < /dev/null '" : "#{@remote_script} < /dev/null"}
        echo \$? >#{@exit_code_path}
      ) | tee #{@output_path}
    else
      UNSHARE=''
      if #{UNSHARE_PREFIX} true >/dev/null 2>/dev/null; then
          UNSHARE='#{UNSHARE_PREFIX}'
      fi
      exec $UNSHARE "$0" inner
    fi
  SCRIPT
  @remote_script_wrapper = upload_data(
    wrapper,
    File.join(File.dirname(@remote_script), 'script-wrapper'),
    555)
end

#publish_data(data, type, pm = nil) ⇒ Object



255
256
257
258
259
260
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 255

def publish_data(data, type, pm = nil)
  pm ||= @process_manager
  data = data.dup if data.frozen?
  super(data.force_encoding('UTF-8'), type) unless @user_method.filter_password?(data)
  @user_method.on_data(data, pm.stdin) if pm
end

#refreshObject



214
215
216
217
218
219
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 214

def refresh
  return if @process_manager.nil?
  super
ensure
  check_expecting_disconnect
end

#startObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 146

def start
  Proxy::RemoteExecution::Utils.prune_known_hosts!(@host, @ssh_port, logger) if @first_execution
  ensure_local_directory(@socket_working_dir)
  @connection = MultiplexedSSHConnection.new(@options.merge(:id => @id), logger: logger)
  @connection.establish!
  preflight_checks
  prepare_start
  script = initialization_script
  logger.debug("executing script:\n#{indent_multiline(script)}")
  trigger(script)
rescue StandardError, NotImplementedError => e
  logger.error("error while initializing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
  publish_exception('Error initializing command', e)
end

#timeoutObject



231
232
233
234
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 231

def timeout
  @logger.debug('job timed out')
  super
end

#timeout_intervalObject



236
237
238
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 236

def timeout_interval
  execution_timeout_interval
end

#trigger(*args) ⇒ Object



161
162
163
# File 'lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb', line 161

def trigger(*args)
  run_async(*args)
end