diff options
-rw-r--r-- | lib/leap_cli/commands/bootstrap.rb | 131 | ||||
-rw-r--r-- | lib/leap_cli/commands/clean.rb | 16 | ||||
-rw-r--r-- | lib/leap_cli/commands/compile.rb | 11 | ||||
-rw-r--r-- | lib/leap_cli/commands/user.rb | 7 | ||||
-rw-r--r-- | lib/leap_cli/commands/util.rb | 103 | ||||
-rw-r--r-- | lib/leap_cli/config/manager.rb | 7 | ||||
-rw-r--r-- | lib/leap_cli/log.rb | 3 | ||||
-rw-r--r-- | lib/leap_cli/util.rb | 90 |
8 files changed, 253 insertions, 115 deletions
diff --git a/lib/leap_cli/commands/bootstrap.rb b/lib/leap_cli/commands/bootstrap.rb new file mode 100644 index 0000000..11188fb --- /dev/null +++ b/lib/leap_cli/commands/bootstrap.rb @@ -0,0 +1,131 @@ +require 'net/ssh/known_hosts' +require 'tempfile' + +module LeapCli; module 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_name = args.first + node = manager.node(node_name) + assert!(node, "Node '#{node_name}' not found.") + 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.") + install_public_host_key(node) + end + end + + desc 'not yet implemented' + command :'rename-node' do |c| + c.action do |global_options,options,args| + end + end + + desc 'not yet implemented' + command :'rm-node' do |c| + c.action do |global_options,options,args| + end + end + + # + # saves the public ssh host key for node into the provider directory. + # + # see `man sshd` for the format of known_hosts + # + def install_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) + if key_in_known_hosts?(public_key, [node.name, node.ip_address, node.domain.name]) + progress("Public ssh host key for #{node.name} is already trusted (key found in 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 + # we write the file without ipaddress or hostname, because these might change later, but we want to keep the same key. + write_file!([:node_ssh_pub_key, node.name], [key_type, public_key].join(' ')) + update_known_hosts + 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 + # + 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 + + # + # 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 + +end; end
\ No newline at end of file diff --git a/lib/leap_cli/commands/clean.rb b/lib/leap_cli/commands/clean.rb new file mode 100644 index 0000000..ed9c901 --- /dev/null +++ b/lib/leap_cli/commands/clean.rb @@ -0,0 +1,16 @@ +module LeapCli + module Commands + + desc 'Removes all files generated with the "compile" command' + command :clean do |c| + c.action do |global_options,options,args| + Dir.glob(named_path(:hiera, '*')).each do |file| + remove_file! file + end + remove_file! named_path(:authorized_keys) + remove_file! named_path(:known_hosts) + end + end + + end +end
\ No newline at end of file diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb index 3e9d42d..429d1c5 100644 --- a/lib/leap_cli/commands/compile.rb +++ b/lib/leap_cli/commands/compile.rb @@ -1,3 +1,4 @@ + module LeapCli module Commands @@ -7,7 +8,17 @@ module LeapCli manager.load(Path.provider) ensure_dir(Path.hiera) manager.export(Path.hiera) + update_authorized_keys + update_known_hosts + end + end + + def update_authorized_keys + buffer = StringIO.new + Dir.glob(named_path(:user_ssh, '*')).each do |keyfile| + buffer << File.read(keyfile) end + write_file!(:authorized_keys, buffer.string) end end diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb index af59074..00c4b62 100644 --- a/lib/leap_cli/commands/user.rb +++ b/lib/leap_cli/commands/user.rb @@ -13,7 +13,7 @@ require 'gpgme' module LeapCli module Commands - desc 'adds a new trusted sysadmin' + desc 'Adds a new trusted sysadmin' arg_name '<username>', :optional => false, :multiple => false command :'add-user' do |c| @@ -47,11 +47,12 @@ module LeapCli assert!(pgp_pub_key, 'Sorry, could not find OpenPGP public key.') if ssh_pub_key - write_file!(:user_ssh, username, ssh_pub_key) + write_file!([:user_ssh, username], ssh_pub_key) end if pgp_pub_key - write_file!(:user_pgp, username, pgp_pub_key) + write_file!([:user_pgp, username], pgp_pub_key) end + end end diff --git a/lib/leap_cli/commands/util.rb b/lib/leap_cli/commands/util.rb index ad4f01c..b5a102f 100644 --- a/lib/leap_cli/commands/util.rb +++ b/lib/leap_cli/commands/util.rb @@ -2,11 +2,12 @@ module LeapCli module Commands extend self extend LeapCli::Util -# # -# # keeps prompting the user for a numbered choice, until they pick a good one or bail out. -# # -# # block is yielded and is responsible for rendering the choices. -# # + + # + # keeps prompting the user for a numbered choice, until they pick a good one or bail out. + # + # block is yielded and is responsible for rendering the choices. + # def numbered_choice_menu(msg, items, &block) while true say("\n" + msg + ':') @@ -28,98 +29,6 @@ module LeapCli end end -# # -# # read a file, exit if the file doesn't exist. -# # -# def read_file!(file_path) -# if !File.exists?(file_path) -# bail!("File '%s' does not exist." % file_path) -# else -# File.readfile(file_path) -# end -# end - -# ## -# ## LOGGING -# ## - -# def log0(message=nil, &block) -# if message -# puts message -# elsif block -# puts yield(block) -# end -# end - -# def log1(message=nil, &block) -# if LeapCli.log_level > 0 -# if message -# puts message -# elsif block -# puts yield(block) -# end -# end -# end - -# def log2(message=nil, &block) -# if LeapCli.log_level > 1 -# if message -# puts message -# elsif block -# puts yield(block) -# end -# end -# end - -# def progress(message) -# log1(" * " + message) -# end - -# ## -# ## QUITTING -# ## - -# # -# # quit and print help -# # -# def help!(message=nil) -# ENV['GLI_DEBUG'] = "false" -# help_now!(message) -# #say("ERROR: " + message) -# end - -# # -# # quit with a message that we are bailing out. -# # -# def bail!(message="") -# say(message) -# say("Bailing out.") -# raise SystemExit.new -# #ENV['GLI_DEBUG'] = "false" -# #exit_now!(message) -# end - -# # -# # quit with no message -# # -# def quit!(message='') -# say(message) -# raise SystemExit.new -# end - -# # -# # bails out with message if assertion is false. -# # -# def assert!(boolean, message) -# bail!(message) unless boolean -# end - -# # -# # assert that the command is available -# # -# def assert_bin!(cmd_name) -# assert! `which #{cmd_name}`.strip.any?, "Sorry, bailing out, the command '%s' is not installed." % cmd_name -# end end end diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb index b35251a..432ba0b 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -85,6 +85,13 @@ module LeapCli return node_list end + # + # returns a single Config::Object that corresponds to a Node. + # + def node(name) + nodes[name] + end + private def load_all_json(pattern, config_type = :class) diff --git a/lib/leap_cli/log.rb b/lib/leap_cli/log.rb index ac35eae..58f1a1c 100644 --- a/lib/leap_cli/log.rb +++ b/lib/leap_cli/log.rb @@ -46,3 +46,6 @@ def progress(message) log1(" * " + message) end +def progress2(message) + log2(" * " + message) +end diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb index 67fca8d..6095b2b 100644 --- a/lib/leap_cli/util.rb +++ b/lib/leap_cli/util.rb @@ -1,6 +1,14 @@ require 'md5' module LeapCli + + class FileMissing < Exception + attr_reader :file_path + def initialize(file_path) + @file_path = file_path + end + end + module Util extend self @@ -50,6 +58,18 @@ module LeapCli assert! `which #{cmd_name}`.strip.any?, "Sorry, bailing out, the command '%s' is not installed." % cmd_name end + # + # assert that the command is run without an error. + # if successful, return output. + # + def assert_run!(cmd, message) + log2(" * run: #{cmd}") + cmd = cmd + " 2>&1" + output = `#{cmd}` + assert!($?.success?, message) + return output + end + ## ## FILES AND DIRECTORIES ## @@ -67,7 +87,7 @@ module LeapCli end def progress_nochange(path) - progress 'no change %s' % relative_path(path) + progress2 'no change %s' % relative_path(path) end def progress_removed(path) @@ -93,23 +113,43 @@ module LeapCli NAMED_PATHS = { :user_ssh => 'users/#{arg}/#{arg}_ssh.pub', - :user_pgp => 'users/#{arg}/#{arg}_pgp.pub' + :user_pgp => 'users/#{arg}/#{arg}_pgp.pub', + :hiera => 'hiera/#{arg}.yaml', + :node_ssh_pub_key => 'files/nodes/#{arg}/#{arg}_ssh_key.pub', + :known_hosts => 'files/ssh/known_hosts', + :authorized_keys => 'files/ssh/authorized_keys' } - # - # read a file, exit if the file doesn't exist. - # - def read_file!(file_path) - if !File.exists?(file_path) - bail!("File '%s' does not exist." % file_path) - else - File.readfile(file_path) + def read_file!(*args) + begin + try_to_read_file!(*args) + rescue FileMissing => exc + bail!("File '%s' does not exist." % exc.file_path) + end + end + + def read_file(*args) + begin + try_to_read_file!(*args) + rescue FileMissing => exc + return nil end end + # + # Three ways to call: + # + # - write_file!(file_path, file_contents) + # - write_file!(named_path, file_contents) + # - write_file!(named_path, file_contents, argument) -- deprecated + # - write_file!([named_path, argument], file_contents) + # + # def write_file!(*args) if args.first.is_a? Symbol write_named_file!(*args) + elsif args.first.is_a? Array + write_named_file!(args.first[0], args.last, args.first[1]) else write_to_path!(*args) end @@ -123,15 +163,17 @@ module LeapCli end # - # saves a named file + # saves a named file. # - def write_named_file!(name, arg, contents) - assert!(NAMED_PATHS[name], "Error, I don't know the path for #{arg}") + def write_named_file!(name, contents, arg=nil) + fullpath = named_path(name, arg) + write_to_path!(fullpath, contents) + end + def named_path(name, arg=nil) + assert!(NAMED_PATHS[name], "Error, I don't know the path for :#{name} (with argument '#{arg}')") filename = eval('"' + NAMED_PATHS[name] + '"') fullpath = Path.provider + '/' + filename - - write_to_path!(fullpath, contents) end def write_to_path!(filepath, contents) @@ -166,6 +208,24 @@ module LeapCli end end + # + # trys to read a file, raise exception if the file doesn't exist. + # + def try_to_read_file!(*args) + if args.first.is_a? Symbol + file_path = named_path(args.first) + elsif args.first.is_a? Array + file_path = named_path(*args.first) + else + file_path = args.first + end + if !File.exists?(file_path) + raise FileMissing.new(file_path) + else + File.read(file_path) + end + end + end end |