Class: WavefrontCli::Base

Inherits:
Object
  • Object
show all
Includes:
Wavefront::Validators
Defined in:
lib/wavefront-cli/base.rb

Overview

Parent of all the CLI classes. This class uses metaprogramming techniques to try to make adding new CLI commands and sub-commands as simple as possible.

To define a subcommand ‘cmd’, you only need add it to the docopt description in the relevant section, and create a method ‘do_cmd’. The WavefrontCli::Base::dispatch() method will find it, and call it. If your subcommand has multiple words, like ‘delete tag’, your do method would be called do_delete_tag. The do_ methods are able to access the Wavefront SDK object as wf, and all docopt options as options.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Base

Returns a new instance of Base.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/wavefront-cli/base.rb', line 26

def initialize(options)
  @options = options
  sdk_class = self.class.name.sub(/Cli/, '')
  @klass_word = sdk_class.split('::').last.downcase
  validate_input

  if options.include?(:help) && options[:help]
    puts options
    exit 0
  end

  require File.join('wavefront-sdk', @klass_word)
  @klass = Object.const_get(sdk_class)

  send(:post_initialize, options) if respond_to?(:post_initialize)
end

Instance Attribute Details

#klassObject

Returns the value of attribute klass.



22
23
24
# File 'lib/wavefront-cli/base.rb', line 22

def klass
  @klass
end

#klass_wordObject

Returns the value of attribute klass_word.



22
23
24
# File 'lib/wavefront-cli/base.rb', line 22

def klass_word
  @klass_word
end

#optionsObject

Returns the value of attribute options.



22
23
24
# File 'lib/wavefront-cli/base.rb', line 22

def options
  @options
end

#wfObject

Returns the value of attribute wf.



22
23
24
# File 'lib/wavefront-cli/base.rb', line 22

def wf
  @wf
end

Instance Method Details

#check_status(status) ⇒ Object



192
193
194
# File 'lib/wavefront-cli/base.rb', line 192

def check_status(status)
  status.respond_to?(:result) && status.result == 'OK'
end

#dispatchnil

Works out the user’s command by matching any options docopt has set to ‘true’ with any ‘do_’ method in the class. Then calls that method, and displays whatever it returns.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/wavefront-cli/base.rb', line 129

def dispatch
  #
  # Take a list of do_ methods, remove the 'do_' from their name,
  # and break them into arrays of '_' separated words.
  #
  m_list = methods.select { |m| m.to_s.start_with?('do_') }.map do |m|
    m.to_s.split('_')[1..-1]
  end

  # Sort that array of arrays by length, longest first.  Then look
  # through each deconstructed method name and see if the user
  # supplied an option for each component. Call the first one that
  # matches. The order will ensure we match "do_delete_tags" before
  # we match "do_delete".
  #
  m_list.sort_by(&:length).reverse.each do |m|
    if m.reject { |w| options[w.to_sym] }.empty?
      method = (%w(do) + m).join('_')
      return display(public_send(method), method)
    end
  end

  if respond_to?(:do_default)
    return display(public_send(:do_default), :do_default)
  end

  raise WavefrontCli::Exception::UnhandledCommand
end

#display(data, method) ⇒ Object

Display a Ruby object as JSON, YAML, or human-readable. We provide a default method to format human-readable output, but you can override it by creating your own humanize_command_output method control how its output is handled by setting the response instance variable.

Parameters:

  • data (WavefrontResponse)

    an object returned by a Wavefront SDK method. This will contain a ‘response’ and ‘status’ structures.

  • method (String)

    the name of the method which produced this output. Used to find a suitable humanize method.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/wavefront-cli/base.rb', line 171

def display(data, method)
  exit if options[:noop]

  [:status, :response].each do |b|
    abort "no #{b} block in API response" unless data.respond_to?(b)
  end

  unless check_status(data.status)
    handle_error(method, data.status.code) if format_var == :human
    abort "API #{data.status.code}: #{data.status.message}."
  end

  resp = if data.response.respond_to?(:items)
           data.response.items
         else
           data.response
         end

  handle_response(resp, format_var, method)
end

#do_deleteObject



285
286
287
# File 'lib/wavefront-cli/base.rb', line 285

