From 628165fd0a4e03bb7bbef3a464447924195e10b8 Mon Sep 17 00:00:00 2001 From: elijah Date: Tue, 23 Oct 2012 03:53:06 -0700 Subject: added a bunch of new commands, including init-node and deploy --- lib/leap_cli/commands/bootstrap.rb | 131 ------------------------- lib/leap_cli/commands/clean.rb | 6 +- lib/leap_cli/commands/compile.rb | 16 +--- lib/leap_cli/commands/deploy.rb | 21 +++-- lib/leap_cli/commands/node.rb | 179 +++++++++++++++++++++++++++++++++++ lib/leap_cli/commands/pre.rb | 5 + lib/leap_cli/commands/shell.rb | 12 +++ lib/leap_cli/commands/user.rb | 19 +++- lib/leap_cli/commands/util.rb | 159 ++++++++++++++++++++++++++----- lib/leap_cli/config/manager.rb | 28 ++++-- lib/leap_cli/path.rb | 189 ++++++++++++++++++++++++++++--------- lib/leap_cli/remote/plugin.rb | 35 +++++++ lib/leap_cli/remote/tasks.rb | 36 +++++++ lib/leap_cli/util.rb | 134 ++++++++++---------------- 14 files changed, 652 insertions(+), 318 deletions(-) delete mode 100644 lib/leap_cli/commands/bootstrap.rb create mode 100644 lib/leap_cli/commands/node.rb create mode 100644 lib/leap_cli/commands/shell.rb create mode 100644 lib/leap_cli/remote/plugin.rb create mode 100644 lib/leap_cli/remote/tasks.rb (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/bootstrap.rb b/lib/leap_cli/commands/bootstrap.rb deleted file mode 100644 index 11188fb..0000000 --- a/lib/leap_cli/commands/bootstrap.rb +++ /dev/null @@ -1,131 +0,0 @@ -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 '', :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 index ed9c901..8847b7d 100644 --- a/lib/leap_cli/commands/clean.rb +++ b/lib/leap_cli/commands/clean.rb @@ -4,11 +4,11 @@ module LeapCli 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| + Dir.glob(path([:hiera, '*'])).each do |file| remove_file! file end - remove_file! named_path(:authorized_keys) - remove_file! named_path(:known_hosts) + remove_file! path(:authorized_keys) + remove_file! path(:known_hosts) end end diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb index 429d1c5..c5bb93e 100644 --- a/lib/leap_cli/commands/compile.rb +++ b/lib/leap_cli/commands/compile.rb @@ -5,20 +5,14 @@ module LeapCli desc 'Compile json files to hiera configs' command :compile do |c| c.action do |global_options,options,args| - manager.load(Path.provider) - ensure_dir(Path.hiera) - manager.export(Path.hiera) - update_authorized_keys - update_known_hosts + update_compiled_ssh_configs # this must come first, hiera configs import these files. + manager.export Path.named_path(:hiera_dir) # generate a hiera .yaml config for each node 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) + def update_compiled_ssh_configs + update_authorized_keys + update_known_hosts end end diff --git a/lib/leap_cli/commands/deploy.rb b/lib/leap_cli/commands/deploy.rb index 9ec984c..c5efed5 100644 --- a/lib/leap_cli/commands/deploy.rb +++ b/lib/leap_cli/commands/deploy.rb @@ -6,12 +6,21 @@ module LeapCli arg_name '' command :deploy do |c| c.action do |global_options,options,args| - nodes = manager.filter(args) - say "Deploying to these nodes: #{nodes.keys.join(', ')}" - if agree "Continue? " - say "deploy not yet implemented" - else - say "OK. Bye." + nodes = manager.filter!(args) + if nodes.size > 1 + say "Deploying to these nodes: #{nodes.keys.join(', ')}" + unless agree "Continue? " + quit! "OK. Bye." + end + end + leap_root = '/root/leap' + ssh_connect(nodes) do |ssh| + ssh.leap.mkdir_leap leap_root + ssh.leap.rsync_update do |server| + node = manager.node(server.host) + {:source => Path.named_path([:hiera, node.name]), :dest => "#{leap_root}/config/#{node.name}.yaml"} + end + ssh.apply_puppet end end end diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb new file mode 100644 index 0000000..46c8fb6 --- /dev/null +++ b/lib/leap_cli/commands/node.rb @@ -0,0 +1,179 @@ +require 'net/ssh/known_hosts' +require 'tempfile' + +module LeapCli; module Commands + + ## + ## 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 '', :optional => false, :multiple => false + command :'init-node' do |c| + c.action do |global_options,options,args| + node = get_node_from_args(args) + ping_node(node) + save_public_host_key(node) + update_compiled_ssh_configs + ssh_connect(node, :bootstrap => true) do |ssh| + ssh.install_authorized_keys + ssh.install_prerequisites + end + end + end + + desc 'not yet implemented' + command :'rename-node' do |c| + c.action do |global_options,options,args| + end + end + + desc 'not yet implemented' + arg_name '', :optional => false, :multiple => false + command :'rm-node' do |c| + c.action do |global_options,options,args| + remove_file!() + end + end + + ## + ## PUBLIC HELPERS + ## + + # + # 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 + + def get_node_from_args(args) + node_name = args.first + node = manager.node(node_name) + assert!(node, "Node '#{node_name}' not found.") + node + end + + private + + ## + ## PRIVATE HELPERS + ## + + # + # saves the public ssh host key for node into the provider directory. + # + # see `man sshd` for the format of known_hosts + # + def save_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) + pub_key_path = Path.named_path([:node_ssh_pub_key, node.name]) + if Path.exists?(pub_key_path) + if file_content_equals?(pub_key_path, node_pub_key_contents(key_type, public_key)) + progress("Public SSH host key for #{node.name} has not changed") + else + bail!("WARNING: The public SSH host key we just fetched for #{node.name} doesn't match what we have saved previously. Remove the file #{pub_key_path} if you really want to change it.") + end + elsif key_in_known_hosts?(public_key, [node.name, node.ip_address, node.domain.name]) + progress("Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/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 + write_file!([:node_ssh_pub_key, node.name], node_pub_key_contents(key_type, public_key)) + 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 + # + # i think this could better be done this way: + # blob = Net::SSH::Buffer.from(:key, key).to_s + # fingerprint = OpenSSL::Digest::MD5.hexdigest(blob).scan(/../).join(":") + # + 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 + + def ping_node(node) + 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.") + end + + # + # returns a string that can be used for the contents of the files/nodes/x/x_ssh_key.pub file + # + # We write the file without ipaddress or hostname, because these might change later. + # The ip and host is added at when compiling the combined known_hosts file. + # + def node_pub_key_contents(key_type, public_key) + [key_type, public_key].join(' ') + end + +end; end \ No newline at end of file diff --git a/lib/leap_cli/commands/pre.rb b/lib/leap_cli/commands/pre.rb index ada6a6a..d80a9c2 100644 --- a/lib/leap_cli/commands/pre.rb +++ b/lib/leap_cli/commands/pre.rb @@ -20,6 +20,11 @@ module LeapCli # set verbosity # LeapCli.log_level = global[:verbose].to_i + if LeapCli.log_level > 1 + ENV['GLI_DEBUG'] = "true" + else + ENV['GLI_DEBUG'] = "false" + end # # require a root directory diff --git a/lib/leap_cli/commands/shell.rb b/lib/leap_cli/commands/shell.rb new file mode 100644 index 0000000..df392bd --- /dev/null +++ b/lib/leap_cli/commands/shell.rb @@ -0,0 +1,12 @@ +module LeapCli; module Commands + + desc 'Log in to the specified node with an interactive shell' + arg_name '', :optional => false, :multiple => false + command :shell, :ssh do |c| + c.action do |global_options,options,args| + node = get_node_from_args(args) + exec "ssh -l root -o 'HostName=#{node.ip_address}' -o 'HostKeyAlias=#{node.name}' -o 'UserKnownHostsFile=#{path(:known_hosts)}' -o 'StrictHostKeyChecking=yes' #{node.name}" + end + end + +end; end \ No newline at end of file diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb index 00c4b62..7be91c8 100644 --- a/lib/leap_cli/commands/user.rb +++ b/lib/leap_cli/commands/user.rb @@ -1,13 +1,14 @@ require 'gpgme' # -# notes: +# perhaps we want to verify that the key files are actually the key files we expect. +# we could use 'file' for this: # -# file ~/.gnupg/00440025.asc -# /home/elijah/.gnupg/00440025.asc: PGP public key block +# > file ~/.gnupg/00440025.asc +# ~/.gnupg/00440025.asc: PGP public key block # -# file ~/.ssh/id_rsa.pub -# /home/elijah/.ssh/id_rsa.pub: OpenSSH RSA public key +# > file ~/.ssh/id_rsa.pub +# ~/.ssh/id_rsa.pub: OpenSSH RSA public key # module LeapCli @@ -103,5 +104,13 @@ module LeapCli return `gpg --armor --export-options export-minimal --export #{key_id}`.strip end + def update_authorized_keys + buffer = StringIO.new + Dir.glob(path([:user_ssh, '*'])).each do |keyfile| + buffer << File.read(keyfile) + end + write_file!(:authorized_keys, buffer.string) + 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 index b5a102f..803fe88 100644 --- a/lib/leap_cli/commands/util.rb +++ b/lib/leap_cli/commands/util.rb @@ -1,34 +1,143 @@ -module LeapCli - module Commands - extend self - extend LeapCli::Util +module LeapCli; module Commands - # - # 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/ + extend self + extend LeapCli::Util + + def path(name) + Path.named_path(name) + end + + # + # 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 - i = index.to_i - 1 - if i < 0 || i >= items.length - bail! - else - return i - end + return i end end end + end + + # + # + # + # FYI + # Capistrano::Logger::IMPORTANT = 0 + # Capistrano::Logger::INFO = 1 + # Capistrano::Logger::DEBUG = 2 + # Capistrano::Logger::TRACE = 3 + # + def ssh_connect(nodes, options={}, &block) + node_list = parse_node_list(nodes) + + cap = new_capistrano + cap.logger.level = LeapCli.log_level + user = options[:user] || 'root' + cap.set :user, user + cap.set :ssh_options, ssh_options + cap.set :use_sudo, false # we may want to change this in the future + + # supply drop options + cap.set :puppet_source, [Path.platform, 'puppet'].join('/') + cap.set :puppet_destination, '/root/leap' + #cap.set :puppet_command, 'puppet apply' + cap.set :puppet_lib, "puppet/modules" + cap.set :puppet_parameters, '--confdir=puppet puppet/manifests/site.pp' + #cap.set :puppet_stream_output, false + #puppet apply --confdir=puppet puppet/manifests/site.pp | grep -v 'warning:.*is deprecated' + #puppet_cmd = "cd #{puppet_destination} && #{sudo_cmd} #{puppet_command} --modulepath=#{puppet_lib} #{puppet_parameters}" + + # + # allow password authentication when we are bootstraping a single node. + # + if options[:bootstrap] && node_list.size == 1 + hostname = node_list.values.first.name + cap.set(:password) { ask("SSH password for #{user}@#{hostname}> ") } # only called if needed + # this can be used instead to hide echo -- Capistrano::CLI.password_prompt + end + + node_list.each do |name, node| + cap.server node.name, :dummy_arg, node_options(node) + end + yield cap + end + + + private + + + # + # For available options, see http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start + # + def ssh_options + { + :config => false, + :user_known_hosts_file => path(:known_hosts), + :paranoid => true + } + end + + # + # For notes on advanced ways to set server-specific options, see + # http://railsware.com/blog/2011/11/02/advanced-server-definitions-in-capistrano/ + # + def node_options(node) + password_proc = Proc.new {Capistrano::CLI.password_prompt "Root SSH password for #{node.name}"} # only called if needed + { + :password => password_proc, + :ssh_options => { + :host_key_alias => node.name, + :host_name => node.ip_address, + :port => node.ssh.port + } + } + end + def new_capistrano + # load once the library files + @capistrano_enabled ||= begin + require 'capistrano' + #require 'capistrano/cli' + require 'leap_cli/remote/plugin' + Capistrano.plugin :leap, LeapCli::Remote::Plugin + true + end + + # create capistrano instance + cap = Capistrano::Configuration.new + + # add tasks to capistrano instance + cap.load File.dirname(__FILE__) + '/../remote/tasks.rb' + + return cap end -end + + def parse_node_list(nodes) + if nodes.is_a? Config::Object + Config::ObjectList.new(node_list) + elsif nodes.is_a? Config::ObjectList + nodes + elsif nodes.is_a? String + manager.filter!(nodes) + else + bail! "argument error" + end + end + +end; end diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb index 432ba0b..79ae5b8 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -18,12 +18,16 @@ module LeapCli # # load .json configuration files # - def load(dir) - @services = load_all_json("#{dir}/services/*.json") - @tags = load_all_json("#{dir}/tags/*.json") - @common = load_all_json("#{dir}/common.json")['common'] - @provider = load_all_json("#{dir}/provider.json")['provider'] - @nodes = load_all_json("#{dir}/nodes/*.json") + def load(provider_dir=Path.provider) + @services = load_all_json(Path.named_path( [:service_config, '*'], provider_dir )) + @tags = load_all_json(Path.named_path( [:tag_config, '*'], provider_dir )) + @common = load_all_json(Path.named_path( :common_config, provider_dir ))['common'] + @provider = load_all_json(Path.named_path( :provider_config, provider_dir ))['provider'] + @nodes = load_all_json(Path.named_path( [:node_config, '*'], provider_dir )) + + Util::assert!(@provider, "Failed to load provider.json") + Util::assert!(@common, "Failed to load common.json") + @nodes.each do |name, node| @nodes[name] = apply_inheritance(node) end @@ -32,11 +36,10 @@ module LeapCli # # save compiled hiera .yaml files # - def export(dir) + def export(dir=Path.named_path(:hiera_dir)) existing_files = Dir.glob(dir + '/*.yaml') updated_files = [] @nodes.each do |name, node| - # not sure if people will approve of this change: filepath = "#{dir}/#{name}.yaml" updated_files << filepath Util::write_file!(filepath, node.to_yaml) @@ -85,6 +88,15 @@ module LeapCli return node_list end + # + # same as filter(), but exits if there is no matching nodes + # + def filter!(filters) + node_list = filter(filters) + Util::assert! node_list.any?, "Could not match any nodes from '#{filters}'" + return node_list + end + # # returns a single Config::Object that corresponds to a Node. # diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb index f3cbad9..9b4e3c9 100644 --- a/lib/leap_cli/path.rb +++ b/lib/leap_cli/path.rb @@ -1,69 +1,166 @@ require 'fileutils' -module LeapCli - module Path +module LeapCli; module Path - def self.root - @root ||= File.expand_path("#{provider}/..") - end + NAMED_PATHS = { + # directories + :hiera_dir => 'hiera', + :files_dir => 'files', + :nodes_dir => 'nodes', + :services_dir => 'services', + :tags_dir => 'tags', - def self.platform - @platform ||= File.expand_path("#{root}/leap_platform") - end + # input config files + :common_config => 'common.json', + :provider_config => 'provider.json', + :node_config => 'nodes/#{arg}.json', + :service_config => 'services/#{arg}.json', + :tag_config => 'tags/#{arg}.json', - def self.provider - @provider ||= if @root - File.expand_path("#{root}/provider") - else - find_in_directory_tree('provider.json') - end - end + # output files + :user_ssh => 'users/#{arg}/#{arg}_ssh.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' + } - def self.hiera - @hiera ||= "#{provider}/hiera" - end + # + # required file structure + # + # Option 1 -- A project directory with platform and provider directories + # + # -: $root + # :-- leap_platform + # '-: provider + # '-- provider.json + # + # $root can be any name + # + # Option 2 -- A stand alone provider directory + # + # -: $provider + # '-- provider.json + # + # $provider can be any name. Some commands are not available. + # + # In either case, the 'leap' command must be run from inside the provider directory or + # you must specify root directory with --root=dir. + # - def self.files - @files ||= "#{provider}/files" - end + def self.root + @root ||= File.expand_path("#{provider}/..") + end + + def self.platform + @platform ||= File.expand_path("#{root}/leap_platform") + end + + def self.platform_provider + "#{platform}/provider" + end - def self.ok? - provider != '/' + def self.provider + @provider ||= if @root + File.expand_path("#{root}/provider") + else + find_in_directory_tree('provider.json') end + end + + def self.ok? + provider != '/' + end + + def self.set_root(root_path) + @root = File.expand_path(root_path) + raise "No such directory '#{@root}'" unless File.directory?(@root) + end - def self.set_root(root_path) - @root = File.expand_path(root_path) - raise "No such directory '#{@root}'" unless File.directory?(@root) + # + # all the places we search for a file when using find_file. + # this is perhaps too many places. + # + def self.search_path + @search_path ||= begin + search_path = [] + [Path.platform_provider, Path.provider].each do |provider| + files_dir = named_path(:files_dir, provider) + search_path << provider + search_path << named_path(:files_dir, provider) + search_path << named_path(:nodes_dir, files_dir) + search_path << named_path(:services_dir, files_dir) + search_path << named_path(:tags_dir, files_dir) + end + search_path end + end - def self.find_file(name, filename) - path = [Path.files, filename].join('/') + # + # tries to find a file somewhere with 'filename', under a directory 'name' if possible. + # + def self.find_file(name, filename) + # named path? + if filename.is_a? Symbol + path = named_path([filename, name], platform_provider) return path if File.exists?(path) - path = [Path.files, name, filename].join('/') + path = named_path([filename, name], provider) return path if File.exists?(path) - path = [Path.files, 'nodes', name, filename].join('/') - return path if File.exists?(path) - path = [Path.files, 'services', name, filename].join('/') + end + + # otherwise, lets search + search_path.each do |path_root| + path = [path_root, name, filename].join('/') return path if File.exists?(path) - path = [Path.files, 'tags', name, filename].join('/') + end + search_path.each do |path_root| + path = [path_root, filename].join('/') return path if File.exists?(path) + end + + # give up + return nil + end + + # + # Three ways of calling: + # + # - named_path [:user_ssh, 'bob'] ==> 'users/bob/bob_ssh.pub' + # - named_path :known_hosts ==> 'files/ssh/known_hosts' + # - named_path '/tmp/x' ==> '/tmp/x' + # + def self.named_path(name, provider_dir=Path.provider) + if name.is_a? Array + name, arg = name + else + arg = nil + end - # give up - return nil + if name.is_a? Symbol + Util::assert!(NAMED_PATHS[name], "Error, I don't know the path for :#{name} (with argument '#{arg}')") + filename = eval('"' + NAMED_PATHS[name] + '"') + return provider_dir + '/' + filename + else + return name end + end - private + def self.exists?(name, provider_dir=nil) + File.exists?(named_path(name, provider_dir)) + end + + private - def self.find_in_directory_tree(filename) - search_dir = Dir.pwd - while search_dir != "/" - Dir.foreach(search_dir) do |f| - return search_dir if f == filename - end - search_dir = File.dirname(search_dir) + def self.find_in_directory_tree(filename) + search_dir = Dir.pwd + while search_dir != "/" + Dir.foreach(search_dir) do |f| + return search_dir if f == filename end - return search_dir + search_dir = File.dirname(search_dir) end - + return search_dir end -end + +end; end diff --git a/lib/leap_cli/remote/plugin.rb b/lib/leap_cli/remote/plugin.rb new file mode 100644 index 0000000..22ffe34 --- /dev/null +++ b/lib/leap_cli/remote/plugin.rb @@ -0,0 +1,35 @@ +# +# these methods are made available in capistrano tasks as 'leap.method_name' +# + +module LeapCli; module Remote; module Plugin + + def mkdir_leap(base_dir) + run "mkdir -p #{base_dir}/config && chown -R root #{base_dir} && chmod -R ag-rwx,u+rwX #{base_dir}" + end + + # + # takes a block, yielded a server, that should return {:source => '', :dest => ''} + # + def rsync_update + SupplyDrop::Util.thread_pool_size = puppet_parallel_rsync_pool_size + servers = SupplyDrop::Util.optionally_async(find_servers, puppet_parallel_rsync) + failed_servers = servers.map do |server| + #p server + #p server.options + # build rsync command + _paths = yield server + _source = _paths[:source] + _user = server.user || fetch(:user, ENV['USER']) + _dest = SupplyDrop::Rsync.remote_address(_user, server.host, _paths[:dest]) + _opts = {:ssh => ssh_options.merge(server.options[:ssh_options]||{})} + rsync_cmd = SupplyDrop::Rsync.command(_source, _dest, _opts) + + # run command + logger.debug rsync_cmd + server.host unless system rsync_cmd + end.compact + raise "rsync failed on #{failed_servers.join(',')}" if failed_servers.any? + end + +end; end; end \ No newline at end of file diff --git a/lib/leap_cli/remote/tasks.rb b/lib/leap_cli/remote/tasks.rb new file mode 100644 index 0000000..e524133 --- /dev/null +++ b/lib/leap_cli/remote/tasks.rb @@ -0,0 +1,36 @@ +# +# This file is evaluated just the same as a typical capistrano "deploy.rb" +# For DSL manual, see https://github.com/capistrano/capistrano/wiki +# + +require 'supply_drop' + +MAX_HOSTS = 10 + +task :install_authorized_keys, :max_hosts => MAX_HOSTS do + run 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' + upload LeapCli::Path.named_path(:authorized_keys), '/root/.ssh/authorized_keys', :mode => '600' +end + +task :install_prerequisites, :max_hosts => MAX_HOSTS do + puppet.bootstrap.ubuntu + # + # runs this: + # run "mkdir -p #{puppet_destination}" + # run "#{sudo} apt-get update" + # run "#{sudo} apt-get install -y puppet rsync" + # +end + +#task :update_platform, :max_hosts => MAX_HOSTS do +# puppet.update_code +#end + +#task :mk_leap_dir, :max_hosts => MAX_HOSTS do +# run 'mkdir -p /root/leap/config && chown -R root /root/leap && chmod -R ag-rwx,u+rwX /root/leap' +#end + +task :apply_puppet, :max_hosts => MAX_HOSTS do + raise "now such directory #{puppet_source}" unless File.directory?(puppet_source) + puppet.apply +end diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb index 6095b2b..503f865 100644 --- a/lib/leap_cli/util.rb +++ b/lib/leap_cli/util.rb @@ -2,13 +2,6 @@ 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 @@ -29,8 +22,8 @@ module LeapCli # quit with a message that we are bailing out. # def bail!(message="") - say(message) - say("Bailing out.") + puts(message) + puts("Bailing out.") raise SystemExit.new #ENV['GLI_DEBUG'] = "false" #exit_now!(message) @@ -40,7 +33,7 @@ module LeapCli # quit with no message # def quit!(message='') - say(message) + puts(message) raise SystemExit.new end @@ -111,76 +104,58 @@ module LeapCli end end - NAMED_PATHS = { - :user_ssh => 'users/#{arg}/#{arg}_ssh.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' - } - - 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 + ## + ## FILE READING, WRITING, DELETING, and MOVING + ## # - # Three ways to call: + # All file read and write methods support using named paths in the place of an actual file path. + # + # To call using a named path, use a symbol in the place of filepath, like so: + # + # read_file(:known_hosts) + # + # In some cases, the named path will take an argument. In this case, set the filepath to be an array: # - # - 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) + # write_file!([:user_ssh, 'bob'], ssh_key_str) # + # To resolve a named path, use the shortcut helper 'path()' # - 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]) + # path([:user_ssh, 'bob']) ==> files/users/bob/bob_ssh_pub.key + # + + def read_file!(filepath) + filepath = Path.named_path(filepath) + if !File.exists?(filepath) + bail!("File '%s' does not exist." % exc.file_path) else - write_to_path!(*args) + File.read(filepath) end end - def remove_file!(file_path) - if File.exists?(file_path) - File.unlink(file_path) - progress_removed(file_path) + def read_file(filepath) + filepath = Path.named_path(filepath) + if !File.exists?(filepath) + nil + else + File.read(filepath) end end - # - # saves a named file. - # - 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 + def remove_file!(filepath) + filepath = Path.named_path(filepath) + if File.exists?(filepath) + File.unlink(filepath) + progress_removed(filepath) + end end - def write_to_path!(filepath, contents) + def write_file!(filepath, contents) + filepath = Path.named_path(filepath) ensure_dir File.dirname(filepath) existed = File.exists?(filepath) if existed - if file_content_is?(filepath, contents) + if file_content_equals?(filepath, contents) progress_nochange filepath return end @@ -197,9 +172,20 @@ module LeapCli end end - private + #def rename_file(filepath) + #end - def file_content_is?(filepath, contents) + #private + + ## + ## PRIVATE HELPER METHODS + ## + + # + # compares md5 fingerprints to see if the contents of a file match the string we have in memory + # + def file_content_equals?(filepath, contents) + filepath = Path.named_path(filepath) output = `md5sum '#{filepath}'`.strip if $?.to_i == 0 return output.split(" ").first == MD5.md5(contents).to_s @@ -208,24 +194,6 @@ 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 -- cgit v1.2.3