Class: WavefrontCli::Base

Inherits:
Object
  • Object
show all
Includes:
Wavefront::Validators, Constants
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.

Constant Summary

Constants included from Constants

Constants::ALL_PAGE_SIZE, Constants::DEFAULT_CONFIG, Constants::DEFAULT_OPTS, Constants::HUMAN_TIME_FORMAT, Constants::HUMAN_TIME_FORMAT_MS

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Base

Returns a new instance of Base.



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

def initialize(options)
  @options = options
  sdk_class = _sdk_class
  @klass_word = sdk_class.split('::').last.downcase
  validate_input

  options_and_exit if options[:help]

  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.



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

def klass
  @klass
end

#klass_wordObject

Returns the value of attribute klass_word.



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

def klass_word
  @klass_word
end

#optionsObject

Returns the value of attribute options.



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

def options
  @options
end

#wfObject

Returns the value of attribute wf.



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

def wf
  @wf
end

Instance Method Details

#_sdk_classObject

Normally we map the class name to a similar one in the SDK. Overriding his method lets you map to something else.



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

def _sdk_class
  self.class.name.sub(/Cli/, '')
end

#cannot_noop!Object

Operations which do require multiple operations cannot be perormed as a no-op. Drop in a call to this method for those things. The exception is caught in controller.rb



490
491
492
# File 'lib/wavefront-cli/base.rb', line 490

def cannot_noop!
  raise WavefrontCli::Exception::UnsupportedNoop if options[:noop]
end

#check_status(status) ⇒ Object



244
245
246
# File 'lib/wavefront-cli/base.rb', line 244

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

#conds_to_query(conds) ⇒ Object

Turn a list of search conditions into an API query



433
434
435
436
437
438
439
440
441
# File 'lib/wavefront-cli/base.rb', line 433

def conds_to_query(conds)
  conds.each_with_object([]) do |cond, aggr|
    key, value = cond.split(/\W/, 2)
    q = { key: key, value: value }
    q[:matchingMethod] = 'EXACT' if cond.start_with?("#{key}=")
    q[:matchingMethod] = 'STARTSWITH' if cond.start_with?("#{key}^")
    aggr.<< q
  end
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.

rubocop:disable Metrics/AbcSize

Returns:

  • (nil)

Raises:

  • WavefrontCli::Exception::UnhandledCommand if the command does not match a do_ method.



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

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.

rubocop:disable Metrics/AbcSize

Parameters:

  • data (WavefrontResponse)

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

  • method (String)

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



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/wavefront-cli/base.rb', line 212

