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
|
require 'net/ssh/known_hosts'
require 'tempfile'
module LeapCli; module Commands
##
## COMMANDS
##
#desc 'Create a new configuration for a node'
#command :'new-node' do |c|
# c.action do |global_options,options,args|
# end
#end
desc 'Bootstraps a node, setting up ssh keys and installing prerequisites'
arg_name '<node-name>', :optional => false, :multiple => false
command :'init-node' do |c|
c.action do |global_options,options,args|
node = get_node_from_args(args)
ping_node(node)
save_public_host_key(node)
update_compiled_ssh_configs
ssh_connect(node, :bootstrap => true) do |ssh|
ssh.install_authorized_keys
ssh.install_prerequisites
end
end
end
desc 'not yet implemented'
command :'rename-node' do |c|
c.action do |global_options,options,args|
end
end
desc 'not yet implemented'
arg_name '<node-name>', :optional => false, :multiple => false
command :'rm-node' do |c|
c.action do |global_options,options,args|
remove_file!()
end
end
##
## PUBLIC HELPERS
##
#
# generates the known_hosts file.
#
# we do a 'late' binding on the hostnames and ip part of the ssh pub key record in order to allow
# for the possibility that the hostnames or ip has changed in the node configuration.
#
def update_known_hosts
buffer = StringIO.new
manager.nodes.values.each do |node|
hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',')
pub_key = read_file([:node_ssh_pub_key,node.name])
if pub_key
buffer << [hostnames, pub_key].join(' ')
end
end
write_file!(:known_hosts, buffer.string)
end
def get_node_from_args(args)
node_name = args.first
node = manager.node(node_name)
assert!(node, "Node '#{node_name}' not found.")
node
end
private
##
## PRIVATE HELPERS
##
#
# 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)
progress("Fetching public SSH host key for #{node.name}")
public_key, key_type = get_public_key_for_ip(node.ip_address)
pub_key_path = Path.named_path([:node_ssh_pub_key, node.name])
if Path.exists?(pub_key_path)
if file_content_equals?(pub_key_path, node_pub_key_contents(key_type, public_key))
progress("Public SSH host key for #{node.name} has not changed")
else
bail!("WARNING: The public SSH host key we just fetched for #{node.name} doesn't match what we have saved previously. Remove the file #{pub_key_path} if you really want to change it.")
end
elsif key_in_known_hosts?(public_key, [node.name, node.ip_address, node.domain.name])
progress("Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)")
else
fingerprint, bits = ssh_key_fingerprint(key_type, public_key)
puts
say("This is the SSH host key you got back from node \"#{node.name}\"")
say("Type -- #{bits} bit #{key_type.upcase}")
say("Fingerprint -- " + fingerprint)
say("Public Key -- " + public_key)
if !agree("Is this correct? ")
bail!
else
puts
write_file!([:node_ssh_pub_key, node.name], node_pub_key_contents(key_type, public_key))
end
end
end
def get_public_key_for_ip(address)
assert_bin!('ssh-keyscan')
output = assert_run! "ssh-keyscan -t rsa #{address}", "Could not get the public host key. Maybe sshd is not running?"
line = output.split("\n").grep(/^[^#]/).first
assert! line, "Got zero host keys back!"
ip, key_type, public_key = line.split(' ')
return [public_key, key_type]
end
#
# returns true if the particular host_key is found in a "known_hosts" file installed for the current user or on this machine.
#
# - host_key: string of ssh public host key
# - identifiers: an array of identifers (which could be an ip address or hostname)
#
def key_in_known_hosts?(host_key, identifiers)
identifiers.each do |identifier|
Net::SSH::KnownHosts.search_for(identifier).each do |key|
# i am not sure what format ssh keys are in, but key.to_pem returns something different than we need.
# this little bit of magic code will encode correctly. I think the format is base64 encoding of bits, exponent, and modulus.
key_string = [Net::SSH::Buffer.from(:key, key).to_s].pack("m*").gsub(/\s/, "")
return true if key_string == host_key
end
end
return false
end
#
# gets a fingerprint for a key string
#
# i think this could better be done this way:
# blob = Net::SSH::Buffer.from(:key, key).to_s
# fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":")
#
def ssh_key_fingerprint(type, key)
assert_bin!('ssh-keygen')
file = Tempfile.new('leap_cli_public_key_')
begin
file.write(type)
file.write(" ")
file.write(key)
file.close
output = assert_run!("ssh-keygen -l -f #{file.path}", "Failed to run ssh-keygen on public key.")
bits, fingerprint, filename, key_type = output.split(' ')
return [fingerprint, bits]
ensure
file.close
file.unlink
end
end
def ping_node(node)
progress("Pinging #{node.name}")
assert_run!("ping -W 1 -c 1 #{node.ip_address}", "Could not ping #{node.name} (address #{node.ip_address}). Try again, we only send a single ping.")
end
#
# returns a string that can be used for the contents of the files/nodes/x/x_ssh_key.pub file
#
# We write the file without ipaddress or hostname, because these might change later.
# The ip and host is added at when compiling the combined known_hosts file.
#
def node_pub_key_contents(key_type, public_key)
[key_type, public_key].join(' ')
end
end; end
|