diff options
| -rw-r--r-- | lib/leap_cli/commands/clean.rb | 6 | ||||
| -rw-r--r-- | lib/leap_cli/commands/compile.rb | 16 | ||||
| -rw-r--r-- | lib/leap_cli/commands/deploy.rb | 21 | ||||
| -rw-r--r-- | lib/leap_cli/commands/node.rb (renamed from lib/leap_cli/commands/bootstrap.rb) | 100 | ||||
| -rw-r--r-- | lib/leap_cli/commands/pre.rb | 5 | ||||
| -rw-r--r-- | lib/leap_cli/commands/shell.rb | 12 | ||||
| -rw-r--r-- | lib/leap_cli/commands/user.rb | 19 | ||||
| -rw-r--r-- | lib/leap_cli/commands/util.rb | 159 | ||||
| -rw-r--r-- | lib/leap_cli/config/manager.rb | 28 | ||||
| -rw-r--r-- | lib/leap_cli/path.rb | 189 | ||||
| -rw-r--r-- | lib/leap_cli/remote/plugin.rb | 35 | ||||
| -rw-r--r-- | lib/leap_cli/remote/tasks.rb | 36 | ||||
| -rw-r--r-- | lib/leap_cli/util.rb | 134 | 
13 files changed, 547 insertions, 213 deletions
| 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 '<node filter>'      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/bootstrap.rb b/lib/leap_cli/commands/node.rb index 11188fb..46c8fb6 100644 --- a/lib/leap_cli/commands/bootstrap.rb +++ b/lib/leap_cli/commands/node.rb @@ -3,6 +3,10 @@ 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| @@ -13,12 +17,14 @@ module LeapCli; module Commands    arg_name '<node-name>', :optional => false, :multiple => false    command :'init-node' do |c|      c.action do |global_options,options,args| -      node_name = args.first -      node = manager.node(node_name) -      assert!(node, "Node '#{node_name}' not found.") -      progress("Pinging #{node.name}") -      assert_run!("ping -W 1 -c 1 #{node.ip_address}", "Could not ping #{node_name} (address #{node.ip_address}). Try again, we only send a single ping.") -      install_public_host_key(node) +      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 @@ -29,21 +35,65 @@ module LeapCli; module Commands    end    desc 'not yet implemented' +  arg_name '<node-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 install_public_host_key(node) +  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) -    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)") +    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 @@ -55,12 +105,9 @@ module LeapCli; module Commands          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 +        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) @@ -93,6 +140,10 @@ module LeapCli; module Commands    #    # 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_') @@ -110,22 +161,19 @@ module LeapCli; module Commands      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 +    # -  # generates the known_hosts file. +  # returns a string that can be used for the contents of the files/nodes/x/x_ssh_key.pub 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. +  # 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 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) +  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 '<node-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) @@ -86,6 +89,15 @@ module LeapCli        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.        #        def node(name) 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 | 