def display(data, method)
  if no_api_response.include?(method)
    return display_no_api_response(data, method)
  end

  exit if options[:noop]

  %i[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
    display_api_error(data.status)
  end

  handle_response(data.response, format_var, method)
end

#display_api_error(status) ⇒ Object

Returns System exit.

Parameters:

  • status (Map)

    status object from SDK response

Returns:

  • System exit



235
236
237
238
# File 'lib/wavefront-cli/base.rb', line 235

def display_api_error(status)
  msg = status.message || 'No further information'
  abort format('ERROR: API code %s: %s.', status.code, msg.chomp('.'))
end

#display_no_api_response(data, method) ⇒ Object



240
241
242
# File 'lib/wavefront-cli/base.rb', line 240

def display_no_api_response(data, method)
  handle_response(data, format_var, method)
end

#do_deleteObject



388
389
390
# File 'lib/wavefront-cli/base.rb', line 388

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

#do_describeObject



371
372
373
# File 'lib/wavefront-cli/base.rb', line 371

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

#do_importObject



375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/wavefront-cli/base.rb', line 375

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

  begin
    prepped = import_to_create(raw)
  rescue StandardError => e
    puts e if options[:debug]
    raise WavefrontCli::Exception::UnparseableInput
  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.



361
362
363
364
365
366
367
368
369
# File 'lib/wavefront-cli/base.rb', line 361

def do_list
  list = if options[:all]
           wf.list(ALL_PAGE_SIZE, :all)
         else
           wf.list(options[:offset] || 0, options[:limit] || 100)
         end

  respond_to?(:list_filter) ? list_filter(list) : list
end

#do_search(cond = ) ⇒ Object



402
403
404
405
406
407
# File 'lib/wavefront-cli/base.rb', line 402

def do_search(cond = options[:'<condition>'])
  require 'wavefront-sdk/search'
  wfs = Wavefront::Search.new(mk_creds, mk_opts)
  query = conds_to_query(cond)
  wfs.search(search_key, query, range_hash)
end

#do_tag_addObject



447
448
449
# File 'lib/wavefront-cli/base.rb', line 447

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

#do_tag_clearObject



459
460
461
# File 'lib/wavefront-cli/base.rb', line 459

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

#do_tag_deleteObject



451
452
453
# File 'lib/wavefront-cli/base.rb', line 451

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

#do_tag_setObject



455
456
457
# File 'lib/wavefront-cli/base.rb', line 455

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

#do_tagsObject



443
444
445
# File 'lib/wavefront-cli/base.rb', line 443

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

#do_undeleteObject



392
393
394
# File 'lib/wavefront-cli/base.rb', line 392

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

#do_updateObject



396
397
398
399
400
# File 'lib/wavefront-cli/base.rb', line 396

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

#extract_values(obj, key, aggr = []) ⇒ Array

A recursive function which fetches list of values from a nested hash. Used by WavefrontCli::Dashboard#do_queries

Parameters:

  • obj (Object)

    the thing to search

  • key (String, Symbol)

    the key to search for

  • aggr (Array) (defaults to: [])

    values of matched keys

Returns:



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/wavefront-cli/base.rb', line 501

def extract_values(obj, key, aggr = [])
  if obj.is_a?(Hash)
    obj.each_pair do |k, v|
      if k == key && !v.to_s.empty?
        aggr.<< v
      else
        extract_values(v, key, aggr)
      end
    end
  elsif obj.is_a?(Array)
    obj.each { |e| extract_values(e, key, aggr) }
  end

  aggr
end

#failed_validation_message(input) ⇒ Object



114
115
116
# File 'lib/wavefront-cli/base.rb', line 114

def failed_validation_message(input)
  format("'%s' is not a valid %s ID.", input, klass_word)
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



153
154
155
156
157
# File 'lib/wavefront-cli/base.rb', line 153

def format_var
  options[:format].to_sym
rescue NoMethodError
  :human
end

#handle_error(method, code) ⇒ Object

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



251
252
253
254
# File 'lib/wavefront-cli/base.rb', line 251

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

#handle_response(resp, format, method) ⇒ Object



256
257
258
259
260
261
262
263
# File 'lib/wavefront-cli/base.rb', line 256

def handle_response(resp, format, method)
  if format == :human
    k = load_display_class
    k.new(resp, options).run(method)
  else
    parseable_output(format, resp)
  end
end

#hcl_fieldsObject

rubocop:enable Metrics/AbcSize



280
281
282
# File 'lib/wavefront-cli/base.rb', line 280

def hcl_fields
  []
end

#import_to_create(raw) ⇒ Object

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



466
467
468
# File 'lib/wavefront-cli/base.rb', line 466

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

#load_display_classObject



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

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. If path is ‘-’ then it will read STDIN.

rubocop:disable Metrics/AbcSize

Parameters:

  • path (String)

    the file to load

Returns:

  • (Hash)

    a Ruby object of the loaded file

Raises:

  • WavefrontCli::Exception::UnsupportedFileFormat if the filetype is unknown.

  • pass through any error loading or parsing the file



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/wavefront-cli/base.rb', line 318

def load_file(path)
  return load_from_stdin if path == '-'

  file = Pathname.new(path)

  raise WavefrontCli::Exception::FileNotFound 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 WavefrontCli::Exception::UnsupportedFileFormat
  end
end

#load_from_stdinObject

Read STDIN and return a Ruby object, assuming that STDIN is valid JSON or YAML. This is a dumb method, it does no buffering, so STDIN must be a single block of data. This appears to be a valid assumption for use-cases of this CLI.

Returns:

  • (Object)

Raises:

  • Wavefront::Exception::UnparseableInput if the input does not parse



344
345
346
347
348
349
350
351
352
353
354
# File 'lib/wavefront-cli/base.rb', line 344

def load_from_stdin
  raw = STDIN.read

  if raw.start_with?('---')
    YAML.safe_load(raw)
  else
    JSON.parse(raw)
  end
rescue RuntimeError
  raise Wavefront::Exception::UnparseableInput
end

#mk_credsHash

Make a wavefront-sdk credentials object from standard options.

Returns:

  • (Hash)

    containing token and endpoint.



123
124
125
126
127
# File 'lib/wavefront-cli/base.rb', line 123

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. We force verbosity on for a noop, otherwise we get no output.

Returns:

  • (Hash)

    containing debug, verbose, and noop.



135
136
137
138
139
140
141
142
143
# File 'lib/wavefront-cli/base.rb', line 135

def mk_opts
  ret = { debug:   options[:debug],
          noop:    options[:noop] }

  ret[:verbose] = options[:noop] ? true : options[:verbose]

  ret.merge!(extra_options) if respond_to?(:extra_options)
  ret
end

#no_api_responseArray[String]

Some subcommands don’t make an API call, so they don’t return a Wavefront::Response object. You can override this method with something which returns an array of methods like that. They will bypass the usual response checking.

response

Returns:

  • (Array[String])

    methods which do not include an API



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

def no_api_response
  []
end

#ok_exit(message) ⇒ Object

Print a message and exit 0



67
68
69
70
# File 'lib/wavefront-cli/base.rb', line 67

def ok_exit(message)
  puts message
  exit 0
end

#one_or_allObject

Return a detailed description of one item, if an ID has been given, or all items if it has not.



473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/wavefront-cli/base.rb', line 473

def one_or_all
  if options[:'<id>']
    resp = wf.describe(options[:'<id>'])
    data = [resp.response]
  else
    options[:all] = true
    resp = do_list
    data = resp.response.items
  end

  [resp, data]
end

#options_and_exitObject



61
62
63
# File 'lib/wavefront-cli/base.rb', line 61

def options_and_exit
  ok_exit(options)
end

#parseable_output(format, resp) ⇒ Object

rubocop:disable Metrics/AbcSize



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/wavefront-cli/base.rb', line 266

def parseable_output(format, resp)
  options[:class] = klass_word
  options[:hcl_fields] = hcl_fields
  require_relative File.join('output', format.to_s)
  oclass = Object.const_get(format('WavefrontOutput::%s',
                                   format.to_s.capitalize))
  oclass.new(resp, options).run
rescue LoadError
  raise(WavefrontCli::Exception::UnsupportedOutput,
        format("The '%s' command does not support '%s' output.",
               options[:class], format))
end

#range_hashObject

If the user has specified –all, override any limit and offset values



412
413
414
415
416
417
418
419
420
421
422
# File 'lib/wavefront-cli/base.rb', line 412

def range_hash
  if options[:all]
    limit  = :all
    offset = ALL_PAGE_SIZE
  else
    limit  = options[:limit]
    offset = options[:offset] || options[:cursor]
  end

  { limit: limit, offset: offset }
end

#runObject



72
73
74
75
# File 'lib/wavefront-cli/base.rb', line 72

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

#search_keyObject

The search URI pattern doesn’t always match the command name, or class name. Override this method if this is the case.



427
428
429
# File 'lib/wavefront-cli/base.rb', line 427

def search_key
  klass_word
end

#validate_idObject



108
109
110
111
112
# File 'lib/wavefront-cli/base.rb', line 108

def validate_id
  send(validator_method, options[:'<id>'])
rescue validator_exception
  abort failed_validation_message(options[:'<id>'])
end

#validate_inputObject



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

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.



295
296
297
298
299
300
301
302
303
304
# File 'lib/wavefront-cli/base.rb', line 295

def validate_opts
  unless options[:token]
    raise(WavefrontCli::Exception::CredentialError,
          'Missing API token.')
  end

  return true if options[:endpoint]
  raise(WavefrontCli::Exception::CredentialError,
        'Missing API endpoint.')
end

#validate_tags(key = :'<tag>') ⇒ Object



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

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

#validator_exceptionObject



85
86
87
88
89
# File 'lib/wavefront-cli/base.rb', line 85

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.



81
82
83
# File 'lib/wavefront-cli/base.rb', line 81

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