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
|