]> gitweb.fluxo.info Git - leap/leap_cli.git/commitdiff
added add-user command
authorelijah <elijah@riseup.net>
Sun, 14 Oct 2012 10:02:06 +0000 (03:02 -0700)
committerelijah <elijah@riseup.net>
Sun, 14 Oct 2012 10:02:06 +0000 (03:02 -0700)
13 files changed:
DEVNOTES
README.md
leap_cli.gemspec
lib/leap_cli.rb
lib/leap_cli/commands/compile.rb
lib/leap_cli/commands/init.rb
lib/leap_cli/commands/pre.rb
lib/leap_cli/commands/user.rb [new file with mode: 0644]
lib/leap_cli/commands/util.rb [new file with mode: 0644]
lib/leap_cli/config/manager.rb
lib/leap_cli/log.rb
lib/leap_cli/path.rb
lib/leap_cli/util.rb [new file with mode: 0644]

index 967f2a68b6ec440b11c416ea3c59faff95e61d5f..713d8c25933c81fdf164d83b36d1f607cb29f363 100644 (file)
--- a/DEVNOTES
+++ b/DEVNOTES
@@ -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
+..
+
+
+
index fccd6d1c7c97d79ebe37ee86c7a4826a8b02df4f..3995533326c1c5add9cede323c61a0c30891fbe2 100644 (file)
--- a/README.md
+++ b/README.md
@@ -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
 =================================
index 6a495f0433bba58637bb3a3234540557da56ccc1..cf785baa1ba8397a02edbae6a9936ecabad2a18f 100644 (file)
@@ -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
index 5d35c1ed4f2e27708741fba69ea66e7e663d8a3e..0b3a59f1df87dca276ca92ddfeba663d1a1570c1 100644 (file)
@@ -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
 #
index 8764e5235453f6b71b554b49036cfeb0b7404e05..3e9d42d7b720d2829f42a450c6d10d5be696bf3c 100644 (file)
@@ -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
index 75cc876f0575689a9efcd79c1650796c1181e7a9..de43a45e3766e9414e31946dc017e492726f0473 100644 (file)
@@ -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)
index 2281bf616c3938c36280e3a48331b12b72d88b92..ada6a6a0d5c15dda381301a24b464e378802b6f5 100644 (file)
@@ -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 (file)
index 0000000..af59074
--- /dev/null
@@ -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 (file)
index 0000000..ad4f01c
--- /dev/null
@@ -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
index 55575cff655adbf52107bfc6e165f13ced4b8a47..b35251a089ef0199912ac009282b601d3ff0d06d 100644 (file)
@@ -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
index fe8e5acd65126b0e36c6a01b763d85b9ce619df5..ac35eaeebbefc3b970fa27237ac0bb4a47a757ee 100644 (file)
@@ -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
index 5dc8fe8b5198d24a94908948acd14e7d72686df0..f3cbad9ebbca1b3864f66aa342a5c7d5d96a201b 100644 (file)
@@ -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 (file)
index 0000000..67fca8d
--- /dev/null
@@ -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
+