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,120 @@
1
+ require 'collins_shell/monkeypatch'
2
+ require 'collins_shell/util/asset_stache'
3
+
4
+ module CollinsShell
5
+ module Util
6
+
7
+ SIZABLE_ATTRIBUTES = [:memory_size_total, :disk_storage_total]
8
+
9
+ def call_collins client, operation, &block
10
+ begin
11
+ block.call(client)
12
+ rescue SystemExit => e
13
+ end
14
+ end
15
+
16
+ def get_selector selector, tags, size, remote = false
17
+ asset_params = ::Collins::Asset::Find.to_a.inject({}) {|res,el| res.update(el.to_s.upcase => el)}
18
+ selector = symbolize_hash(selector).inject({}) do |result, (k,v)|
19
+ upcase_key = k.to_s.upcase
20
+ if asset_params.key?(upcase_key) then # normalized reserved params
21
+ corrected_key = asset_params[upcase_key]
22
+ if ::Collins::Asset::Find::DATE_PARAMS.include?(corrected_key) then
23
+ result[corrected_key.to_sym] = ::Collins::Asset.format_date_string(v)
24
+ else
25
+ result[corrected_key.to_sym] = v
26
+ end
27
+ elsif upcase_key == k.to_s then # respect case
28
+ result[k] = v
29
+ else # otherwise downcase
30
+ result[k.downcase] = v
31
+ end
32
+ result
33
+ end
34
+ if not selector.include?(:operation) then
35
+ selector.update(:operation => 'and')
36
+ end
37
+ if not selector.include?(:remoteLookup) and remote then
38
+ selector.update(:remoteLookup => remote.to_s)
39
+ end
40
+ if not selector.include?(:size) and size then
41
+ selector.update(:size => size)
42
+ end
43
+ selector.update(:details => true) if is_array?(tags)
44
+ CollinsShell::Util::SIZABLE_ATTRIBUTES.each do |attrib|
45
+ attrib_value = selector[attrib]
46
+ if attrib_value && attrib_value.to_s.is_disk_size? then
47
+ selector.update(attrib => attrib_value.to_s.to_bytes)
48
+ end
49
+ end
50
+ selector
51
+ end
52
+
53
+ def asset_exec asset, execs, confirm = true
54
+ return unless execs
55
+ mustache = CollinsShell::Util::AssetStache.new asset
56
+ rendered = mustache.render "/bin/bash -c '#{execs}'"
57
+ say_status("exec", rendered, :red)
58
+ require_yes("Running on #{asset.tag}. ARE YOU SURE?", :red) if confirm
59
+ system(rendered)
60
+ end
61
+
62
+ def asset_get tag, options
63
+ call_collins get_collins_client, "get asset" do |client|
64
+ as_asset = Collins::Asset.new(tag)
65
+ as_asset.location = options.remote
66
+ asset = client.get as_asset
67
+ end
68
+ end
69
+
70
+ def print_find_results assets, tags, options = {}
71
+ tags = [:tag,:status,:type,:created,:updated,:location] if not is_array?(tags)
72
+ if options[:url] then
73
+ tags << :url
74
+ end
75
+ if options[:header] then
76
+ puts(tags.join(','))
77
+ end
78
+ [assets].flatten.each do |asset|
79
+ puts format_asset_tags(asset, tags)
80
+ end
81
+ end
82
+
83
+ def format_asset_tags asset, tags
84
+ tags.map do |tag|
85
+ if tag.to_s =~ /^ipmi_(.*)/ then
86
+ asset.ipmi.send($1.to_sym)
87
+ elsif tag.to_s.split('.').length == 2 then
88
+ o, m = tag.to_s.split('.')
89
+ result = asset.send(o.to_sym)
90
+ if result.nil? then
91
+ "nil"
92
+ else
93
+ format_asset_value result.send(m.to_sym)
94
+ end
95
+ elsif tag == :url then
96
+ config = get_collins_config
97
+ " #{config[:host]}/asset/#{asset.tag}"
98
+ else
99
+ result = asset.send(tag.to_sym)
100
+ format_asset_value result
101
+ end
102
+ end.join(',')
103
+ end
104
+
105
+ def format_asset_value value
106
+ if is_array? value then
107
+ value.map {|v| format_asset_value v}
108
+ elsif value.is_a?(Collins::Address) then
109
+ [:address,:gateway,:netmask].map {|s| value.send(s)}.join('|')
110
+ else
111
+ value.to_s
112
+ end
113
+ end
114
+
115
+ def is_array? tags
116
+ tags && tags.is_a?(Array) && tags.length > 0
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,265 @@
1
+ require 'terminal-table'
2
+ require 'collins_shell/util/printer_util'
3
+
4
+ module CollinsShell
5
+
6
+ class AssetPrinter
7
+
8
+ include CollinsShell::PrinterUtil
9
+
10
+ attr_accessor :asset, :detailed, :table, :col_size, :thor, :separator, :logs, :color
11
+
12
+ def initialize asset, thor, options = {}
13
+ @asset = asset
14
+ @table = Terminal::Table.new
15
+ @thor = thor
16
+ @separator = options[:separator]
17
+ @detailed = options.fetch(:detailed, true)
18
+ @col_size = 6
19
+ @color = options.fetch(:color, true)
20
+ @logs = options[:logs]
21
+ end
22
+
23
+ def render
24
+ table.title = header_text("Asset #{asset.tag}")
25
+ collect_asset_basics
26
+ collect_disk_data if detailed
27
+ collect_ip_data if detailed
28
+ collect_ipmi_data if detailed
29
+ collect_nic_data if detailed
30
+ collect_mem_data if detailed
31
+ collect_power_data if detailed
32
+ collect_xtra_data
33
+ collect_log_data
34
+ table_s = table.render
35
+ if separator then
36
+ width = (0...@col_size).inject(0) do |result, idx|
37
+ result + table.column_width(idx)
38
+ end
39
+ sep_s = separator * (1.13 * width.to_f).to_i
40
+ table_s + "\n#{sep_s}\n"
41
+ else
42
+ table_s
43
+ end
44
+ end
45
+ alias :to_s :render
46
+
47
+ def collect_asset_basics
48
+ header = [:tag,:status,:type,:created,:updated,:location]
49
+ table << get_row(header, true, header.length)
50
+ table << :separator
51
+ table << get_row(header.map{|s| asset.send(s)}, false, header.length)
52
+ if asset.state && !asset.state.empty? then
53
+ state = asset.state
54
+ state_headers = [
55
+ title_text("State Name"),
56
+ title_text("State Label"),
57
+ {:value => title_text("State Description"), :colspan => 4}
58
+ ]
59
+ table << :separator
60
+ table << state_headers
61
+ table << :separator
62
+ table << [state.name, state.label, {:value => state.description, :colspan => 4}]
63
+ end
64
+ @col_size = table.number_of_columns
65
+ end
66
+
67
+ def collect_disk_data
68
+ disk_header = ["SIZE_HUMAN", "SIZE", "TYPE", "DESCRIPTION"]
69
+ row_title "Disks"
70
+ table << get_row(disk_header, true)
71
+ table << :separator
72
+ asset.disks.each do |disk|
73
+ table << get_row(disk_header.map{|s| disk[s]})
74
+ end
75
+ end
76
+
77
+ def collect_log_data
78
+ return if (not logs or logs.empty?)
79
+ row_title "Log Entries"
80
+ headers = [
81
+ title_text("Created"),
82
+ title_text("Severity"),
83
+ title_text("Source"),
84
+ title_text("Format"),
85
+ {:value => title_text("Message"), :colspan => 2}
86
+ ]
87
+ table << headers
88
+ table << :separator
89
+ have_multi_row = false
90
+ logs_formatted = logs.map do |log|
91
+ rewritten_msg = log.MESSAGE.strip
92
+ message = wrap_text(rewritten_msg).strip
93
+ if message != rewritten_msg then
94
+ have_multi_row = true
95
+ end
96
+ [
97
+ format_datetime(log.CREATED), log.TYPE, log.SOURCE, log.FORMAT,
98
+ {:value => message, :colspan => 2}
99
+ ]
100
+ end
101
+ last_log_idx = logs.length - 1
102
+ logs_formatted.each_with_index do |row, index|
103
+ table << row
104
+ # Don't print a separator if we don't have a multi-row message
105
+ if index != last_log_idx && have_multi_row then
106
+ table << :separator
107
+ end
108
+ end
109
+ end
110
+
111
+ def collect_ip_data
112
+ address_header = [:address,:gateway,:netmask,:pool,:is_private?,:is_public?]
113
+ row_title "IP Addresses"
114
+ table << get_row(address_header, true)
115
+ table << :separator
116
+ asset.addresses.each{|a| table << get_row(address_header.map{|s| a.send(s)})}
117
+ end
118
+
119
+ def collect_ipmi_data
120
+ ipmi_header = [:address,:gateway,:netmask,:username,:password]
121
+ row_title "IPMI Information"
122
+ table << get_row(ipmi_header, true)
123
+ table << :separator
124
+ table << get_row(ipmi_header.map {|i| asset.ipmi.send(i)})
125
+ end
126
+
127
+ def collect_nic_data
128
+ nic_header = ["SPEED_HUMAN", "MAC_ADDRESS", "DESCRIPTION"]
129
+ row_title "Network Interfaces"
130
+ table << get_row(nic_header, true)
131
+ table << :separator
132
+ asset.nics.each {|nic| table << get_row(nic_header.map{|s| nic[s]})}
133
+ end
134
+
135
+ def collect_mem_data
136
+ memory_header = ["BANK","SIZE","SIZE_HUMAN","DESCRIPTION"]
137
+ row_title "Physical Memory"
138
+ table << get_row(memory_header, true)
139
+ table << :separator
140
+ memory_total = 0
141
+ asset.memory.each do |mem|
142
+ memory_total += mem["SIZE"].to_i
143
+ if mem["SIZE"].to_i != 0 then
144
+ table << get_row(memory_header.map{|s| mem[s]})
145
+ end
146
+ end
147
+ table << :separator
148
+ table << [title_text("TOTAL SIZE (Bytes)"), {:value => memory_total.to_s, :colspan => 5}]
149
+ table << [title_text("TOTAL SIZE (Human)"), {:value => memory_total.to_human_size, :colspan => 5}]
150
+ end
151
+
152
+ def collect_power_data
153
+ power_header = [:type,:key,:value,:label]
154
+ row_title "Power Information"
155
+ table << get_row(power_header, true)
156
+ table << :separator
157
+ asset.power.each do |power|
158
+ power.units.each do |unit|
159
+ table << get_row(power_header.map {|key| unit.send(key)})
160
+ end
161
+ end
162
+ end
163
+
164
+ def format_text key, value
165
+ key_s = key.to_s.downcase
166
+ if key_s.start_with?("json_") or key_s.end_with?("_json") then
167
+ begin
168
+ JSON.parse(value, :symbolize_names => true).to_s
169
+ rescue Exception => e
170
+ value
171
+ end
172
+ else
173
+ value
174
+ end
175
+ end
176
+
177
+ def collect_xtra_data
178
+ return unless (asset.extras and asset.extras["ATTRIBS"] && asset.extras["ATTRIBS"]["0"])
179
+ row_title "Extra Attributes"
180
+ attribs = []
181
+ attribs_unsorted = asset.extras["ATTRIBS"]["0"]
182
+ attribs_sorted = attribs_unsorted.keys.sort.inject({}) do |result,key|
183
+ result.update(key => attribs_unsorted[key])
184
+ end
185
+ attribs_sorted.each do |key,value|
186
+ attribs << title_text(key)
187
+ attribs << format_text(key, value)
188
+ if attribs.length == 6 then
189
+ table << attribs
190
+ attribs = []
191
+ end
192
+ if CollinsShell::Util::SIZABLE_ATTRIBUTES.include?(key.downcase.to_sym) then
193
+ attribs << title_text("#{key} (Human)")
194
+ attribs << value.to_f.to_human_size
195
+ end
196
+ if attribs.length == 6 then
197
+ table << attribs
198
+ attribs = []
199
+ end
200
+ end
201
+ if attribs.length != 0 then
202
+ until attribs.length == 6
203
+ attribs << " "
204
+ end
205
+ table << attribs
206
+ end
207
+ end
208
+
209
+ def header_text txt
210
+ if @color then
211
+ thor.set_color(txt, :bold, :magenta)
212
+ else
213
+ txt
214
+ end
215
+ end
216
+ def title_text txt
217
+ if @color then
218
+ thor.set_color(txt, :bold, :white)
219
+ else
220
+ txt
221
+ end
222
+ end
223
+ def regular_text txt
224
+ txt
225
+ end
226
+
227
+ def get_row row_data, header = false, cols = 0
228
+ num_cols = @col_size
229
+ row_length = row_data.length
230
+ last_index = row_length - 1
231
+ average_size = (num_cols.to_f / row_length.to_f)
232
+ est_size = (num_cols / row_length)
233
+ est_is_actual = (average_size == est_size)
234
+ used_cols = 0
235
+ row = []
236
+ row_data.each_with_index do |col,index|
237
+ if est_is_actual then
238
+ size_per_col = est_size
239
+ elsif index == 0 then
240
+ size_per_col = 1
241
+ elsif average_size > (used_cols.to_f/index.to_f) then
242
+ size_per_col = 2
243
+ else
244
+ size_per_col = 1
245
+ end
246
+ if col.is_a?(DateTime) then
247
+ col = format_datetime(col)
248
+ end
249
+ used_cols += size_per_col
250
+ alignment = if size_per_col > 1 then :center else :left end
251
+ value = if header then title_text(col) else regular_text(col) end
252
+ row << {:value => value, :alignment => alignment, :colspan => size_per_col}
253
+ end
254
+ row
255
+ end
256
+
257
+ def row_title txt
258
+ table << :separator
259
+ table << [{:value => header_text(txt), :alignment => :center, :colspan => @col_size}]
260
+ table << :separator
261
+ end
262
+
263
+ end
264
+
265
+ end
@@ -0,0 +1,32 @@
1
+ require 'mustache'
2
+
3
+ module CollinsShell; module Util
4
+
5
+ class AssetStache < Mustache
6
+ attr_accessor :asset
7
+
8
+ def initialize asset
9
+ @asset = asset
10
+ end
11
+
12
+ def respond_to? meth
13
+ result = asset.send(meth.to_sym)
14
+ if result.nil? then
15
+ super
16
+ else
17
+ result
18
+ end
19
+ end
20
+
21
+ def method_missing meth, *args, &block
22
+ result = asset.send(meth.to_sym)
23
+ if result.nil? then
24
+ super
25
+ else
26
+ result
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ end; end
@@ -0,0 +1,187 @@
1
+ require 'terminal-table'
2
+ require 'collins_shell/util/printer_util'
3
+
4
+ module CollinsShell
5
+
6
+ class LogPrinter
7
+
8
+ include CollinsShell::PrinterUtil
9
+
10
+ attr_reader :asset_tag, :logs, :options, :table, :table_width
11
+
12
+ def initialize asset_or_tag, logs
13
+ @asset_tag = Collins::Util.get_asset_or_tag(asset_or_tag).tag
14
+ if logs.is_a?(Hash) then
15
+ @logs = logs.delete(:logs) || []
16
+ @options = logs
17
+ else
18
+ @logs = logs
19
+ @options = {}
20
+ end
21
+ @table_width = 0
22
+ @table = Terminal::Table.new(:title => "Logs for #{asset_tag}")
23
+ end
24
+
25
+ def render _logs = []
26
+ setup_table
27
+ setup_header
28
+ need_separator, formatted_logs = format_logs _logs
29
+ add_logs_to_table need_separator, formatted_logs
30
+ render!
31
+ end
32
+ alias :to_s :render
33
+
34
+ protected
35
+ # Add the specified logs to the table instance, using a separator if required
36
+ def add_logs_to_table need_separator, logs
37
+ last_log_idx = logs.length - 1
38
+ logs.each_with_index do |row, index|
39
+ table << row
40
+ # Don't print a separator if we don't have a multi-row message
41
+ if index != last_log_idx && need_separator then
42
+ table << :separator
43
+ end
44
+ end
45
+ end
46
+
47
+ # Format logs appropriately for table layout
48
+ def format_logs _logs
49
+ have_multi_row = streaming? # either false (default), or true based on streaming
50
+ logs_to_render = if not _logs.empty? then _logs else logs end
51
+ logs_formatted = logs_to_render.map do |log|
52
+ entry = format_log_entry log
53
+ have_multi_row = true unless entry.find_index{|e| e.to_s.include?("\n")}.nil?
54
+ entry
55
+ end
56
+ [have_multi_row, logs_formatted]
57
+ end
58
+
59
+ # We force column values to pad so terminal-table doesn't try and be smart
60
+ def format_column value, width, alignment
61
+ value_s = value.to_s.strip
62
+ if value_s.size > width then
63
+ alignment = :left # force to left is we wrap
64
+ value_s = wrap_text(value_s, width).strip
65
+ end
66
+ case alignment
67
+ when :left
68
+ value_s.ljust(width)
69
+ when :right
70
+ value_s.rjust(width)
71
+ else
72
+ value_s.center(width)
73
+ end
74
+ end
75
+
76
+ def format_log_entry log
77
+ res = []
78
+ if streaming? then
79
+ res = [
80
+ format_column(format_datetime(log.CREATED), created_col_width, :center),
81
+ format_column(log.TYPE, severity_col_width, :center),
82
+ format_column(log.SOURCE, source_col_width, :center),
83
+ format_column(log.FORMAT, format_col_width, :center),
84
+ format_column(log.MESSAGE, message_col_width, :left)
85
+ ]
86
+ res.unshift(format_column(log.ASSET_TAG, tag_col_width, :left)) if requires_tag?
87
+ else
88
+ res = [
89
+ format_datetime(log.CREATED), log.TYPE, log.SOURCE, log.FORMAT, wrap_text(log.MESSAGE.strip).strip
90
+ ]
91
+ res.unshift(log.ASSET_TAG) if requires_tag?
92
+ end
93
+ res
94
+ end
95
+
96
+ # Grab the rendered table and reset the table instance
97
+ def render!
98
+ if displayed? then
99
+ buffer = table.rows.map do |row|
100
+ row.render + "\n" + Terminal::Table::Separator.new(table).render
101
+ end.join("\n")
102
+ else
103
+ buffer = table.render
104
+ end
105
+ set_displayed
106
+ @table = Terminal::Table.new
107
+ buffer
108
+ end
109
+
110
+ # If needed setup table headings
111
+ def setup_header
112
+ if options.fetch(:header, true) and not displayed? then
113
+ if streaming? then
114
+ headings = [
115
+ format_column("Created", created_col_width, :center),
116
+ format_column("Severity", severity_col_width, :center),
117
+ format_column("Source", source_col_width, :center),
118
+ format_column("Format", format_col_width, :center),
119
+ format_column("Message", message_col_width, :center)
120
+ ]
121
+ headings.unshift(format_column("Asset", tag_col_width, :center)) if requires_tag?
122
+ else
123
+ headings = [
124
+ "Created", "Severity", "Source", "Format", "Message"
125
+ ]
126
+ headings.unshift("Asset") if requires_tag?
127
+ end
128
+ table.headings = headings
129
+ end
130
+ end
131
+
132
+ # Setup table width according to options
133
+ def setup_table
134
+ # Only setup table style once. Either dynamic or based on width of
135
+ if streaming? and not displayed? then
136
+ # If logs will be streaming, use the maximum width for the table
137
+ term_width = dynamic_terminal_width
138
+ @table_width = term_width
139
+ table.style = {:width => table_width}
140
+ elsif streaming? then
141
+ table.style = {:width => table_width}
142
+ end
143
+ end
144
+
145
+ def streaming?
146
+ options.fetch(:streaming, false)
147
+ end
148
+
149
+ def displayed?
150
+ @already_displayed
151
+ end
152
+ def set_displayed
153
+ @already_displayed = true
154
+ end
155
+
156
+ # Require a tag if mode is all
157
+ def requires_tag?
158
+ @requires_tag ||= (options.fetch(:all_tag, nil).to_s.downcase == asset_tag.to_s.downcase)
159
+ end
160
+
161
+ def created_col_width
162
+ 19 # this is the formatted datetime
163
+ end
164
+ def severity_col_width
165
+ 13 # width of INFORMATIONAL
166
+ end
167
+ def source_col_width
168
+ 8 # width of INTERNAL
169
+ end
170
+ def format_col_width
171
+ 16 # width of application/json
172
+ end
173
+ def tag_col_width
174
+ requires_tag? ? 24 : 0
175
+ end
176
+
177
+ # This needs to take into account all the styling and such to avoid terminal table throwing an
178
+ # exception
179
+ def message_col_width
180
+ cells = requires_tag? ? 6 : 5
181
+ dyn_width = (table.cell_spacing * cells) + table.style.border_y.length
182
+ table_width - created_col_width - severity_col_width - source_col_width - format_col_width - tag_col_width - dyn_width
183
+ end
184
+
185
+ end # class LogPrinter
186
+
187
+ end # module CollinsShell