diff options
| -rw-r--r-- | DEVNOTES | 99 | ||||
| -rw-r--r-- | README.md | 30 | ||||
| -rw-r--r-- | leap_cli.gemspec | 2 | ||||
| -rw-r--r-- | lib/leap_cli.rb | 2 | ||||
| -rw-r--r-- | lib/leap_cli/commands/compile.rb | 2 | ||||
| -rw-r--r-- | lib/leap_cli/commands/init.rb | 2 | ||||
| -rw-r--r-- | lib/leap_cli/commands/pre.rb | 4 | ||||
| -rw-r--r-- | lib/leap_cli/commands/user.rb | 106 | ||||
| -rw-r--r-- | lib/leap_cli/commands/util.rb | 125 | ||||
| -rw-r--r-- | lib/leap_cli/config/manager.rb | 18 | ||||
| -rw-r--r-- | lib/leap_cli/log.rb | 19 | ||||
| -rw-r--r-- | lib/leap_cli/path.rb | 10 | ||||
| -rw-r--r-- | lib/leap_cli/util.rb | 171 | 
13 files changed, 551 insertions, 39 deletions
| @@ -44,8 +44,9 @@ useful liberaries  notes to myself  user interaction +  gli -- http://davetron5000.github.com/gli/rdoc/classes/GLI/DSL.html    readline -  highline +  highline  https://github.com/JEG2/highline/tree/master/examples    terminal-tables    rainbow    http://stackoverflow.com/questions/9577718/what-ruby-libraries-should-i-use-for-building-a-console-based-application @@ -58,13 +59,11 @@ help    ronn -- write man pages in markdown  push examples +    https://github.com/net-ssh/net-ssh    https://github.com/seattlerb/rake-remote_task      http://docs.seattlerb.org/rake-remote_task/      https://github.com/seattlerb/rake-remote_task/blob/master/lib/rake/remote_task.rb -  https://github.com/davidwinter/sooty -    push puppet with rake/remote_task -    https://github.com/davidwinter/sooty/blob/master/lib/sooty.rb    calling rsync from ruby      https://github.com/RichGuk/rrsync/blob/master/rrsync.rb      http://rubyforge.org/projects/six-rsync/ @@ -74,3 +73,95 @@ push examples      https://github.com/delano/rye      https://github.com/adamwiggins/rush +ssh keygen +  https://github.com/duritong/puppet-sshd/blob/master/lib/puppet/parser/functions/ssh_keygen.rb + +invoke puppet +  https://github.com/davidwinter/sooty/blob/master/lib/sooty.rb + + +ssh +================================ + +fingerprints +-------------------- + +ssh-keygen -lf <keyfile> tells you the fingerprint of an encryption key + +  ls -1 /etc/ssh/*key* +  /etc/ssh/ssh_host_dsa_key +  /etc/ssh/ssh_host_dsa_key.pub +  /etc/ssh/ssh_host_rsa_key +  /etc/ssh/ssh_host_rsa_key.pub + +fetch the public host ida of a bunch of nodes: +  ssh-keyscan -t rsa <host list> + +ssh certificate authority +---------------------------------- + +maybe wait off on this: "The certificate cert format seems to have changed between 5.5 and 6.0" + +search for "ssh-keygen -s" + +http://blog.habets.pp.se/2011/07/OpenSSH-certificates +http://en.community.dell.com/techcenter/b/techcenter/archive/2011/09/08/setting-up-certificate-authority-keys-with-openssh-version-5-4.aspx +http://serverfault.com/questions/264515/how-to-revoke-an-ssh-certificate-not-ssh-identity-file + +ruby +--------------- + +ruby net::ssh + +      def generate_key_fingerprint(key) +        blob = Net::SSH::Buffer.from(:key, key).to_s +        fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":") + +        [blob, fingerprint] +      rescue ::Exception => e +        [nil, "(could not generate fingerprint: #{e.message})"] +      end + +      def exchange_keys +        result = send_kexinit +        verify_server_key(result[:server_key]) +        session_id = verify_signature(result) +        confirm_newkeys + +        return { :session_id        => session_id, +                 :server_key        => result[:server_key], +                 :shared_secret     => result[:shared_secret], +                 :hashing_algorithm => digester } +      end + +DNS +====================================== + +problem: we want to be able to refer to the nodes by hostname (in a variety of programs) without requiring an external dns server. + +idea: + +   simple lightweight ruby dns server -- https://github.com/ioquatix/rubydns +   another ruby dns server (eventmachine) -- https://github.com/nricciar/em-dns-server + +   modify /etc/resolveconf/resolve.conf.d/tail with +     nameserver locahost +   maybe like this: +     resolveconf -a eth0.leap 'nameserver localhost' + +   the problem is that there is probably already a resolving nameserver living at localhost. +   linux doesn't appear to have a way to let you specify the port number for dns lookups (unlike bsd). boo + +   a few other possibilies: +   * alter /etc/hosts +   * alter dnsmasq to use additional /etc/hosts files (simple switch for this). dnsmasq is running on my desktop, although there is no /etc/dnsmasq. +   * write a libnss_ruby or something that would let you use a custom db for /etc/nsswitch.conf +     see http://uw714doc.sco.com/en/SEC_admin/nssover.html + +ssh solution: + +  ssh -l root -o "HostName=10.9.8.7" -o "HostKeyAlias=server_a" server_a +.. + + + @@ -77,9 +77,35 @@ Options in the configuration files might be nested. For example:        }      } -When compiled into hiera and made available in puppet, this becomes a Hash object with flattened keys: +If the value string is prefixed with an '=' character, the value is evaluated as ruby. For example + +    { +      "domain": { +        "public": "domain.org" +      } +      "api_domain": "= 'api.' + domain.public" +    } + +In this case, "api_domain" will be set to "api.domain.org". + +The following methods are available to the evaluated ruby: + +* nodes -- A list of all nodes. This list can be filtered. + +* global.services -- A list of all services. + +* global.tags -- A list of all tags. + +* file(file_path) -- Inserts the full contents of the file. If the file is an erb +  template, it is rendered. The file is searched for by first checking platform +  and then provider/files, + +* variable -- Any variable inherited by a particular node is available +  by just referencing it using either hash notation or object notation +  (i.e. self['domain']['public'] or domain.public). Circular +  references are not allowed, but otherwise it is ok to nest +  evaluated values in other evaluated values. -    {"openvpn.ip_address" => "1.1.1.1"}  Node Configuration  ================================= diff --git a/leap_cli.gemspec b/leap_cli.gemspec index 6a495f0..cf785ba 100644 --- a/leap_cli.gemspec +++ b/leap_cli.gemspec @@ -43,4 +43,6 @@ spec = Gem::Specification.new do |s|    s.add_runtime_dependency('json_pure')    s.add_runtime_dependency('terminal-table')    s.add_runtime_dependency('highline') +  s.add_runtime_dependency('gpgme') +  end diff --git a/lib/leap_cli.rb b/lib/leap_cli.rb index 5d35c1e..0b3a59f 100644 --- a/lib/leap_cli.rb +++ b/lib/leap_cli.rb @@ -11,12 +11,12 @@ require 'core_ext/nil'  require 'leap_cli/init'  require 'leap_cli/path' +require 'leap_cli/util'  require 'leap_cli/log'  require 'leap_cli/config/object'  require 'leap_cli/config/object_list'  require 'leap_cli/config/manager' -  #  # make 1.8 act like ruby 1.9  # diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb index 8764e52..3e9d42d 100644 --- a/lib/leap_cli/commands/compile.rb +++ b/lib/leap_cli/commands/compile.rb @@ -5,7 +5,7 @@ module LeapCli      command :compile do |c|        c.action do |global_options,options,args|          manager.load(Path.provider) -        Path.ensure_dir(Path.hiera) +        ensure_dir(Path.hiera)          manager.export(Path.hiera)        end      end diff --git a/lib/leap_cli/commands/init.rb b/lib/leap_cli/commands/init.rb index 75cc876..de43a45 100644 --- a/lib/leap_cli/commands/init.rb +++ b/lib/leap_cli/commands/init.rb @@ -7,7 +7,7 @@ module LeapCli        c.action do |global_options,options,args|          directory = args.first          unless directory && directory.any? -          help_now! "Directory name is required." +          help! "Directory name is required."          end          directory = File.expand_path(directory)          if File.exists?(directory) diff --git a/lib/leap_cli/commands/pre.rb b/lib/leap_cli/commands/pre.rb index 2281bf6..ada6a6a 100644 --- a/lib/leap_cli/commands/pre.rb +++ b/lib/leap_cli/commands/pre.rb @@ -7,7 +7,7 @@ module LeapCli      desc 'Verbosity level 0..2'      arg_name 'level' -    default_value '0' +    default_value '1'      flag [:v, :verbose]      desc 'Specify the root directory' @@ -30,7 +30,7 @@ module LeapCli        if Path.ok?          true        else -        fail!("Could not find the root directory. Change current working directory or try --root") +        bail!("Could not find the root directory. Change current working directory or try --root")        end      end diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb new file mode 100644 index 0000000..af59074 --- /dev/null +++ b/lib/leap_cli/commands/user.rb @@ -0,0 +1,106 @@ +require 'gpgme' + +# +# notes: +# +# file ~/.gnupg/00440025.asc +# /home/elijah/.gnupg/00440025.asc: PGP public key block +# +# file ~/.ssh/id_rsa.pub +# /home/elijah/.ssh/id_rsa.pub: OpenSSH RSA public key +# + +module LeapCli +  module Commands + +    desc 'adds a new trusted sysadmin' +    arg_name '<username>', :optional => false, :multiple => false +    command :'add-user' do |c| + +      c.switch 'self', :desc => 'lets you choose among your public keys', :negatable => false +      c.flag 'ssh-pub-key', :desc => 'SSH public key file for this new user' +      c.flag 'pgp-pub-key', :desc => 'OpenPGP public key file for this new user' + +      c.action do |global_options,options,args| +        username = args.first +        if !username.any? && !options[:self] +          help! "Either 'username' or --self is required." +        end + +        ssh_pub_key = nil +        pgp_pub_key = nil + +        if options['ssh-pub-key'] +          ssh_pub_key = read_file!(options['ssh-pub-key']) +        end +        if options['pgp-pub-key'] +          pgp_pub_key = read_file!(options['pgp-pub-key']) +        end + +        if options[:self] +          username ||= `whoami`.strip +          ssh_pub_key ||= pick_ssh_key +          pgp_pub_key ||= pick_pgp_key +        end + +        assert!(ssh_pub_key, 'Sorry, could not find SSH public key.') +        assert!(pgp_pub_key, 'Sorry, could not find OpenPGP public key.') + +        if ssh_pub_key +          write_file!(:user_ssh, username, ssh_pub_key) +        end +        if pgp_pub_key +          write_file!(:user_pgp, username, pgp_pub_key) +        end +      end +    end + +    # +    # let the the user choose among the ssh public keys that we encounter, or just pick the key if there is only one. +    # +    def pick_ssh_key +      assert_bin! 'ssh-add' +      ssh_fingerprints = `ssh-add -l`.split("\n").compact +      assert! ssh_fingerprints.any?, 'Sorry, could not find any SSH public key for you. Have you run ssh-keygen?' + +      if ssh_fingerprints.length > 1 +        key_index = numbered_choice_menu('Choose your SSH public key', ssh_fingerprints) do |key, i| +          say("#{i+1}.  #{key}") +        end +      else +        key_index = 0 +      end + +      ssh_keys = `ssh-add -L`.split("\n").compact +      return ssh_keys[key_index] +    end + +    # +    # let the the user choose among the gpg public keys that we encounter, or just pick the key if there is only one. +    # +    def pick_pgp_key +      secret_keys = GPGME::Key.find(:secret) + +      assert_bin! 'gpg' +      assert! secret_keys.any?, 'Sorry, could not find any OpenPGP keys for you.' + +      if secret_keys.length > 1 +        key_index = numbered_choice_menu('Choose your OpenPGP public key', secret_keys) do |key, i| +          key_info = key.to_s.split("\n")[0..1].map{|line| line.sub(/^\s*(sec|uid)\s*/,'')}.join(' -- ') +          say("#{i+1}.  #{key_info}") +        end +      else +        key_index = 0 +      end + +      key_id = secret_keys[key_index].sha + +      # can't use this, it includes signatures: +      #puts GPGME::Key.export(key_id, :armor => true, :export_options => :export_minimal) + +      # export with signatures removed: +      return `gpg --armor --export-options export-minimal --export #{key_id}`.strip +    end + +  end +end
\ No newline at end of file diff --git a/lib/leap_cli/commands/util.rb b/lib/leap_cli/commands/util.rb new file mode 100644 index 0000000..ad4f01c --- /dev/null +++ b/lib/leap_cli/commands/util.rb @@ -0,0 +1,125 @@ +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. +#     # +    def numbered_choice_menu(msg, items, &block) +      while true +        say("\n" + msg + ':') +        items.each_with_index &block +        say("q.  quit") +        index = ask("number 1-#{items.length}> ") +        if index.empty? +          next +        elsif index =~ /q/ +          bail! +        else +          i = index.to_i - 1 +          if i < 0 || i >= items.length +            bail! +          else +            return i +          end +        end +      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 55575cf..b35251a 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -3,6 +3,7 @@ require 'yaml'  module LeapCli    module Config +      #      # A class to manage all the objects in all the configuration files.      # @@ -32,15 +33,16 @@ module LeapCli        # save compiled hiera .yaml files        #        def export(dir) -        Dir.glob(dir + '/*.yaml').each do |f| -          File.unlink(f) -        end +        existing_files = Dir.glob(dir + '/*.yaml') +        updated_files = []          @nodes.each do |name, node|            # not sure if people will approve of this change: -          # File.open("#{dir}/#{name}.#{node.domain_internal}.yaml", 'w') do |f| -          File.open("#{dir}/#{name}.yaml", 'w') do |f| -            f.write node.to_yaml -          end +          filepath = "#{dir}/#{name}.yaml" +          updated_files << filepath +          Util::write_file!(filepath, node.to_yaml) +        end +        (existing_files - updated_files).each do |filepath| +          Util::remove_file!(filepath)          end        end @@ -99,7 +101,7 @@ module LeapCli        end        def load_json(filename, config_type) -        log2 { filename.sub(/^#{Regexp.escape(Path.root)}/,'') } +        #log2 { filename.sub(/^#{Regexp.escape(Path.root)}/,'') }          #          # read file, strip out comments diff --git a/lib/leap_cli/log.rb b/lib/leap_cli/log.rb index fe8e5ac..ac35eae 100644 --- a/lib/leap_cli/log.rb +++ b/lib/leap_cli/log.rb @@ -1,15 +1,19 @@  module LeapCli +  extend self -  def self.log_level +  def log_level      @log_level    end -  def self.log_level=(value) +  def log_level=(value)      @log_level = value    end -  end +## +## LOGGING +## +  def log0(message=nil, &block)    if message      puts message @@ -38,12 +42,7 @@ def log2(message=nil, &block)    end  end -def help!(message=nil) -  ENV['GLI_DEBUG'] = "false" -  help_now!(message) +def progress(message) +  log1(" * " + message)  end -def fail!(message=nil) -  ENV['GLI_DEBUG'] = "false" -  exit_now!(message) -end
\ No newline at end of file diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb index 5dc8fe8..f3cbad9 100644 --- a/lib/leap_cli/path.rb +++ b/lib/leap_cli/path.rb @@ -36,16 +36,6 @@ module LeapCli        raise "No such directory '#{@root}'" unless File.directory?(@root)      end -    def self.ensure_dir(dir) -      unless File.directory?(dir) -        if File.exists?(dir) -          raise 'Unable to create directory "%s", file already exists.' % dir -        else -          FileUtils.mkdir_p(dir) -        end -      end -    end -      def self.find_file(name, filename)        path = [Path.files, filename].join('/')        return path if File.exists?(path) diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb new file mode 100644 index 0000000..67fca8d --- /dev/null +++ b/lib/leap_cli/util.rb @@ -0,0 +1,171 @@ +require 'md5' + +module LeapCli +  module Util +    extend self + +    ## +    ## 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 + +    ## +    ## FILES AND DIRECTORIES +    ## + +    def relative_path(path) +      path.sub(/^#{Regexp.escape(Path.provider)}\//,'') +    end + +    def progress_created(path) +      progress 'created %s' % relative_path(path) +    end + +    def progress_updated(path) +      progress 'updated %s' % relative_path(path) +    end + +    def progress_nochange(path) +      progress 'no change %s' % relative_path(path) +    end + +    def progress_removed(path) +      progress 'removed %s' % relative_path(path) +    end + +    # +    # creates a directory if it doesn't already exist +    # +    def ensure_dir(dir) +      unless File.directory?(dir) +        if File.exists?(dir) +          bail! 'Unable to create directory "%s", file already exists.' % dir +        else +          FileUtils.mkdir_p(dir) +          unless dir =~ /\/$/ +            dir = dir + '/' +          end +          progress_created dir +        end +      end +    end + +    NAMED_PATHS = { +      :user_ssh => 'users/#{arg}/#{arg}_ssh.pub', +      :user_pgp => 'users/#{arg}/#{arg}_pgp.pub' +    } + +    # +    # 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 + +    def write_file!(*args) +      if args.first.is_a? Symbol +        write_named_file!(*args) +      else +        write_to_path!(*args) +      end +    end + +    def remove_file!(file_path) +      if File.exists?(file_path) +        File.unlink(file_path) +        progress_removed(file_path) +      end +    end + +    # +    # saves a named file +    # +    def write_named_file!(name, arg, contents) +      assert!(NAMED_PATHS[name], "Error, I don't know the path for #{arg}") + +      filename = eval('"' + NAMED_PATHS[name] + '"') +      fullpath = Path.provider + '/' + filename + +      write_to_path!(fullpath, contents) +    end + +    def write_to_path!(filepath, contents) +      ensure_dir File.dirname(filepath) +      existed = File.exists?(filepath) +      if existed +        if file_content_is?(filepath, contents) +          progress_nochange filepath +          return +        end +      end + +      File.open(filepath, 'w') do |f| +        f.write contents +      end + +      if existed +        progress_updated filepath +      else +        progress_created filepath +      end +    end + +    private + +    def file_content_is?(filepath, contents) +      output = `md5sum '#{filepath}'`.strip +      if $?.to_i == 0 +        return output.split(" ").first == MD5.md5(contents).to_s +      else +        return false +      end +    end + +  end +end + | 
