]> gitweb.fluxo.info Git - leap/leap_cli.git/commitdiff
added command init-node
authorelijah <elijah@riseup.net>
Sun, 21 Oct 2012 23:17:05 +0000 (16:17 -0700)
committerelijah <elijah@riseup.net>
Sun, 21 Oct 2012 23:17:05 +0000 (16:17 -0700)
lib/leap_cli/commands/bootstrap.rb [new file with mode: 0644]
lib/leap_cli/commands/clean.rb [new file with mode: 0644]
lib/leap_cli/commands/compile.rb
lib/leap_cli/commands/user.rb
lib/leap_cli/commands/util.rb
lib/leap_cli/config/manager.rb
lib/leap_cli/log.rb
lib/leap_cli/util.rb

diff --git a/lib/leap_cli/commands/bootstrap.rb b/lib/leap_cli/commands/bootstrap.rb
new file mode 100644 (file)
index 0000000..11188fb
--- /dev/null
@@ -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 (file)
index 0000000..ed9c901
--- /dev/null
@@ -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
index 3e9d42d7b720d2829f42a450c6d10d5be696bf3c..429d1c5b2c18996767dd6900a29266c52475a96b 100644 (file)
@@ -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
index af59074f6911dfbab9a01d30a6982ed924ae0618..00c4b62afcc4c5482b4446cac9ca65191305fe2d 100644 (file)
@@ -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
 
index ad4f01c5d66db6018f65418b92a33f04f8c98fc9..b5a102fd58c933acbb9b653e9bbc48f0cc07988b 100644 (file)
@@ -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
index b35251a089ef0199912ac009282b601d3ff0d06d..432ba0bc44616daf7619ce4c11f36db7847dbb2c 100644 (file)
@@ -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)
index ac35eaeebbefc3b970fa27237ac0bb4a47a757ee..58f1a1cb5197e9ca6cdc4cbd850174487211f7dd 100644 (file)
@@ -46,3 +46,6 @@ def progress(message)
   log1(" * " + message)
 end
 
+def progress2(message)
+  log2(" * " + message)
+end
index 67fca8d0c4c7df024bd27d26d551d66c27d6409b..6095b2ba943551b5b673fe301c67e27f528d508b 100644 (file)
@@ -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