aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/leap_cli/commands/bootstrap.rb131
-rw-r--r--lib/leap_cli/commands/clean.rb16
-rw-r--r--lib/leap_cli/commands/compile.rb11
-rw-r--r--lib/leap_cli/commands/user.rb7
-rw-r--r--lib/leap_cli/commands/util.rb103
-rw-r--r--lib/leap_cli/config/manager.rb7
-rw-r--r--lib/leap_cli/log.rb3
-rw-r--r--lib/leap_cli/util.rb90
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