collins_shell 0.2.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.pryrc +1 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +18 -0
  4. data/Gemfile.lock +59 -0
  5. data/README.md +335 -0
  6. data/Rakefile +64 -0
  7. data/VERSION +1 -0
  8. data/bin/collins-shell +36 -0
  9. data/collins_shell.gemspec +95 -0
  10. data/lib/collins_shell.rb +3 -0
  11. data/lib/collins_shell/asset.rb +198 -0
  12. data/lib/collins_shell/cli.rb +185 -0
  13. data/lib/collins_shell/console.rb +129 -0
  14. data/lib/collins_shell/console/asset.rb +127 -0
  15. data/lib/collins_shell/console/cache.rb +17 -0
  16. data/lib/collins_shell/console/command_helpers.rb +131 -0
  17. data/lib/collins_shell/console/commands.rb +28 -0
  18. data/lib/collins_shell/console/commands/cat.rb +123 -0
  19. data/lib/collins_shell/console/commands/cd.rb +61 -0
  20. data/lib/collins_shell/console/commands/io.rb +26 -0
  21. data/lib/collins_shell/console/commands/iterators.rb +190 -0
  22. data/lib/collins_shell/console/commands/tail.rb +178 -0
  23. data/lib/collins_shell/console/commands/versions.rb +42 -0
  24. data/lib/collins_shell/console/filesystem.rb +121 -0
  25. data/lib/collins_shell/console/options_helpers.rb +8 -0
  26. data/lib/collins_shell/errors.rb +7 -0
  27. data/lib/collins_shell/ip_address.rb +144 -0
  28. data/lib/collins_shell/ipmi.rb +67 -0
  29. data/lib/collins_shell/monkeypatch.rb +60 -0
  30. data/lib/collins_shell/provision.rb +152 -0
  31. data/lib/collins_shell/state.rb +98 -0
  32. data/lib/collins_shell/tag.rb +41 -0
  33. data/lib/collins_shell/thor.rb +209 -0
  34. data/lib/collins_shell/util.rb +120 -0
  35. data/lib/collins_shell/util/asset_printer.rb +265 -0
  36. data/lib/collins_shell/util/asset_stache.rb +32 -0
  37. data/lib/collins_shell/util/log_printer.rb +187 -0
  38. data/lib/collins_shell/util/printer_util.rb +28 -0
  39. metadata +200 -0
