Class: Sunshine::RemoteShell

Inherits:
Shell
  • Object
show all
Defined in:
lib/sunshine/remote_shell.rb

Overview

Keeps an SSH connection open to a server the app will be deployed to. Deploy servers use the ssh command and support any ssh feature. By default, deploy servers use the ControlMaster feature to share socket connections, with the ControlPath = ~/.ssh/sunshine-%r%h:%p

Setting session-persistant environment variables is supported by accessing the @env attribute.

Constant Summary collapse

LOGIN_LOOP =

The loop to keep the ssh connection open.

"echo ok; echo ready; "+
"for (( ; ; )); do kill -0 $PPID && sleep 10 || exit; done;"
LOGIN_TIMEOUT =
30

Constants inherited from Shell

Shell::LOCAL_HOST, Shell::LOCAL_USER

Instance Attribute Summary collapse

Attributes inherited from Shell

#env, #input, #mutex, #output, #password, #pid, #sudo, #timeout

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Shell

#==, #agree, #ask, #choose, #close, #env_cmd, #execute, #idle?, #os_name, #prompt_for_password, #quote_cmd, #sh_cmd, #sudo_cmd, #symlink, #sync, #system, #timed_out?, #update_activity, #with_mutex, #with_session, #write

Constructor Details

#initialize(host, options = {}) ⇒ RemoteShell

Remote shells essentially need a host and optional user. Typical instantiation is done through either of these methods:

RemoteShell.new "user@host"
RemoteShell.new "host", :user => "user"

The constructor also supports the following options:

:env

hash - hash of environment variables to set for the ssh session

:password

string - password for ssh login; if missing the deploy server

will attempt to prompt the user for a password.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/sunshine/remote_shell.rb', line 55

def initialize host, options={}
  super $stdout, options

  @host, @user = host.split("@").reverse

  @user ||= options[:user]

  @rsync_flags = ["-azrP"]
  @rsync_flags.concat [*options[:rsync_flags]] if options[:rsync_flags]

  @ssh_flags = [
    "-o ControlMaster=auto",
    "-o ControlPath=~/.ssh/sunshine-%r@%h:%p"
  ]
  @ssh_flags.concat ["-l", @user] if @user
  @ssh_flags.concat [*options[:ssh_flags]] if options[:ssh_flags]

  @parent_pid = nil

  self.class.register self
end

Instance Attribute Details

#hostObject (readonly)

Returns the value of attribute host.



40
41
42
# File 'lib/sunshine/remote_shell.rb', line 40

def host
  @host
end

#parent_pidObject (readonly)

Returns the value of attribute parent_pid.



40
41
42
# File 'lib/sunshine/remote_shell.rb', line 40

def parent_pid
  @parent_pid
end

#rsync_flagsObject

Returns the value of attribute rsync_flags.



41
42
43
# File 'lib/sunshine/remote_shell.rb', line 41

def rsync_flags
  @rsync_flags
end

#ssh_flagsObject

Returns the value of attribute ssh_flags.



41
42
43
# File 'lib/sunshine/remote_shell.rb', line 41

def ssh_flags
  @ssh_flags
end

#userObject (readonly)

Returns the value of attribute user.



40
41
42
# File 'lib/sunshine/remote_shell.rb', line 40

def user
  @user
end

Class Method Details

.disconnect_allObject

Closes all remote shell connections.



25
26
27
28
# File 'lib/sunshine/remote_shell.rb', line 25

def self.disconnect_all
  return unless defined?(@remote_shells)
  @remote_shells.each{|rs| rs.disconnect}
end

.register(remote_shell) ⇒ Object

Registers a remote shell for global access from the class. Handled automatically on initialization.



35
36
37
# File 'lib/sunshine/remote_shell.rb', line 35

def self.register remote_shell
  (@remote_shells ||= []) << remote_shell
end

Instance Method Details

#build_remote_cmd(cmd, options = {}) ⇒ Object

Builds an ssh command with permissions, env, etc.



210
211
212
213
214
215
# File 'lib/sunshine/remote_shell.rb', line 210

def build_remote_cmd cmd, options={}
  cmd = sh_cmd   cmd
  cmd = env_cmd  cmd
  cmd = sudo_cmd cmd, options
  cmd = ssh_cmd  cmd, options
end

#build_rsync_flags(options) ⇒ Object

Figure out which rsync flags to use.



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/sunshine/remote_shell.rb', line 232

def build_rsync_flags options
  flags = @rsync_flags.dup

  remote_rsync = 'rsync'
  rsync_sudo = sudo_cmd remote_rsync, options

  unless rsync_sudo == remote_rsync
    flags << "--rsync-path='#{ rsync_sudo.join(" ") }'"
  end

  flags << "-e \"ssh #{@ssh_flags.join(' ')}\"" if @ssh_flags

  flags.concat [*options[:flags]] if options[:flags]

  flags
end

#call(command_str, options = {}, &block) ⇒ Object

