summaryrefslogtreecommitdiff
path: root/lib/leap_cli/commands/node_init.rb
blob: 59661295b0de928f6238073ddc98eebd65eee672 (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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#
# Node initialization.
# Most of the fun stuff is in tasks.rb.
#

module LeapCli; module Commands

  desc 'Node management'
  command :node do |cmd|
    cmd.desc 'Bootstraps a node or nodes, setting up SSH keys and installing prerequisite packages'
    cmd.long_desc "This command prepares a server to be used with the LEAP Platform by saving the server's SSH host key, " +
                   "copying the authorized_keys file, installing packages that are required for deploying, and registering important facts. " +
                   "Node init must be run before deploying to a server, and the server must be running and available via the network. " +
                   "This command only needs to be run once, but there is no harm in running it multiple times."
    cmd.arg_name 'FILTER'
    cmd.command :init do |init|
      #init.switch 'echo', :desc => 'If set, passwords are visible as you type them (default is hidden)', :negatable => false
      # ^^ i am not sure how to get this working with sshkit
      init.flag :port, :desc => 'Override the default SSH port.', :arg_name => 'PORT'
      init.flag :ip,   :desc => 'Override the default SSH IP address.', :arg_name => 'IPADDRESS'

      init.action do |global,options,args|
        run_node_init(global, options, args)
      end
    end
  end

  private

  def run_node_init(global, options, args)
    require 'leap_cli/ssh'
    assert! args.any?, 'You must specify a FILTER'
    finished = []
    manager.filter!(args).each_node do |node|
      is_node_alive(node, options)
      save_public_host_key(node, global, options) unless node.vagrant?
      update_compiled_ssh_configs
      # allow password auth for new nodes:
      options[:auth_methods] = ["publickey", "password"]
      if node.vm?
        # new AWS virtual machines will only allow login as 'admin'
        # before we continue, we must enable root access.
        SSH.remote_command(node, options.merge(:user => 'admin')) do |ssh, host|
          ssh.scripts.allow_root_ssh
        end
      end
      SSH.remote_command(node, options) do |ssh, host|
        if node.vagrant?
          ssh.scripts.install_insecure_vagrant_key
        end
        ssh.scripts.install_authorized_keys
        ssh.scripts.install_prerequisites
        unless node.vagrant?
          ssh.log(:checking, "SSH host keys") do
            response = ssh.capture(get_ssh_keys_cmd, :log_output => false)
            if response
              update_local_ssh_host_keys(node, response)
            end
          end
        end
        ssh.log(:updating, "facts") do
          response = ssh.capture(facter_cmd)
          if response
            update_node_facts(node.name, response)
          end
        end
      end
      finished << node.name
    end
    log :completed, "initialization of nodes #{finished.join(', ')}"
  end

  ##
  ## PRIVATE HELPERS
  ##

  def is_node_alive(node, options)
    address = options[:ip] || node.ip_address
    port = options[:port] || node.ssh.port
    log :connecting, "to node #{node.name}"
    assert_run! "nc -zw3 #{address} #{port}",
      "Failed to reach #{node.name} (address #{address}, port #{port}). You can override the configured IP address and port with --ip or --port."
  end

  #
  # saves the public ssh host key for node into the provider directory.
  #
  # see `man sshd` for the format of known_hosts
  #
  def save_public_host_key(node, global, options)
    log :fetching, "public SSH host key for #{node.name}"
    address = options[:ip] || node.ip_address
    port = options[:port] || node.ssh.port
    host_keys = get_public_keys_for_ip(address, port)
    pub_key_path = Path.named_path([:node_ssh_pub_key, node.name])

    if Path.exists?(pub_key_path)
      if host_keys.include? SSH::Key.load(pub_key_path)
        log :trusted, "- Public SSH host key for #{node.name} matches previously saved key", :indent => 1
      else
        bail! do
          log :error, "The public SSH host keys we just fetched for #{node.name} doesn't match what we have saved previously.", :indent => 1
          log "Delete the file #{pub_key_path} if you really want to remove the trusted SSH host key.", :indent => 2
        end
      end
    else
      known_key = host_keys.detect{|k|k.in_known_hosts?(node.name, node.ip_address, node.domain.name)}
      if known_key
        log :trusted, "- Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)"
      else
        public_key = SSH::Key.pick_best_key(host_keys)
        if public_key.nil?
          bail!("We got back #{host_keys.size} host keys from #{node.name}, but we can't support any of them.")
        else
          say("   This is the SSH host key you got back from node \"#{node.name}\"")
          say("   Type        -- #{public_key.bits} bit #{public_key.type.upcase}")
          say("   Fingerprint -- " + public_key.fingerprint)
          say("   Public Key  -- " + public_key.key)
          if !global[:yes] && !agree("   Is this correct? ")
            bail!
          else
            known_key = public_key
          end
        end
      end
      puts
      write_file! [:node_ssh_pub_key, node.name], known_key.to_s
    end
  end

  #
  # Get the public host keys for a host using ssh-keyscan.
  # Return an array of SSH::Key objects, one for each key.
  #
  def get_public_keys_for_ip(address, port=22)
    assert_bin!('ssh-keyscan')
    output = assert_run! "ssh-keyscan -p #{port} #{address}", "Could not get the public host key from #{address}:#{port}. Maybe sshd is not running?"
    if output.empty?
      bail! :failed, "ssh-keyscan returned empty output."
    end

    if output =~ /No route to host/
      bail! :failed, 'ssh-keyscan: no route to %s' % address
    else
      keys = SSH::Key.parse_keys(output)
      if keys.empty?
        bail! "ssh-keyscan got zero host keys back (that we understand)! Output was: #{output}"
      else
        return keys
      end
    end
  end

  # run on the server to generate a string suitable for passing to SSH::Key.parse_keys()
  def get_ssh_keys_cmd
    "/bin/grep ^HostKey /etc/ssh/sshd_config | /usr/bin/awk '{print $2 \".pub\"}' | /usr/bin/xargs /bin/cat"
  end

  #
  # Sometimes the ssh host keys on the server will be better than what we have
  # stored locally. In these cases, ask the user if they want to upgrade.
  #
  def update_local_ssh_host_keys(node, remote_keys_string)
    remote_keys = SSH::Key.parse_keys(remote_keys_string)
    return unless remote_keys.any?
    current_key = SSH::Key.load(Path.named_path([:node_ssh_pub_key, node.name]))
    best_key = SSH::Key.pick_best_key(remote_keys)
    return unless best_key && current_key
    if current_key != best_key
      say("   One of the SSH host keys for node '#{node.name}' is better than what you currently have trusted.")
      say("     Current key: #{current_key.summary}")
      say("     Better key: #{best_key.summary}")
      if agree("   Do you want to use the better key? ")
        write_file! [:node_ssh_pub_key, node.name], best_key.to_s
      end
    else
      log(3, "current host key does not need updating")
    end
  end

end; end