def do_delete
  wf.delete(options[:'<id>'])
end

#do_describeObject



268
269
270
# File 'lib/wavefront-cli/base.rb', line 268

def do_describe
  wf.describe(options[:'<id>'])
end

#do_importObject



272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/wavefront-cli/base.rb', line 272

def do_import
  raw = load_file(options[:'<file>'])

  begin
    prepped = import_to_create(raw)
  rescue => e
    puts e if options[:debug]
    raise 'could not parse input.'
  end

  wf.create(prepped)
end

#do_listObject

Below here are common methods. Most are used by most classes, but if they don’t match a command described in the docopt text, the dispatcher will never call them. So, there’s no harm inheriting unneeded things. Some classes override them.



264
265
266
# File 'lib/wavefront-cli/base.rb', line 264

def do_list
  wf.list(options[:offset] || 0, options[:limit] || 100)
end

#do_searchObject



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/wavefront-cli/base.rb', line 298

def do_search
  require 'wavefront-sdk/search'
  wfs = Wavefront::Search.new(mk_creds, mk_opts)

  query = options[:'<condition>'].each_with_object([]) do |c, aggr|
    key, value = c.split(/\W/, 2)
    q = { key: key, value: value }
    q[:matchingMethod] = 'EXACT' if c.start_with?("#{key}=")
    q[:matchingMethod] = 'STARTSWITH' if c.start_with?("#{key}^")
    aggr.<< q
  end

  wfs.search(klass_word, query, { limit: options[:limit],
                                  offset: options[:offset] || options[:cursor]})
end

#do_tag_addObject



318
319
320
# File 'lib/wavefront-cli/base.rb', line 318

def do_tag_add
  wf.tag_add(options[:'<id>'], options[:'<tag>'].first)
end

#do_tag_clearObject



330
331
332
# File 'lib/wavefront-cli/base.rb', line 330

def do_tag_clear
  wf.tag_set(options[:'<id>'], [])
end

#do_tag_deleteObject



322
323
324
# File 'lib/wavefront-cli/base.rb', line 322

def do_tag_delete
  wf.tag_delete(options[:'<id>'], options[:'<tag>'].first)
end

#do_tag_setObject



326
327
328
# File 'lib/wavefront-cli/base.rb', line 326

def do_tag_set
  wf.tag_set(options[:'<id>'], options[:'<tag>'])
end

#do_tagsObject



314
315
316
# File 'lib/wavefront-cli/base.rb', line 314

def do_tags
  wf.tags(options[:'<id>'])
end

#do_undeleteObject



289
290
291
# File 'lib/wavefront-cli/base.rb', line 289

def do_undelete
  wf.undelete(options[:'<id>'])
end

#do_updateObject



293
294
295
296
# File 'lib/wavefront-cli/base.rb', line 293

def do_update
  k, v = options[:'<key=value>'].split('=')
  wf.update(options[:'<id>'], k => v)
end

#format_varSymbol

To allow a user to default to different output formats for different object, we are able to define a format for each class. instance, alertformat or agentformat. This method returns such a string appropriate for the inheriting class.

Returns:

  • (Symbol)

    name of the option or config-file key which sets the default output format for this class



116
117
118
119
# File 'lib/wavefront-cli/base.rb', line 116

def format_var
  options[:format].to_sym
  # (self.class.name.split('::').last.downcase + 'format').to_sym
end

#handle_error(method, code) ⇒ Object

This gives us a chance to catch different errors in WavefrontDisplay classes. If nothing catches, them abort.



199
200
201
202
# File 'lib/wavefront-cli/base.rb', line 199

def handle_error(method, code)
  k = load_display_class
  k.new({}, options).run_error([method, code].join('_'))
end

#handle_response(resp, format, method) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/wavefront-cli/base.rb', line 204

def handle_response(resp, format, method)
  case format
  when :json
    puts resp.to_json
  when :yaml # We don't want the YAML keys to be symbols.
    puts JSON.parse(resp.to_json).to_yaml
  when :ruby
    p resp
  when :human
    k = load_display_class
    k.new(resp, options).run(method)
  else
    raise "Unknown output format '#{format}'."
  end
