summaryrefslogtreecommitdiff
path: root/lib/leap_cli/util/remote_command.rb
blob: 6353e368831dbc60accd60120b49d1c2ee23750e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
module LeapCli; module Util; module RemoteCommand
  extend self

  #
  # FYI
  #  Capistrano::Logger::IMPORTANT = 0
  #  Capistrano::Logger::INFO      = 1
  #  Capistrano::Logger::DEBUG     = 2
  #  Capistrano::Logger::TRACE     = 3
  #
  def ssh_connect(nodes, options={}, &block)
    options ||= {}
    node_list = parse_node_list(nodes)

    cap = new_capistrano
    cap.logger = LeapCli::Logger.new(:level => [LeapCli.log_level,3].min)
    user = options[:user] || 'root'
    cap.set :user, user
    cap.set :ssh_options, ssh_options # ssh options common to all nodes
    cap.set :use_sudo, false          # we may want to change this in the future

    # Allow password authentication when we are bootstraping a single node
    # (and key authentication fails).
    if options[:bootstrap] && node_list.size == 1
      hostname = node_list.values.first.name
      if options[:echo]
        cap.set(:password) { ask "Root SSH password for #{user}@#{hostname}> " }
      else
        cap.set(:password) { Capistrano::CLI.password_prompt " * Typed password will be hidden (use --echo to make it visible)\nRoot SSH password for #{user}@#{hostname}> " }
      end
    end

    node_list.each do |name, node|
      cap.server node.domain.full, :dummy_arg, node_options(node, options[:ssh_options])
    end

    yield cap
  rescue Capistrano::ConnectionError => exc
    # not sure if this will work if english is not the locale??
    if exc.message =~ /Too many authentication failures/
      at_exit {ssh_config_help_message}
    end
    raise exc
  end

  private

  #
  # For available options, see http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start
  #
  # Capistrano has some very evil behavior in it's ssh.rb:
  #
  #   ssh_options = Net::SSH.configuration_for(
  #     server.host, ssh_options.fetch(:config, true)
  #   ).merge(ssh_options)
  #   # Once we've loaded the config, we don't need Net::SSH to do it again.
  #   ssh_options[:config] = false
  #
  # Net:SSH is supposed to call Net::SSH.configuration_for, but Capistrano is doing it
  # in advance and then disabling loading of configs.
  #
  # The result of this is the following: if you have IdentityFile in your ~/.ssh/config
  # file, then the above code will transform the ssh_options by reading ~/.ssh/config
  # and adding the keys specified via IdentityFile to ssh_options...
  # AND IT WILL SET :keys_only TO TRUE.
  #
  # The problem is that :keys_only will disable Net:SSH's ability to use ssh-agent.
  # With :keys_only set to true, it will not consult the ssh-agent at all.
  #
  # So nice of capistrano to parse ~/.ssh/config for us, but then add flags to the
  # ssh_options that prevent's these options from being useful.
  #
  # The current hackaround is to force :keys_only to be false. This allows the config
  # to be read and also allows ssh-agent to still be used.
  #
  def ssh_options
    {
      :keys_only => false, # Don't you dare change this.
      :global_known_hosts_file => path(:known_hosts),
      :user_known_hosts_file => '/dev/null',
      :paranoid => true,
      :verbose => net_ssh_log_level
    }
  end

  def net_ssh_log_level
    case LeapCli.log_level
      when 1 then 3
      when 2 then 2
      when 3 then 1
      else 0
    end
  end

  #
  # For notes on advanced ways to set server-specific options, see
  # http://railsware.com/blog/2011/11/02/advanced-server-definitions-in-capistrano/
  #
  # if, in the future, we want to do per-node password options, it would be done like so:
  #
  #  password_proc = Proc.new {Capistrano::CLI.password_prompt "Root SSH password for #{node.name}"}
  #  return {:password => password_proc}
  #
  def node_options(node, ssh_options_override=nil)
    {
      :ssh_options => {
        # :host_key_alias => node.name, << incompatible with ports in known_hosts
        :host_name => node.ip_address,
        :port => node.ssh.port
      }.merge(contingent_ssh_options_for_node(node)).merge(ssh_options_override||{})
    }
  end

  def new_capistrano
    # load once the library files
    @capistrano_enabled ||= begin
      require 'capistrano'
      require 'capistrano/cli'
      require 'lib_ext/capistrano_connections'
      require 'leap_cli/remote/leap_plugin'
      require 'leap_cli/remote/puppet_plugin'
      require 'leap_cli/remote/rsync_plugin'
      Capistrano.plugin :leap, LeapCli::Remote::LeapPlugin
      Capistrano.plugin :puppet, LeapCli::Remote::PuppetPlugin
      Capistrano.plugin :rsync, LeapCli::Remote::RsyncPlugin
      true
    end

    # create capistrano instance
    cap = Capistrano::Configuration.new

    # add tasks to capistrano instance
    cap.load File.dirname(__FILE__) + '/../remote/tasks.rb'

    return cap
  end

  def contingent_ssh_options_for_node(node)
    opts = {}
    if node.vagrant?
      opts[:keys] = [vagrant_ssh_key_file]
      opts[:keys_only] = true # only use the keys specified above, and ignore whatever keys the ssh-agent is aware of.
      opts[:paranoid] = false # we skip host checking for vagrant nodes, because fingerprint is different for everyone.
      if LeapCli::log_level <= 1
        opts[:verbose] = :error # suppress all the warnings about adding host keys to known_hosts, since it is not actually doing that.
      end
    end
    return opts
  end

end; end; end