]> gitweb.fluxo.info Git - leap/leap_cli.git/commitdiff
added a bunch of new commands, including init-node and deploy
authorelijah <elijah@riseup.net>
Tue, 23 Oct 2012 10:53:06 +0000 (03:53 -0700)
committerelijah <elijah@riseup.net>
Tue, 23 Oct 2012 10:53:06 +0000 (03:53 -0700)
13 files changed:
lib/leap_cli/commands/clean.rb
lib/leap_cli/commands/compile.rb
lib/leap_cli/commands/deploy.rb
lib/leap_cli/commands/node.rb [moved from lib/leap_cli/commands/bootstrap.rb with 63% similarity]
lib/leap_cli/commands/pre.rb
lib/leap_cli/commands/shell.rb [new file with mode: 0644]
lib/leap_cli/commands/user.rb
lib/leap_cli/commands/util.rb
lib/leap_cli/config/manager.rb
lib/leap_cli/path.rb
lib/leap_cli/remote/plugin.rb [new file with mode: 0644]
lib/leap_cli/remote/tasks.rb [new file with mode: 0644]
lib/leap_cli/util.rb

index ed9c901715f21247e0cf52e362e1305b6511a503..8847b7d94a315323dbb0061bb020d16b360034fa 100644 (file)
@@ -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
 
index 429d1c5b2c18996767dd6900a29266c52475a96b..c5bb93e015bd224f4d0337406b98e39c773e65f0 100644 (file)
@@ -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
index 9ec984c3aba1ce719945b076d240a2f17c396256..c5efed5de0c83a0ff1b48a784cd56998e979128e 100644 (file)
@@ -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
similarity index 63%
rename from lib/leap_cli/commands/bootstrap.rb
rename to lib/leap_cli/commands/node.rb
index 11188fb9acdfa5b5e2578f66b9df716fb97af2ae..46c8fb6d2b89d982cd7dfad7c9e6f2468b2713af 100644 (file)
@@ -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
index ada6a6a0d5c15dda381301a24b464e378802b6f5..d80a9c23ac8a5666ec1ced5b9a2bf871739c9b0f 100644 (file)
@@ -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 (file)
index 0000000..df392bd
--- /dev/null
@@ -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
index 00c4b62afcc4c5482b4446cac9ca65191305fe2d..7be91c8841009f310cacf74c5c72ae12d93156c5 100644 (file)
@@ -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
index b5a102fd58c933acbb9b653e9bbc48f0cc07988b..803fe885d4ac33bf598481e32693dc4bf27b498d 100644 (file)
-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
index 432ba0bc44616daf7619ce4c11f36db7847dbb2c..79ae5b8fa4baf1e8fa571f230282bdec3e5b8bae 100644 (file)
@@ -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.
       #
index f3cbad9ebbca1b3864f66aa342a5c7d5d96a201b..9b4e3c9e341f511cfca90025b58b0db0ed1f2d49 100644 (file)
 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 (file)
index 0000000..22ffe34
--- /dev/null
@@ -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 (file)
index 0000000..e524133
--- /dev/null
@@ -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
index 6095b2ba943551b5b673fe301c67e27f528d508b..503f865e8344a9418c38d84b459debed088d0500 100644 (file)
@@ -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