Runs a command via SSH. Optional block is passed the stream(stderr, stdout) and string data.



82
83
84
85
86
# File 'lib/sunshine/remote_shell.rb', line 82

def call command_str, options={}, &block
  Sunshine.logger.info @host, "Running: #{command_str}" do
    execute build_remote_cmd(command_str, options), &block
  end
end

#connectObject

Connect to host via SSH and return process pid



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/sunshine/remote_shell.rb', line 92

def connect
  return true if connected?

  cmd = ssh_cmd quote_cmd(LOGIN_LOOP), :sudo => false

  @parent_pid, inn, out, err = popen4 cmd.join(" ")
  inn.sync = true

  data  = ""
  ready = nil
  start_time = Time.now.to_i

  until ready || out.eof?
    data << out.readpartial(1024)
    ready = data =~ /ready/

    raise TimeoutError if timed_out?(start_time, LOGIN_TIMEOUT)
  end

  unless connected?
    disconnect
    host_info = [@user, @host].compact.join("@")
    raise ConnectionError, "Can't connect to #{host_info}"
  end

  inn.close rescue nil
  out.close rescue nil
  err.close rescue nil

  @parent_pid
end

#connected?Boolean

Check if SSH session is open and returns process pid

Returns:

  • (Boolean)


128
129
130
# File 'lib/sunshine/remote_shell.rb', line 128

def connected?
  Process.kill(0, @parent_pid) && @parent_pid rescue false
end

#disconnectObject

Disconnect from host



136
137
138
139
# File 'lib/sunshine/remote_shell.rb', line 136

def disconnect
  kill_process @parent_pid, "HUP" rescue nil
  @parent_pid = nil
end

#download(from_path, to_path, options = {}, &block) ⇒ Object

Download a file via rsync



145
146
147
148
149
150
# File 'lib/sunshine/remote_shell.rb', line 145

def download from_path, to_path, options={}, &block
  from_path = "#{@host}:#{from_path}"
  Sunshine.logger.info @host, "Downloading #{from_path} -> #{to_path}" do
    execute rsync_cmd(from_path, to_path, options), &block
  end
end

#expand_path(path) ⇒ Object

Expand a path:

shell.expand_path "~user/thing"
#=> "/home/user/thing"


158
159
160
161
162
# File 'lib/sunshine/remote_shell.rb', line 158

def expand_path path
  dir = File.dirname path
  full_dir = call "cd #{dir} && pwd"
  File.join full_dir, File.basename(path)
end

#file?(filepath) ⇒ Boolean

Checks if the given file exists

Returns:

  • (Boolean)


168
169
170
# File 'lib/sunshine/remote_shell.rb', line 168

def file? filepath
  self.system "test -f #{filepath}"
end

#make_file(filepath, content, options = {}) ⇒ Object

Create a file remotely



194
195
196
197
198
199
200
201
202
203
204
# File 'lib/sunshine/remote_shell.rb', line 194

def make_file filepath, content, options={}

  temp_filepath =
    "#{TMP_DIR}/#{File.basename(filepath)}_#{Time.now.to_i}#{rand(10000)}"

  File.open(temp_filepath, "w+"){|f| f.write(content)}

  self.upload temp_filepath, filepath, options

  File.delete(temp_filepath)
end

#rsync_cmd(from_path, to_path, options = {}) ⇒ Object

Creates an rsync command.



253
254
255
256
# File 'lib/sunshine/remote_shell.rb', line 253

def rsync_cmd from_path, to_path, options={}
  cmd  = ["rsync", build_rsync_flags(options), from_path, to_path]
  cmd.flatten.compact.join(" ")
end

#ssh_cmd(cmd, options = nil) ⇒ Object

Wraps the command in an ssh call.



262
263
264
265
266
267
268
# File 'lib/sunshine/remote_shell.rb', line 262

def ssh_cmd cmd, options=nil
  options ||= {}

  flags = [*options[:flags]].concat @ssh_flags

  ["ssh", flags, @host, cmd].flatten.compact
end

#tty!(cmd = nil) ⇒ Object

Start an interactive shell with preset permissions and env. Optionally pass a command to be run first.



177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/sunshine/remote_shell.rb', line 177

def tty! cmd=nil
  sync do
    cmd = [cmd, "sh -il"].compact.join " && "
    cmd = quote_cmd cmd

    pid = fork do
      exec \
        ssh_cmd(sudo_cmd(env_cmd(cmd)), :flags => "-t").to_a.join(" ")
    end
    Process.waitpid pid
  end
end

#upload(from_path, to_path, options = {}, &block) ⇒ Object

Uploads a file via rsync.



221
222
223
224
225
226
# File 'lib/sunshine/remote_shell.rb', line 221

def upload from_path, to_path, options={}, &block
  to_path = "#{@host}:#{to_path}"
  Sunshine.logger.info @host, "Uploading #{from_path} -> #{to_path}" do
    execute rsync_cmd(from_path, to_path, options), &block
  end
end