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,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