end

#import_to_create(raw) ⇒ Object

Most things will re-import with the POST method if you remove the ID.



337
338
339
# File 'lib/wavefront-cli/base.rb', line 337

def import_to_create(raw)
  raw.delete_if { |k, _v| k == 'id' }
end

#load_display_classObject



220
221
222
223
# File 'lib/wavefront-cli/base.rb', line 220

def load_display_class
  require_relative File.join('display', klass_word)
  Object.const_get(klass.name.sub('Wavefront', 'WavefrontDisplay'))
end

#load_file(path) ⇒ Hash

Give it a path to a file (as a string) and it will return the contents of that file as a Ruby object. Automatically detects JSON and YAML. Raises an exception if it doesn’t look like either.

Parameters:

  • path (String)

    the file to load

Returns:

  • (Hash)

    a Ruby object of the loaded file

Raises:

  • pass through any error loading or parsing the file



246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/wavefront-cli/base.rb', line 246

def load_file(path)
  file = Pathname.new(path)
  raise 'Import file does not exist.' unless file.exist?

  if file.extname == '.json'
    JSON.parse(IO.read(file))
  elsif file.extname == '.yaml' || file.extname == '.yml'
    YAML.safe_load(IO.read(file))
  else
    raise 'Unsupported file format.'
  end
end

#mk_credsHash

Make a wavefront-sdk credentials object from standard options.

Returns:

  • (Hash)

    containing token and endpoint.



89
90
91
92
93
94
# File 'lib/wavefront-cli/base.rb', line 89

def mk_creds
  { token:    options[:token],
    endpoint: options[:endpoint],
    agent:    "wavefront-cli-#{WF_CLI_VERSION}"
  }
end

#mk_optsHash

Make a common wavefront-sdk options object from standard CLI options.

Returns:

  • (Hash)

    containing debug, verbose, and noop.



101
102
103
104
105
106
# File 'lib/wavefront-cli/base.rb', line 101

def mk_opts
  { debug:   options[:debug],
    verbose: options[:verbose],
    noop:    options[:noop],
  }
end

#runObject



43
44
45
46
# File 'lib/wavefront-cli/base.rb', line 43

def run
  @wf = klass.new(mk_creds, mk_opts)
  dispatch
end

#validate_idObject



78
79
80
81
82
# File 'lib/wavefront-cli/base.rb', line 78

def validate_id
  send(validator_method, options[:'<id>'])
rescue validator_exception
  abort "'#{options[:'<id>']}' is not a valid #{klass_word} ID."
end

#validate_inputObject



62
63
64
65
66
# File 'lib/wavefront-cli/base.rb', line 62

def validate_input
  validate_id if options[:'<id>']
  validate_tags if options[:'<tag>']
  send(:extra_validation) if respond_to?(:extra_validation)
end

#validate_optsObject

There are things we need to have. If we don’t have them, stop the user right now. Also, if we’re in debug mode, print out a hash of options, which can be very useful when doing actual debugging. Some classes may have to override this method. The writer, for instance, uses a proxy and has no token.



231
232
233
234
# File 'lib/wavefront-cli/base.rb', line 231

def validate_opts
  raise 'Please supply an API token.' unless options[:token]
  raise 'Please supply an API endpoint.' unless options[:endpoint]
end

#validate_tagsObject



68
69
70
71
72
73
74
75
76
# File 'lib/wavefront-cli/base.rb', line 68

def validate_tags
  Array(options[:'<tag>']).each do |t|
    begin
      send(:wf_tag?, t)
    rescue Wavefront::Exception::InvalidTag
      abort "'#{t}' is not a valid tag."
    end
  end
end

#validator_exceptionObject



56
57
58
59
60
# File 'lib/wavefront-cli/base.rb', line 56

def validator_exception
  Object.const_get(
    "Wavefront::Exception::Invalid#{klass_word.capitalize}Id"
  )
end

#validator_methodObject

We normally validate with a predictable method name. Alert IDs are validated with #wf_alert_id? etc. If you need to change that, override this method.



52
53
54
# File 'lib/wavefront-cli/base.rb', line 52

def validator_method
  "wf_#{klass_word}_id?".to_sym
end