Class: Nutshell::RemoteShell

Inherits:
Shell
  • Object
show all
Defined in:
lib/nutshell/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/nutshell-%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, #sudo, #timeout

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Shell

#==, #agree, #ask, #choose, #close, #env_cmd, #execute, #os_name, #prompt_for_password, #quote_cmd, #session, #sh_cmd, #sudo_cmd, #symlink, #sync, #system, #timed_out?, #update_timeout, #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.



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

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/nutshell-%r@%h:%p"
  ]
  @ssh_flags.concat ["-l", @user] if @user
  @ssh_flags.concat [*options[:ssh_flags]] if options[:ssh_flags]

  @pid, @inn, @out, @err = nil

  self.class.register self
end

Instance Attribute Details

#hostObject (readonly)

Returns the value of attribute host.



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

def host
  @host
end

#pidObject (readonly)

Returns the value of attribute pid.



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

def pid
  @pid
end

#rsync_flagsObject

Returns the value of attribute rsync_flags.



42
43
44
# File 'lib/nutshell/remote_shell.rb', line 42

def rsync_flags
  @rsync_flags
end

#ssh_flagsObject

Returns the value of attribute ssh_flags.



42
43
44
# File 'lib/nutshell/remote_shell.rb', line 42

def ssh_flags
  @ssh_flags
end

#userObject (readonly)

Returns the value of attribute user.



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

def user
  @user
end

Class Method Details

.disconnect_allObject

Closes all remote shell connections.



26
27
28
29
# File 'lib/nutshell/remote_shell.rb', line 26

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.



36
37
38
# File 'lib/nutshell/remote_shell.rb', line 36

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.



209
210
211
212
213
214
# File 'lib/nutshell/remote_shell.rb', line 209

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.



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/nutshell/remote_shell.rb', line 229

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.



83
84
85
# File 'lib/nutshell/remote_shell.rb', line 83

def call command_str, options={}, &block
  execute build_remote_cmd(command_str, options), &block
end

#connectObject

Connect to host via SSH and return process pid



91
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
# File 'lib/nutshell/remote_shell.rb', line 91

def connect
  return true if connected?

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

  @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 ready && connected?
    disconnect
    host_info = [@user, @host].compact.join("@")
    raise ConnectionError, "Can't connect to #{host_info}"
  end

  @inn.close
  @pid
end

#connected?Boolean

Check if SSH session is open and returns process pid

Returns:

  • (Boolean)


124
125
126
# File 'lib/nutshell/remote_shell.rb', line 124

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

#disconnectObject

Disconnect from host



132
133
134
135
136
137
138
139
140
# File 'lib/nutshell/remote_shell.rb', line 132

def disconnect
  @inn.close rescue nil
  @out.close rescue nil
  @err.close rescue nil

  kill_process @pid, "HUP" rescue nil

  @pid = nil
end

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

Download a file via rsync



146
147
148
149
# File 'lib/nutshell/remote_shell.rb', line 146

def download from_path, to_path, options={}, &block
  from_path = "#{@host}:#{from_path}"
  execute rsync_cmd(from_path, to_path, options), &block
end

#expand_path(path) ⇒ Object

Expand a path:

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


157
158
159
160
161
# File 'lib/nutshell/remote_shell.rb', line 157

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)


167
168
169
# File 'lib/nutshell/remote_shell.rb', line 167

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

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

Create a file remotely



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

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.



250
251
252
253
# File 'lib/nutshell/remote_shell.rb', line 250

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.



259
260
261
262
263
264
265
# File 'lib/nutshell/remote_shell.rb', line 259

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.



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

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



220
221
222
223
# File 'lib/nutshell/remote_shell.rb', line 220

def upload from_path, to_path, options={}, &block
  to_path = "#{@host}:#{to_path}"
  execute rsync_cmd(from_path, to_path, options), &block
end