collins_shell 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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