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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
require 'net/ssh/known_hosts'
require 'tempfile'
require 'ipaddr'
module LeapCli; module Commands
##
## COMMANDS
##
desc 'Node management'
command :node do |node|
node.desc 'Create a new configuration file for a node named NAME.'
node.long_desc ["If specified, the optional argument SEED can be used to seed values in the node configuration file.",
"The format is property_name:value.",
"For example: `leap node add web1 ip_address:1.2.3.4 services:webapp`.",
"To set nested properties, property name can contain '.', like so: `leap node add web1 ssh.port:44`",
"Separeate multiple values for a single property with a comma, like so: `leap node add mynode services:webapp,dns`"].join("\n\n")
node.arg_name 'NAME [SEED]' # , :optional => false, :multiple => false
node.command :add do |add|
add.switch :local, :desc => 'Make a local testing node (by automatically assigning the next available local IP address). Local nodes are run as virtual machines on your computer.', :negatable => false
add.action do |global_options,options,args|
# argument sanity checks
name = args.first
assert! name, 'No <node-name> specified.'
assert! name =~ /^[0-9a-z-]+$/, "illegal characters used in node name '#{name}'"
assert_files_missing! [:node_config, name]
# create and seed new node
node = Config::Node.new(manager)
if options[:local]
node['ip_address'] = pick_next_vagrant_ip_address
end
seed_node_data(node, args[1..-1])
validate_ip_address(node)
# write the file
write_file! [:node_config, name], node.dump_json + "\n"
node['name'] = name
if file_exists? :ca_cert, :ca_key
generate_cert_for_node(manager.reload_node(node))
end
end
end
node.desc 'Bootstraps a node or nodes, setting up SSH keys and installing prerequisite packages'
node.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, and installing packages that are required for deploying. " +
"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."
node.arg_name 'FILTER' #, :optional => false, :multiple => false
node.command :init do |init|
init.switch 'echo', :desc => 'If set, passwords are visible as you type them (default is hidden)', :negatable => false
init.action do |global,options,args|
assert! args.any?, 'You must specify a node-filter'
finished = []
manager.filter(args).each_node do |node|
ping_node(node)
save_public_host_key(node, global)
update_compiled_ssh_configs
ssh_connect(node, :bootstrap => true, :echo => options[:echo]) do |ssh|
ssh.install_authorized_keys
ssh.install_prerequisites
end
finished << node.name
end
log :completed, "initialization of nodes #{finished.join(', ')}"
end
end
node.desc 'Renames a node file, and all its related files.'
node.arg_name 'OLD_NAME NEW_NAME'
node.command :mv do |mv|
mv.action do |global_options,options,args|
node = get_node_from_args(args)
new_name = args.last
ensure_dir [:node_files_dir, new_name]
Path::NODE_PATHS.each do |path|
rename_file! [path, node.name], [path, new_name]
end
remove_directory! [:node_files_dir, node.name]
end
end
node.desc 'Removes all the files related to the node named NAME.'
node.arg_name 'NAME' #:optional => false #, :multiple => false
node.command :rm do |rm|
rm.action do |global_options,options,args|
node = get_node_from_args(args)
(Path::NODE_PATHS + [:node_files_dir]).each do |path|
remove_file! [path, node.name]
end
if node.vagrant?
vagrant_command("destroy --force", [node.name])
end
end
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.keys.sort.each do |node_name|
node = manager.nodes[node_name]
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(' ')
buffer << "\n"
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, global)
log :fetching, "public SSH host key for #{node.name}"
public_key = get_public_key_for_ip(node.ip_address, node.ssh.port)
pub_key_path = Path.named_path([:node_ssh_pub_key, node.name])
if Path.exists?(pub_key_path)
if public_key == SshKey.load_from_file(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 key we just fetched for #{node.name} doesn't match what we have saved previously.", :indent => 1
log "Remove the file #{pub_key_path} if you really want to change it.", :indent => 2
end
end
elsif public_key.in_known_hosts?(node.name, node.ip_address, node.domain.name)
log :trusted, "- Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)"
else
puts
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
puts
write_file! [:node_ssh_pub_key, node.name], public_key.to_s
end
end
end
def get_public_key_for_ip(address, port=22)
assert_bin!('ssh-keyscan')
output = assert_run! "ssh-keyscan -p #{port} -t rsa #{address}", "Could not get the public host key from #{address}:#{port}. 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 SshKey.load(public_key, key_type)
end
def ping_node(node)
log :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
def seed_node_data(node, args)
args.each do |seed|
key, value = seed.split(':')
if value =~ /,/
value = value.split(',')
end
assert! key =~ /^[0-9a-z\._]+$/, "illegal characters used in property '#{key}'"
if key =~ /\./
key_parts = key.split('.')
final_key = key_parts.pop
current_object = node
key_parts.each do |key_part|
current_object[key_part] = Config::Object.new
current_object = current_object[key_part]
end
current_object[final_key] = value
else
node[key] = value
end
end
end
def validate_ip_address(node)
IPAddr.new(node['ip_address'])
rescue ArgumentError
bail! do
if node['ip_address']
log :invalid, "ip_address #{node['ip_address'].inspect}"
else
log :missing, "ip_address"
end
end
end
end; end
|