@@ -0,0 +1,129 @@
1
+ require 'terminal-table'
2
+ require 'pry'
3
+
4
+ require 'collins_shell/console/commands'
5
+ require 'collins_shell/console/filesystem'
6
+
7
+ class Pry
8
+ # We kill the built in completion so we can tell users what things are available
9
+ module InputCompleter
10
+ class << self
11
+ def build_completion_proc(target, commands=[""])
12
+ proc do |input|
13
+ commands.map do |cmd|
14
+ cmd_s = cmd.to_s
15
+ if cmd_s.include?(":wtf") then
16
+ "wtf?"
17
+ else
18
+ cmd_s
19
+ end
20
+ end.select{|s| s.start_with?(input)}.uniq # commands.map
21
+ end # proc do
22
+ end # def build_completion_proc
23
+ end # class << self
24
+ end # module InputCompleter
25
+ end # class Pry
26
+
27
+ module CollinsShell; module Console
28
+
29
+ class << self
30
+ def run_pry_command command_string, options = {}
31
+ options = {
32
+ :show_output => true,
33
+ :output => Pry.output,
34
+ :commands => get_pry_commands
35
+ }.merge!(options)
36
+ output = options[:show_output] ? options[:output] : StringIO.new
37
+ pry = Pry.new(
38
+ :output => output, :input => StringIO.new(command_string),
39
+ :commands => options[:commands], :prompt => proc{""}, :hooks => Pry::Hooks.new
40
+ )
41
+ if options[:binding_stack] then
42
+ pry.binding_stack = options[:binding_stack]
43
+ end
44
+ pry.rep(options[:context])
45
+ end
46
+ def launch(options)
47
+ self.options = options
48
+ Pry.config.commands = get_pry_commands
49
+ Pry.config.pager = true
50
+ Pry.custom_completions = get_pry_custom_completions
51
+ Pry.config.exception_handler = get_pry_exception_handler
52
+ target = CollinsShell::Console::Filesystem.new options
53
+ setup_pry_hooks
54
+ Pry.start(target, :prompt => get_pry_prompt)
55
+ end
56
+ def options=(options)
57
+ @options = options
58
+ end
59
+ def options
60
+ @options
61
+ end
62
+ private
63
+ def setup_pry_hooks
64
+ before_message = [
65
+ 'Welcome to the collins console. A few notes:',
66
+ ' - collins-shell interacts with real servers (BE CAREFUL).',
67
+ ' - collins-shell operates in contexts, which the prompt tells you about.',
68
+ ' - collins-shell can be customized to your tastes. Read the docs.',
69
+ ' - Type help at any time for help'
70
+ ].join("\n")
71
+ Pry.config.hooks.add_hook(:before_session, :session_start) do |out, *|
72
+ out.puts before_message
73
+ end
74
+ Pry.config.hooks.add_hook(:after_session, :session_end) do |out, *|
75
+ out.puts "Goodbye!"
76
+ end
77
+ end
78
+ def get_pry_prompt
79
+ get_prompt = proc { |o, waiting|
80
+ sym = waiting ? "*" : ">"
81
+ if o.is_a?(CollinsShell::Console::Filesystem) then
82
+ ext = ""
83
+ if o.asset? then
84
+ ext = "*"
85
+ end
86
+ "collins #{o.path}#{ext} #{sym} "
87
+ else
88
+ "collins-#{o.class} #{sym} "
89
+ end
90
+ }
91
+ [
92
+ proc { |o, *| get_prompt.call(o, false) },
93
+ proc { |o, *| get_prompt.call(o, true) }
94
+ ]
95
+ end
96
+ def get_pry_custom_completions
97
+ proc do
98
+ last = binding_stack.last
99
+ last = last.eval('self') unless last.nil?
100
+ if last.is_a?(CollinsShell::Console::Filesystem) then
101
+ (last.available_commands + commands.commands.keys).flatten
102
+ else
103
+ commands.commands.keys
104
+ end
105
+ end
106
+ end
107
+ def get_pry_exception_handler
108
+ proc do |output, exception, _pry_|
109
+ if exception.is_a?(Interrupt) then
110
+ output.puts ""
111
+ else
112
+ cli = CollinsShell::Cli.new [], {}
113
+ cli.print_error exception, "command failed"
114
+ output.puts ""
115
+ bold = Pry::Helpers::Text.bold("type 'bt' or 'wtf?!?' for more context")
116
+ output.puts bold
117
+ end
118
+ end
119
+ end
120
+ def get_pry_commands
121
+ Pry::CommandSet.new do
122
+ import_from Pry::Commands, "help", "history", "hist", "wtf?", "show-doc", "show-source"
123
+ alias_command "bt", "wtf?"
124
+ import CollinsShell::Console::Commands::Default
125
+ end
126
+ end
127
+ end
128
+
129
+ end; end
@@ -0,0 +1,127 @@
1
+ module CollinsShell; module Console
2
+ class Asset
3
+ include CollinsShell::Console::CommandHelpers
4
+
5
+ attr_reader :tag
6
+ def initialize asset
7
+ @tag = asset
8
+ @asset_client = collins_client.with_asset(@tag)
9
+ end
10
+ def power! action = nil
11
+ Collins::Option(action).map do |action|
12
+ action = Collins::Power.normalize_action(action)
13
+ verifying_response("power #{action}") {
14
+ @asset_client.power!(action)
15
+ }
16
+ end.get_or_else {
17
+ cput("A power action argument is required. power <action>")
18
+ }
19
+ end
20
+ def reboot! how = "rebootSoft"
21
+ Collins::Option(how).map do |how|
22
+ action = Collins::Power.normalize_action(how)
23
+ verifying_response("reboot") {
24
+ @asset_client.power!(action)
25
+ }
26
+ end.get_or_else {
27
+ cput("A reboot argument is required. reboot <action>")
28
+ }
29
+ end
30
+ def stat
31
+ s = <<-STAT
32
+ Asset: #{underlying.tag}
33
+ Status: #{underlying.status}
34
+ Type: #{underlying.type}
35
+ Hostname: #{Collins::Option(underlying.hostname).get_or_else("(none)")}
36
+ Created: #{Collins::Option(underlying.created).get_or_else("(none)")}
37
+ Updated: #{Collins::Option(underlying.updated).get_or_else("(none)")}
38
+ Deleted: #{Collins::Option(underlying.deleted).get_or_else("(none)")}
39
+ STAT
40
+ cput(s)
41
+ end
42
+ def set_status! status = nil, reason = nil
43
+ msg = "set_status request a %s. set_status <status>, <reason>"
44
+ (raise sprintf(msg, "status")) if status.nil?
45
+ (raise sprintf(msg, "reason")) if reason.nil?
46
+ verifying_response("set the status to '#{status}' on") {
47
+ @asset_client.set_status!(status, reason)
48
+ }
49
+ end
50
+ def log! msg, level = nil
51
+ (raise "log requires a message. log <message>") if (msg.nil? || msg.to_s.empty?)
52
+ @asset_client.log!(msg, level)
53
+ end
54
+ def logs options = {}
55
+ @asset_client.logs(options)
56
+ end
57
+ def set! key = nil, value = nil, group_id = nil
58
+ msg = "set requires a %s. set <key>, <value>"
59
+ (raise sprintf(msg, "key")) if key.nil?
60
+ (raise sprintf(msg, "value")) if value.nil?
61
+ case value
62
+ when String, Symbol, Fixnum, TrueClass, FalseClass then
63
+ value = value.to_s
64
+ else
65
+ raise "value can't be a #{value.class}"
66
+ end
67
+ verifying_response("set the key '#{key}' to '#{value}' on") {
68
+ @asset_client.set_attribute!(key.to_s, value, group_id)
69
+ }
70
+ end
71
+ def rm! key = nil, group_id = nil
72
+ (raise "rm requires a key. rm <key>") if key.nil?
73
+ verifying_response("delete the key '#{key}' on") {
74
+ @asset_client.delete_attribute!(key, group_id)
75
+ }
76
+ end
77
+ def key? key = nil
78
+ (raise "key? requires a key. key? <key>") if key.nil?
79
+ @asset_client.get.send("#{key}?".to_sym)
80
+ end
81
+ def key key = nil
82
+ (raise "key requires a key. key <key>") if key.nil?
83
+ @asset_client.get.send(key.to_sym)
84
+ end
85
+ def on?
86
+ power? == "on"
87
+ end
88
+ def power?
89
+ @asset_client.power_status
90
+ end
91
+ def respond_to? meth, include_private = false
92
+ if meth.to_sym == :asset then
93
+ true
94
+ elsif @asset_client.respond_to?(meth) then
95
+ true
96
+ else
97
+ super
98
+ end
99
+ end
100
+ protected
101
+ def method_missing meth, *args, &block
102
+ if meth.to_sym == :asset then
103
+ underlying
104
+ elsif @asset_client.respond_to?(meth) then
105
+ @asset_client.send(meth, *args, &block)
106
+ else
107
+ super
108
+ end
109
+ end
110
+ def underlying
111
+ @underlying ||= get_asset(@tag)
112
+ end
113
+ def verifying_response message, &block
114
+ message = "You are about to #{message} asset #{tag}. Are you sure? "
115
+ if shell_handle.require_yes(message, :red, false) then
116
+ block.call
117
+ else
118
+ cput("Aborted operation")
119
+ end
120
+ end
121
+ def cput message
122
+ Pry.output.puts(message)
123
+ end
124
+
125
+ end # class CollinsShell::Console::Asset
126
+
127
+ end; end
@@ -0,0 +1,17 @@
1
+ # This is a very basic shared cache class
2
+ module CollinsShell; module Console; module Cache
3
+ @@_cache = {}
4
+
5
+ def clear_cache
6
+ @@_cache = {}
7
+ end
8
+
9
+ def cache_get_or_else key, &block
10
+ if @@_cache[key].nil? then
11
+ @@_cache[key] = block.call
12
+ else
13
+ @@_cache[key]
14
+ end
15
+ end
16
+
17
+ end; end; end
@@ -0,0 +1,131 @@
1
+ require 'collins_shell/console/cache'
2
+
3
+ module CollinsShell; module Console; module CommandHelpers
4
+
5
+ include CollinsShell::Console::Cache
6
+
7
+ # @return [Boolean] true if asset associated with specified tag exists
8
+ def asset_exists? tag
9
+ m = "asset_exists?(#{tag})"
10
+ cache_get_or_else(m) {
11
+ call_collins(m) {|c| c.exists?(tag)}
12
+ }
13
+ end
14
+
15
+ # @return [Collins::Asset] associated with the specified tag
16
+ def get_asset tag
17
+ m = "get_asset(#{tag})"
18
+ cache_get_or_else(m) {
19
+ call_collins(m) {|c| c.get(tag)}
20
+ }
21
+ end
22
+
23
+ # @return [Object] result of calling collins using the specifed block
24
+ def call_collins operation, &block
25
+ shell_handle.call_collins(collins_client, operation, &block)
26
+ end
27
+ # @return [Collins::Client] A Collins::Client instance
28
+ def collins_client
29
+ shell_handle.get_collins_client cli_options
30
+ end
31
+
32
+ # @return [Hash] CLI specified options
33
+ def cli_options
34
+ CollinsShell::Console.options
35
+ end
36
+
37
+ # @return [Array<Collins::Asset>,NilClass]
38
+ def find_one_asset tags_values, details = true
39
+ find_assets(tags_values, details).first
40
+ end
41
+
42
+ # Given an array of keys and values, return matching assets
43
+ # @param [Array<String>] tags_values an array where even elements are keys for a search and odd elements are values
44
+ # @param [Boolean] details If true, return full assets, otherwise return a sorted array of tags
45
+ # @return [Array<Collins::Asset>,Array<String>]
46
+ def find_assets tags_values, details
47
+ q = tags_values.each_slice(2).inject({}) {|h,o| h.update(o[0].to_s.to_sym => o[1])}
48
+ q.update(:details => details)
49
+ selector = shell_handle.get_selector(q, nil, 5000)
50
+ call_collins("find(#{selector})") do |c|
51
+ if details then
52
+ c.find(selector)
53
+ else
54
+ c.find(selector).map{|a| a.tag}.sort
55
+ end
56
+ end
57
+ end
58
+
59
+ # @return [Array<String>,Array<Collins::Asset>] an array of values associated with the specified tag, or the asset associated with that tag if resolve asset is true.
60
+ def get_tag_values tag, resolve_asset = true
61
+ m = "get_tag_values(#{tag}, #{resolve_asset.to_s})"
62
+ cache_get_or_else(m) {
63
+ call_collins(m) {|c| c.get_tag_values(tag)}.sort
64
+ }
65
+ rescue Exception => e
66
+ if e.is_a?(Collins::RequestError) then
67
+ if e.code.to_i == 404 then
68
+ [get_asset(tag)] if resolve_asset
69
+ else
70
+ raise e
71
+ end
72
+ else
73
+ [get_asset(tag)] if resolve_asset
74
+ end
75
+ end
76
+
77
+ def virtual_tags
78
+ Collins::Asset::Find.to_a
79
+ end
80
+
81
+ # return known tags including virtual ones (ones not stored, but that can be used as
82
+ # query parameters
83
+ def get_all_tags include_virtual = true
84
+ begin
85
+ cache_get_or_else("get_all_tags(#{include_virtual.to_s})") {
86
+ tags = call_collins("get_all_tags"){|c| c.get_all_tags}.map{|t|t.name}
87
+ if include_virtual then
88
+ [tags + Collins::Asset::Find.to_a].flatten.sort
89
+ else
90
+ tags.sort
91
+ end
92
+ }
93
+ rescue Exception => e
94
+ puts "Error retrieving tags: #{e}"
95
+ []
96
+ end
97
+ end
98
+
99
+ # Given a possible string tag and the context stack, find the asset tag
100
+ # @param [NilClass,String] tag a possible tag for use
101
+ # @param [Array<Binding>] stack the context stack
102
+ # @return [String,NilClass] Either the given tag, the tag on the top of the context stack, or nil if neither is available
103
+ def resolve_asset_tag tag, stack
104
+ Collins::Option(tag).map{|s| s.strip}.filter_not{|s| s.empty?}.get_or_else {
105
+ tag_from_stack stack
106
+ }
107
+ end
108
+
109
+ # @return [CollinsShell::Util] a handle on the CLI interface
110
+ def shell_handle
111
+ CollinsShell::Asset.new([], cli_options)
112
+ end
113
+
114
+ # @param [Array<Binding>] stack the context stack
115
+ # @return [NilClass,String] the asset tag at the top of the stack, if it exists
116
+ def tag_from_stack stack
117
+ node = stack.last
118
+ node = node.eval('self') unless node.nil?
119
+ if node and node.asset? then
120
+ node.console_asset.tag
121
+ else
122
+ nil
123
+ end
124
+ end
125
+
126
+ # @return [Boolean] true if in asset context
127
+ def asset_context? stack
128
+ !tag_from_stack(stack).nil?
129
+ end
130
+
131
+ end; end; end
@@ -0,0 +1,28 @@
1
+ require 'pry'
2
+
3
+ require 'collins_shell/console/options_helpers'
4
+ require 'collins_shell/console/command_helpers'
5
+ require 'collins_shell/console/commands/cd'
6
+ require 'collins_shell/console/commands/cat'
7
+ require 'collins_shell/console/commands/tail'
8
+ require 'collins_shell/console/commands/io'
9
+ require 'collins_shell/console/commands/iterators'
10
+ require 'collins_shell/console/commands/versions'
11
+
12
+ # TODO List
13
+ # * Confirmation for destructive options
14
+ # * Docs
15
+ # * Remote
16
+ # * Testing
17
+ # * Ssh
18
+ # * Ping
19
+ module CollinsShell; module Console; module Commands
20
+ Default = Pry::CommandSet.new do
21
+ import CollinsShell::Console::Commands::Cd
22
+ import CollinsShell::Console::Commands::Cat
23
+ import CollinsShell::Console::Commands::Tail
24
+ import CollinsShell::Console::Commands::Io
25
+ import CollinsShell::Console::Commands::Iterators
26
+ import CollinsShell::Console::Commands::Versions
27
+ end
28
+ end; end; end
@@ -0,0 +1,123 @@
1
+ require 'pry'
2
+
3
+ module CollinsShell; module Console; module Commands
4
+ Cat = Pry::CommandSet.new do
5
+ create_command "cat", "Output data associated with the specified asset" do
6
+
7
+ include CollinsShell::Console::CommandHelpers
8
+
9
+ group "I/O"
10
+
11
+ def options(opt)
12
+ opt.banner <<-BANNER
13
+ Usage: cat [-l|--logs] [-b|--brief] [--help]
14
+
15
+ Cat the specified asset or log.
16
+
17
+ If you are in an asset context, cat does not require the asset tag. In an asset context you can do:
18
+ cat # no-arg displays current asset
19
+ cat -b # short-display
20
+ cat -l # table formatted logs
21
+ cat /var/log/SEVERITY # display logs of a specified type
22
+ cat /var/log/messages # all logs
23
+ If you are not in an asset context, cat requires the tag of the asset you want to display.
24
+ cat # display this help
25
+ cat asset-tag # display asset
26
+ cat -b asset-tag # short-display
27
+ cat -l asset-tag # table formatted logs
28
+ cat /var/log/assets/asset-tag # display logs for asset
29
+ cat /var/log/hosts/hostname # display logs for host with name
30
+ BANNER
31
+ opt.on :b, "brief", "Brief output, not detailed"
32
+ opt.on :l, "logs", "Display logs as well"
33
+ end
34
+
35
+ def process
36
+ stack = _pry_.binding_stack
37
+ if args.first.to_s.start_with?('/var/log/') then
38
+ display_logs args.first, stack
39
+ else
40
+ tag = resolve_asset_tag args.first, stack
41
+ if tag.nil? then
42
+ run "help", "cat"
43
+ return
44
+ end
45
+ display_asset tag
46
+ end
47
+ end
48
+
49
+ # Given a logfile specification, parse it and render it
50
+ def display_logs logfile, stack
51
+ rejects = ['var', 'log']
52
+ paths = logfile.split('/').reject{|s| s.empty? || rejects.include?(s)}
53
+ if paths.size == 2 then
54
+ display_sub_logs paths[0], paths[1]
55
+ elsif paths.size == 1 then
56
+ display_asset_logs tag_from_stack(stack), paths[0]
57
+ else
58
+ run "help", "cat"
59
+ end
60
+ end
61
+
62
+ # Render logs of the type `/var/log/assets/TAG` or `/var/log/hosts/HOSTNAME`
63
+ def display_sub_logs arg1, arg2
64
+ if arg1 == 'assets' then
65
+ display_asset_logs arg2, 'messages'
66
+ elsif arg1 == 'hosts' then
67
+ asset = find_one_asset(['HOSTNAME', arg2])
68
+ display_asset_logs asset, 'messages'
69
+ else
70
+ output.puts "#{text.bold('Invalid log type:')} Only 'assets' or 'hosts' are valid, found '#{arg1}'"
71
+ output.puts
72
+ run "help", "cat"
73
+ end
74
+ end
75
+
76
+ # Render logs for an asset according to type, where type is 'messages' (all) or a severity
77
+ def display_asset_logs asset_tag, type
78
+ begin
79
+ asset = Collins::Util.get_asset_or_tag(asset_tag).tag
80
+ rescue => e
81
+ output.puts "#{text.bold('Invalid asset:')} '#{asset_tag}' not valid - #{e}"
82
+ return
83
+ end
84
+ severity = Collins::Api::Logging::Severity
85
+ severity_level = severity.value_of type
86
+ if type == 'messages' then
87
+ get_and_print_logs asset_tag, "ASC"
88
+ elsif not severity_level.nil? then
89
+ get_and_print_logs asset_tag, "ASC", severity_level
90
+ else
91
+ message = "Only '/var/log/messages' or '/var/log/SEVERITY' are valid here"
92
+ sevs = severity.to_a.join(', ')
93
+ output.puts "#{text.bold('Invalid path specified:')}: #{message}"
94
+ output.puts "Valid severity levels are: #{sevs}"
95
+ end
96
+ end
97
+
98
+ def display_asset tag
99
+ if asset_exists? tag then
100
+ asset = get_asset tag
101
+ show_logs = opts.logs?
102
+ show_details = !opts.brief?
103
+ show_color = Pry.color
104
+ logs = []
105
+ logs = call_collins("logs(#{tag})") {|c| c.logs(tag, :size => 5000)} if show_logs
106
+ printer = CollinsShell::AssetPrinter.new asset, shell_handle, :logs => logs, :detailed => show_details, :color => show_color
107
+ render_output printer.to_s
108
+ else
109
+ output.puts "#{text.bold('No such asset:')} #{tag}"
110
+ end
111
+ end
112
+
113
+ def get_and_print_logs asset_tag, sort, filter = nil, size = 5000
114
+ logs = call_collins "logs(#{asset_tag})" do |client|
115
+ client.logs asset_tag, :sort => sort, :size => size, :filter => filter
116
+ end
117
+ printer = CollinsShell::LogPrinter.new asset_tag, logs
118
+ output.puts printer.to_s
119
+ end
120
+
121
+ end # create_command
122
+ end # CommandSet
123
+ end; end; end