Module: CollinsShell::Console::Commands

Defined in:
lib/collins_shell/console/commands.rb,
lib/collins_shell/console/commands/cd.rb,
lib/collins_shell/console/commands/io.rb,
lib/collins_shell/console/commands/cat.rb,
lib/collins_shell/console/commands/tail.rb,
lib/collins_shell/console/commands/versions.rb,
lib/collins_shell/console/commands/iterators.rb

Constant Summary collapse

Default =
Pry::CommandSet.new do
  import CollinsShell::Console::Commands::Cd
  import CollinsShell::Console::Commands::Cat
  import CollinsShell::Console::Commands::Tail
  import CollinsShell::Console::Commands::Io
  import CollinsShell::Console::Commands::Iterators
  import CollinsShell::Console::Commands::Versions
end
Cd =
Pry::CommandSet.new do
  create_command "cd" do
    include CollinsShell::Console::CommandHelpers

    description "Change context to an asset or path"
    group "Context"

    banner <<-BANNER
      Usage: cd /<tag>
             cd <tag>
             cd /path
             cd path

      Changes context to the specified tag or path.
      Further commands apply to the asset or path.
    BANNER
    def process
      path = arg_string.split(/\//)
      stack  = _pry_.binding_stack.dup

      stack = [stack.first] if path.empty?

      path.each do |context|
        begin
          case context.chomp
          when ""
            stack = [stack.first]
          when "."
            next
          when ".."
            unless stack.size == 1
              stack.pop
            end
          else
            # We know we start out with the root fs being the context
            fs_parent = stack.last.eval('self')
            # Pushing the path value onto the parent gives us back a child
            begin
              fs_child = fs_parent.push(context)
              # We can't have assets as children of assets, replace current asset with new one
              output.puts fs_child.path
              stack.push(Pry.binding_for(fs_child))
            rescue Exception => e
              output.puts("#{text.bold('Could not change context:')} #{e}")
            end
          end
        rescue Exception => e
          output.puts e.backtrace
          output.puts("Got exception: #{e}")
        end 
      end # path.each
      _pry_.binding_stack = stack
    end # def process
  end
end
Io =
Pry::CommandSet.new do
  create_command "wc", "Pipe results to wc to get the number of assets/lines" do
    command_options :keep_retval => true

    group "I/O"

    def process
      args.size
    end
  end

  create_command "more", "Ensure results are paginated" do
    command_options :keep_retval => true

    group "I/O"

    def process
      value = args.map {|a| a.to_s}.join("\n")
      render_output value, opts
      nil
    end
  end

end
Cat =
Pry::CommandSet.new do
  create_command "cat", "Output data associated with the specified asset" do

    include CollinsShell::Console::CommandHelpers

    group "I/O"

    def options(opt)
      opt.banner <<-BANNER
        Usage: cat [-l|--logs] [-b|--brief] [--help]

        Cat the specified asset or log.

        If you are in an asset context, cat does not require the asset tag. In an asset context you can do:
          cat                               # no-arg displays current asset
          cat -b                            # short-display
          cat -l                            # table formatted logs
          cat /var/log/SEVERITY             # display logs of a specified type
          cat /var/log/messages             # all logs
        If you are not in an asset context, cat requires the tag of the asset you want to display.
          cat                               # display this help
          cat asset-tag                     # display asset
          cat -b asset-tag                  # short-display
          cat -l asset-tag                  # table formatted logs
          cat /var/log/assets/asset-tag     # display logs for asset
          cat /var/log/hosts/hostname       # display logs for host with name
      BANNER
      opt.on :b, "brief", "Brief output, not detailed"
      opt.on :l, "logs", "Display logs as well"
    end

    def process
      stack = _pry_.binding_stack
      if args.first.to_s.start_with?('/var/log/') then
        display_logs args.first, stack
      else
        tag = resolve_asset_tag args.first, stack
        if tag.nil? then
          run "help", "cat"
          return
        end
        display_asset tag
      end
    end

    # Given a logfile specification, parse it and render it
    def display_logs logfile, stack
      rejects = ['var', 'log']
      paths = logfile.split('/').reject{|s| s.empty? || rejects.include?(s)}
      if paths.size == 2 then
        display_sub_logs paths[0], paths[1]
      elsif paths.size == 1 then
        display_asset_logs tag_from_stack(stack), paths[0]
      else
        run "help", "cat"
      end
    end

    # Render logs of the type `/var/log/assets/TAG` or `/var/log/hosts/HOSTNAME`
    def display_sub_logs arg1, arg2
      if arg1 == 'assets' then
        display_asset_logs arg2, 'messages'
      elsif arg1 == 'hosts' then
        asset = find_one_asset(['HOSTNAME', arg2])
        display_asset_logs asset, 'messages'
      else
        output.puts "#{text.bold('Invalid log type:')} Only 'assets' or 'hosts' are valid, found '#{arg1}'"
        output.puts
        run "help", "cat"
      end
    end

    # Render logs for an asset according to type, where type is 'messages' (all) or a severity
    def display_asset_logs asset_tag, type
      begin
        asset = Collins::Util.get_asset_or_tag(asset_tag).tag
      rescue => e
        output.puts "#{text.bold('Invalid asset:')} '#{asset_tag}' not valid - #{e}"
        return
      end
      severity = Collins::Api::Logging::Severity
      severity_level = severity.value_of type
      if type == 'messages' then
        get_and_print_logs asset_tag, "ASC"
      elsif not severity_level.nil? then
        get_and_print_logs asset_tag, "ASC", severity_level
      else
        message = "Only '/var/log/messages' or '/var/log/SEVERITY' are valid here"
        sevs = severity.to_a.join(', ')
        output.puts "#{text.bold('Invalid path specified:')}: #{message}"
        output.puts "Valid severity levels are: #{sevs}"
      end
    end

    def display_asset tag
      tag = tag.gsub(/^\//, '')
      if asset_exists? tag then
        asset = get_asset tag
        show_logs = opts.logs?
        show_details = !opts.brief?
        show_color = Pry.color
        logs = []
        logs = call_collins("logs(#{tag})") {|c| c.logs(tag, :size => 5000)} if show_logs
        printer = CollinsShell::AssetPrinter.new asset, shell_handle, :logs => logs, :detailed => show_details, :color => show_color
        render_output printer.to_s
      else
        output.puts  "#{text.bold('No such asset:')} #{tag}"
      end
    end

    def get_and_print_logs asset_tag, sort, filter = nil, size = 5000
      logs = call_collins "logs(#{asset_tag})" do |client|
        client.logs asset_tag, :sort => sort, :size => size, :filter => filter
      end
      printer = CollinsShell::LogPrinter.new asset_tag, logs
      output.puts printer.to_s
    end

  end # create_command
end
Tail =
Pry::CommandSet.new do
  create_command "tail", "Print the last lines of a log file" do

    include CollinsShell::Console::CommandHelpers

    group "I/O"

    def setup
      @lines = 10
      @follow = false
      @got_flags = false
      @sleep = 10
      @test = false
    end

    def integer_arg(opt, short, long, description, &block)
      opt.on short, long, description, :argument => true, :as => :integer do |i|
        if i < 1 then
          raise ArgumentError.new("Missing a required argument for --#{long}")
        end
        @got_flags = true
        block.call(i)
      end
    end

    def options(opt)
      opt.banner <<-BANNER
        Usage: tail [OPTION] [FILE]
               tail --follow [FILE]
               tail --lines N [FILE]
               tail --sleep N [FILE]

        Tail the specified log.

        If you are in an asset context, tail does not require the asset tag. In an asset context you can do:
          tail                                   # display this help
          tail -n 10                             # same as tail -n 10 /var/log/messages
          tail -f                                # same as tail -f /var/log/messages
          tail [-n|-f] /var/log/SEVERITY         # tail /var/log/SEVERITY
          tail /var/log/messages                 # last 10 messages
          tail -n 10 /var/log/messages           # last 10 messages
          tail -f /var/log/messages              # follow log messages
        If you are not in an asset context, log requires the tag of the asset you want to display.
          tail                                   # display this help
          tail [-n|-f] asset-tag                 # same as tail in asset context
          tail [-n|-f] /var/log/messages         # show logs for all assets (requires permission)
          tail [-n|-f] /var/log/assets/asset-tag # same as tail in asset context
          tail [-n|-f] /var/log/hosts/hostname   # same as tail in asset context, but finds host
      BANNER
      opt.on :f, "follow", "Output appended data as file grows" do
        @got_flags = true
        @follow = true
      end
      opt.on :t, :test, "Show logs that have already been seen" do
        @got_flags = true
        @test = true
      end
      integer_arg(opt, :n, :lines, "Show the last n lines of the file") do |i|
        @lines = i
      end
      integer_arg(opt, :s, :sleep, "Sleep this many seconds between pools") do |i|
        @sleep = i
      end
    end

    def process
      stack = _pry_.binding_stack
      if args.first.to_s.start_with?('/var/log/') then
        display_logs args.first, stack
      else
        tag = args.first.to_s.strip
        if tag.empty? then
          if asset_context?(stack) and @got_flags then
            display_asset_logs tag_from_stack(stack), 'messages'
          else
            run "help", "tail"
            return
          end
        else
          display_asset_logs tag, 'messages'
        end
      end
    end

    # Given a logfile specification, parse it and render it
    def display_logs logfile, stack
      rejects = ['var', 'log']
      paths = logfile.split('/').reject{|s| s.empty? || rejects.include?(s)}
      if paths.size == 2 then
        display_sub_logs paths[0], paths[1]
      elsif paths.size == 1 then
        display_asset_logs (tag_from_stack(stack) || 'all'), paths[0]
      else
        run "help", "tag"
      end
    end

    # Render logs of the type `/var/log/assets/TAG` or `/var/log/hosts/HOSTNAME`
    def display_sub_logs arg1, arg2
      if arg1 == 'assets' then
        display_asset_logs arg2, 'messages'
      elsif arg1 == 'hosts' then
        asset = find_one_asset(['HOSTNAME', arg2])
        display_asset_logs asset, 'messages'
      else
        output.puts "#{text.bold('Invalid log type:')} Only 'assets' or 'hosts' are valid, found '#{arg1}'"
        output.puts
        run "help", "tag"
      end
    end

    # Render logs for an asset according to type, where type is 'messages' (all) or a severity
    def display_asset_logs asset_tag, type
      begin
        asset = Collins::Util.get_asset_or_tag(asset_tag).tag
      rescue => e
        output.puts "#{text.bold('Invalid asset:')} '#{asset_tag}' not valid - #{e}"
        return
      end
      severity = Collins::Api::Logging::Severity
      severity_level = severity.value_of type
      if type == 'messages' then
        get_and_print_logs asset_tag
      elsif not severity_level.nil? then
        get_and_print_logs asset_tag, severity_level
      else
        message = "Only '/var/log/messages' or '/var/log/SEVERITY' are valid here"
        sevs = severity.to_a.join(', ')
        output.puts "#{text.bold('Invalid path specified:')}: #{message}"
        output.puts "Valid severity levels are: #{sevs}"
      end
    end

    def all? tag
      tag.to_s.downcase == 'all'
    end

    def get_and_print_logs asset_tag, filter = nil
      size = @lines
      if not @follow then
        logs = call_collins "logs(#{asset_tag})" do |client|
          client.logs asset_tag, :sort => "DESC", :size => size, :filter => filter, :all_tag => 'all'
        end.reverse
        printer = CollinsShell::LogPrinter.new asset_tag, :logs => logs, :all_tag => 'all'
        output.puts printer.to_s
      else
        seen = Set.new
        printer = CollinsShell::LogPrinter.new asset_tag, :streaming => true, :all_tag => 'all'
        while true do
          logs = call_collins "logs(#{asset_tag})" do |client|
            client.logs asset_tag, :sort => "DESC", :size => size, :filter => filter, :all_tag => 'all'
          end.reverse
          unseen_logs = select_logs_for_follow seen, logs
          if unseen_logs.size > 0 then
            output.puts printer.render(unseen_logs)
          end
          sleep(@sleep)
        end # while true
      end # else for follow
    end

    def select_logs_for_follow seen_set, logs
      if @test then
        logs
      else
        unseen_logs = logs.reject {|l| seen_set.include?(l.to_s.hash)}
        unseen_logs.each {|l| seen_set << l.to_s.hash}
        unseen_logs
      end
    end

  end # create_command
end
Versions =
Pry::CommandSet.new do
  create_command "latest", "Latest version of collins shell" do
    group "Software"

    def options(opt)
      opt.banner <<-BANNER
        Usage: latest

        Display the latest version of collins shell
      BANNER
    end

    def process
      o = CollinsShell::Console.options
      render_output CollinsShell::Cli.new([], o).get_latest_version
    end

  end # create_command

  create_command "version", "Current version of collins shell" do
    group "Software"

    def options(opt)
      opt.banner <<-BANNER
        Usage: version

        Display the current version of collins shell
      BANNER
    end

    def process
      o = CollinsShell::Console.options
      render_output CollinsShell::Cli.new([], o).get_version
    end

  end # create_command

end
Iterators =
Pry::CommandSet.new do

  create_command "ls", "Find assets according to specific criteria" do
    include CollinsShell::Console::OptionsHelpers
    include CollinsShell::Console::CommandHelpers

    command_options :keep_retval => true, :takes_block => true
    group "Context"

    # --return (after printing also return value) and --flood (disable paging)
    def options opt
      opt.banner <<-BANNER
        Usage: ls [-d|--delimiter] [-g|--grep] [-F--format] [-f|--flood] [path]

        ls provides you information based on your current context (either a path or an asset)

        When in an asset, ls will show you available commands
        When in a path, ls will show you either assets that match the path or values appropriate for the path
        When in no context, will show you available tags (to use in a path)

        Examples:
          ls /HOSTNAME/.*dev.*
          ls /HOSTNAME/.*dev.* --format='{{hostname}} {{status}} {{tag}}' --grep=blake

        You can customize the default format used by ls (when applied to assets) by creating a ~/.pryrc file with contents like:

            Pry.config.default_asset_format = '{{tag}} {{hostname}} {{status}}'

        Where the rhs of the default_asset_format is the format you want to use
      BANNER
      pager_options opt
      opt.on :d, "delimiter", "Delimiter for use with --format, defaults to \\n", :argument => true
      opt.on :r, "return", "Return values as well as outputting them"
      opt.on :g, "grep", "A regular expression to provide to results", :argument => true, :optional => false
      opt.on :F, "format", "Provide a format for output", :argument => true, :optional => false
    end

    def process
      # If a pipe is being used, grab the first pipe command
      first_after_pipe = arg_string.split('|', 2)[1].to_s.split(' ').first
      # Take a /PATH/FORMAT and convert it to an array
      path = args.first.to_s.split(/\//).reject{|s| s.empty? || s == "|" }
      # Account for Filesystem context
      stack  = _pry_.binding_stack.dup
      fs_node = stack.last.eval('self')
      # Are we just getting a naked query?
      display_commands = (fs_node.asset? && args.empty?)
      # Doing an ls in the stack, relative
      if not fs_node.root? and not arg_string.start_with?('/') then
        path.each do |context|
          case context.chomp
          when "", "." # blank is empty root node, . is self. Do nothing
            next
          when ".." # Up one level
            fs_node = fs_node.pop
          else
            begin
              fs_node = fs_node.push(context)
            rescue Exception => e
              output.puts("#{text.bold('Could not check context:')} #{e}")
              raise e
            end
          end
        end
        path = fs_node.stack
      end
      if command_block || get_format then
        details = true
      else
        details = false
      end
      # Should we just display commands?
      if display_commands then
        output.puts("Available formats (see ls --help):")
        r = fs_node.asset_methods(true)
        process_values r.map{|m| "{{#{m}}}"}, 8
        output.puts("\nAvailable commands:")
        process_values fs_node.available_commands, 8
        output.puts()
      # If we have nothing, grab all tags
      elsif path.size == 0 then
        value = get_all_tags
        process_values value
      # If we have an odd number, the last one is a tag so grab the values
      # for that tag
      elsif path.size % 2 == 1 then
        if virtual_tags.include?(path.last) then
          value = ["virtual tags have no values"]
        else
          value = get_tag_values(path.last) || []
        end
        process_values value
      # If we have an even number, grab assets that have these tag/values
      else
        assets = find_assets(path, details)
        process_values assets, 6
      end
    end

    # Handle faking unix pipes to commands, block invocation, and printing
    def process_values init_value, size = 4
      should_return = opts.return?
      cmd = arg_string.split('|',2)

      if cmd.size == 2 then
        cmds = cmd.last.split('|').map{|s| s.strip}
      else
        cmds = []
      end

      formatted = init_value
      formatter = nil
      if opts.format? then
        formatter = opts[:format]
      elsif Pry.config.default_asset_format then
        if formatted.any? {|o| o.is_a?(Collins::Asset)} then
          formatter = Pry.config.default_asset_format
        end
      end

      if formatter then
        formatted = formatted.map do |v|
          a = CollinsShell::Util::AssetStache.new(v)
          a.render formatter
        end
      end

      grep_regex = Regexp.new(opts[:g] || ".")
      if formatted.respond_to?(:grep) then
        formatted = formatted.grep(grep_regex)
      end

      if cmds.size > 0 then
        results = formatted
        while cmd = cmds.shift do
          results = run(cmd, results).retval
        end
        value = results
        value_s = results.to_s
      else
        value = formatted
        if formatter then
          delim = Collins::Option(opts[:delimiter]).get_or_else("\n")
          value_s = value.join(delim)
        else
          value_s = format_values(value, size)
        end
      end
      if command_block then
        command_block.call(value)
      else
        # Handle commands like 'more' that should not return a value
        if not value.nil? and not value_s.empty? then
          render_output value_s, opts
        end
        value if should_return
      end
    end

    def get_format
      if opts.format? then
        opts[:format]
      elsif Pry.config.default_asset_format then
        Pry.config.default_asset_format
      else
        nil
      end
    end

    def format_values array, width = 4
      return "" if array.empty?
      t = Terminal::Table.new
      t.style = {:border_x => "", :border_y => "", :border_i => ""}
      line = []
      array.each do |o|
        line << o
        if line.size >= width then
          t << line
          line = []
        end
      end
      if not line.empty? then
        t << line
      end
      t.to_s
    end
  end
end