From c6d2272ddb370b9731e17b06fa08971e6cda5202 Mon Sep 17 00:00:00 2001 From: elijah Date: Sun, 14 Oct 2012 03:02:06 -0700 Subject: added add-user command --- DEVNOTES | 99 ++++++++++++++++++++++- README.md | 30 ++++++- leap_cli.gemspec | 2 + lib/leap_cli.rb | 2 +- lib/leap_cli/commands/compile.rb | 2 +- lib/leap_cli/commands/init.rb | 2 +- lib/leap_cli/commands/pre.rb | 4 +- lib/leap_cli/commands/user.rb | 106 ++++++++++++++++++++++++ lib/leap_cli/commands/util.rb | 125 ++++++++++++++++++++++++++++ lib/leap_cli/config/manager.rb | 18 +++-- lib/leap_cli/log.rb | 19 +++-- lib/leap_cli/path.rb | 10 --- lib/leap_cli/util.rb | 171 +++++++++++++++++++++++++++++++++++++++ 13 files changed, 551 insertions(+), 39 deletions(-) create mode 100644 lib/leap_cli/commands/user.rb create mode 100644 lib/leap_cli/commands/util.rb create mode 100644 lib/leap_cli/util.rb diff --git a/DEVNOTES b/DEVNOTES index 967f2a6..713d8c2 100644 --- 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 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 + +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 +.. + + + diff --git a/README.md b/README.md index fccd6d1..3995533 100644 --- 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 ================================= 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 '', :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 + -- cgit v1.2.3