Class: Indocker::SshSession

Inherits:
Object
  • Object
show all
Defined in:
lib/indocker/ssh_session.rb

Defined Under Namespace

Classes: ExecResult

Constant Summary collapse

LOCALHOST =
'localhost'.freeze
JUMP_CONTROL_PERSIST =
'600'.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host:, user:, port:, logger:, jump_host: nil) ⇒ SshSession

Returns a new instance of SshSession.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/indocker/ssh_session.rb', line 86

def initialize(host:, user:, port:, logger:, jump_host: nil)
  @host = host
  @user = user
  @port = port
  @jump_host = jump_host
  @logger = logger

  if host != LOCALHOST
    require 'net/ssh'

    options = {
      port:               @port,
      non_interactive:    true,
      timeout:            Indocker.ssh_connect_timeout,
      keepalive:          true,
      keepalive_interval: 10,
      keepalive_maxcount: 3,
    }

    if @jump_host
      require 'net/ssh/proxy/command'
      options[:proxy] = Net::SSH::Proxy::Command.new(self.class.jump_proxy_command(@jump_host))
    end

    @ssh = Net::SSH.start(@host, @user, options)
  end
end

Instance Attribute Details

#hostObject (readonly)

Returns the value of attribute host.



23
24
25
# File 'lib/indocker/ssh_session.rb', line 23

def host
  @host
end

#jump_hostObject (readonly)

Returns the value of attribute jump_host.



23
24
25
# File 'lib/indocker/ssh_session.rb', line 23

def jump_host
  @jump_host
end

#loggerObject (readonly)

Returns the value of attribute logger.



23
24
25
# File 'lib/indocker/ssh_session.rb', line 23

def logger
  @logger
end

#portObject (readonly)

Returns the value of attribute port.



23
24
25
# File 'lib/indocker/ssh_session.rb', line 23

def port
  @port
end

#userObject (readonly)

Returns the value of attribute user.



23
24
25
# File 'lib/indocker/ssh_session.rb', line 23

def user
  @user
end

Class Method Details

.for(server, logger:) ⇒ Object

Opens a session to a server as it is defined in the configuration, including its jump host, if any. Every remote operation goes through here, so a server reached through a jump host is reached that way by binaries sync, container deploys and the deployment checker alike.



76
77
78
79
80
81
82
83
84
# File 'lib/indocker/ssh_session.rb', line 76

def self.for(server, logger:)
  new(
    host:      server.host,
    user:      server.user,
    port:      server.port,
    jump_host: server.jump_host,
    logger:    logger
  )
end

.jump_proxy_command(jump_host) ⇒ Object

Builds the tunnel to a jump host, shared by Net::SSH sessions and by rsync.

Net::SSH::Proxy::Jump would build a bare ssh -W: a jump host that asks for a password hangs the deploy, and probing a server whose sshd is still starting prints "channel 0: open failed" from the ssh client on every retry.

Multiplexing matters even more. A deploy syncs and deploys to every server in parallel, and each of those opens its own tunnel; sshd drops everything above MaxStartups (10 by default), which surfaces as "connection closed by remote host" on random servers. Sharing one connection to the jump host keeps the whole fan-out down to a single tunnel.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/indocker/ssh_session.rb', line 35

def self.jump_proxy_command(jump_host)
  require 'digest'

  host = jump_host
  port = nil

  if host =~ /\A(.+):(\d+)\z/
    host = $1
    port = $2
  end

  control_path = "/tmp/indocker-jump-#{Digest::MD5.hexdigest(jump_host)[0, 10]}"

  command = [
    "ssh -q",
    "-o BatchMode=yes",
    "-o ConnectTimeout=#{Indocker.ssh_connect_timeout}",
    "-o ControlMaster=auto",
    "-o ControlPath=#{control_path}",
    "-o ControlPersist=#{JUMP_CONTROL_PERSIST}",
  ]

  command << "-p #{port}" if port
  command << "-W %h:%p"
  command << host

  command.join(' ')
end

.warm_jump_host(jump_host, logger:) ⇒ Object

Opens the shared connection to a jump host up front. Without it the first wave of parallel operations races to create the master connection and opens a burst of them.



66
67
68
69
70
# File 'lib/indocker/ssh_session.rb', line 66

def self.warm_jump_host(jump_host, logger:)
  command = jump_proxy_command(jump_host).sub("-W %h:%p ", "")

  Indocker::Shell.command("#{command} true", logger)
end

Instance Method Details

#closeObject



126
127
128
129
130
# File 'lib/indocker/ssh_session.rb', line 126

def close
  if @ssh
    @ssh.close
  end
end

#exec!(command) ⇒ Object



118
119
120
121
122
123
124
# File 'lib/indocker/ssh_session.rb', line 118

def exec!(command)
  if local?
    exec_locally!(command)
  else
    exec_remotely!(command)
  end
end

#local?Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/indocker/ssh_session.rb', line 114

def local?
  !@ssh
end