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::EVENT_STATE_DIR, Constants::HUMAN_TIME_FORMAT, Constants::HUMAN_TIME_FORMAT_MS, Constants::SEARCH_SPLIT

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Base

Returns a new instance of Base.



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

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_sdk_class
  @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.



25
26
27
# File 'lib/wavefront-cli/base.rb', line 25

def klass
  @klass
end

#klass_wordObject

Returns the value of attribute klass_word.



25
26
27
# File 'lib/wavefront-cli/base.rb', line 25

def klass_word
  @klass_word
end

#optionsObject

Returns the value of attribute options.



25
26
27
# File 'lib/wavefront-cli/base.rb', line 25

def options
  @options
end

#wfObject

Returns the value of attribute wf.



25
26
27
# File 'lib/wavefront-cli/base.rb', line 25

def wf
  @wf
end

Instance Method Details

#_sdk_classObject

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



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

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



531
532
533
# File 'lib/wavefront-cli/base.rb', line 531

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

#check_response_blocks(data) ⇒ Object



247
248
249
250
251
# File 'lib/wavefront-cli/base.rb', line 247

def check_response_blocks(data)
  %i[status response].each do |b|
    abort "No #{b} block in API response" unless data.respond_to?(b)
  end
end

#check_status(status) ⇒ Object



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

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

#cli_output_class(output_format) ⇒ Object



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

def cli_output_class(output_format)
  require_relative File.join('output', output_format.to_s)
  Object.const_get(format('WavefrontOutput::%<class>s',
                          class: output_format.to_s.capitalize))
end

#conds_to_query(conds) ⇒ Array[Hash]

Turn a list of search conditions into an API query

Parameters:

Returns:



480
481
482
483
484
485
# File 'lib/wavefront-cli/base.rb', line 480

def conds_to_query(conds)
  conds.map do |cond|
    key, value = cond.split(SEARCH_SPLIT, 2)
    { key: key, value: value }.merge(matching_method(cond))
  end
end

#descriptive_nameObject



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

def descriptive_name
  klass_word
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.

Returns:

  • (nil)

Raises:

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



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/wavefront-cli/base.rb', line 179

def dispatch
  # 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".
  #
  method_word_list.reverse_each do |w_list|
    if w_list.reject { |w| options[w.to_sym] }.empty?
      method = name_of_do_method(w_list)
      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 ‘response’ and ‘status’ structures.

  • method (String)

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



225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/wavefront-cli/base.rb', line 225

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

  exit if options[:noop]

  ok_exit data if options[:format] == 'raw'

  check_response_blocks(data)
  warning_message(data.status)
  status_error_handler(data, method)
  handle_response(data.response, format_var, method)
end

#display_api_error(status) ⇒ Object

Classes can provide methods which give the user information on a given error code. They are named #handle_errcode_xxx, and return a string.

Parameters:

  • status (Map)

    status object from SDK response

Returns:

  • System exit



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/wavefront-cli/base.rb', line 259

def display_api_error(status)
  method = format('handle_errcode_%<code>s', code: status.code).to_sym

  msg = if respond_to?(method)
          send(method, status)
        else
          status.message || 'No further information'
        end

  abort format('ERROR: API code %<code>s. %<message>s.',
               code: status.code,
               message: msg.chomp('.')).fold(TW, 7)
end

#display_classObject



333
334
335
# File 'lib/wavefront-cli/base.rb', line 333

def display_class
  klass.name.sub('Wavefront', 'WavefrontDisplay')
end

#display_no_api_response(data, method) ⇒ Object



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

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

#do_deleteObject



406
407
408
# File 'lib/wavefront-cli/base.rb', line 406

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

#do_describeObject



368
369
370
# File 'lib/wavefront-cli/base.rb', line 368

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

#do_dumpObject



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

def do_dump
  cannot_noop!

  case options[:format]
  when 'yaml'
    ok_exit dump_yaml
  when 'json'
    ok_exit dump_json
  else
    abort format("Dump format must be 'json' or 'yaml'. " \
                 "(Tried '%<format>s')", options)
  end
end

#do_importObject



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

def do_import
  require_relative 'subcommands/import'
  WavefrontCli::Subcommand::Import.new(self, options).run!
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.



358
359
360
361
362
363
364
365
366
# File 'lib/wavefront-cli/base.rb', line 358

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



438
439
440
441
442
443
# File 'lib/wavefront-cli/base.rb', line 438

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_setObject



432
433
434
435
436
# File 'lib/wavefront-cli/base.rb', line 432

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

#do_undeleteObject



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

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

#dump_jsonObject



390
391
392
# File 'lib/wavefront-cli/base.rb', line 390

def dump_json
  item_dump_call.to_json
end

#dump_yamlObject



386
387
388
# File 'lib/wavefront-cli/base.rb', line 386

def dump_yaml
  JSON.parse(item_dump_call.to_json).to_yaml
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 rubocop:disable Metrics/MethodLength

Parameters:

  • obj (Object)

    the thing to search

  • key (String, Symbol)

    the key to search for

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

    values of matched keys

Returns:



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/wavefront-cli/base.rb', line 543

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

  aggr
end

#failed_validation_message(input) ⇒ Object



118
119
120
121
122
# File 'lib/wavefront-cli/base.rb', line 118

def failed_validation_message(input)
  format("'%<value>s' is not a valid %<thing>s ID.",
         value: input,
         thing: descriptive_name)
end

#format_varSymbol

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

Returns:

  • (Symbol)

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



165
166
167
168
169
# File 'lib/wavefront-cli/base.rb', line 165

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.



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

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

#handle_response(resp, format, method) ⇒ Object



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

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



324
325
326
# File 'lib/wavefront-cli/base.rb', line 324

def hcl_fields
  []
end

#import_to_create(raw) ⇒ Object

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



564
565
566
567
568
569
570
571
# File 'lib/wavefront-cli/base.rb', line 564

def import_to_create(raw)
  raw.each_with_object({}) do |(k, v), a|
    a[k.to_sym] = v unless k == :id
  end
rescue StandardError => e
  puts e if options[:debug]
  raise WavefrontCli::Exception::UnparseableInput
end

#item_dump_callObject

Broken out into its own method because ‘users’ does not use pagination



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

def item_dump_call
  wf.list(ALL_PAGE_SIZE, :all).response.items
end

#load_display_classObject



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

def load_display_class
  require_relative File.join('display', klass_word)
  Object.const_get(display_class)
end

#matching_method(cond) ⇒ Hash

rubocop:disable Metrics/MethodLength

Parameters:

  • cond (String)

    a search condition, like “key=value”

Returns:

  • (Hash)

    of matchingMethod and negated



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/wavefront-cli/base.rb', line 491

def matching_method(cond)
  case cond
  when /^\w+~/
    { matchingMethod: 'CONTAINS', negated: false }
  when /^\w+!~/
    { matchingMethod: 'CONTAINS', negated: true }
  when /^\w+=/
    { matchingMethod: 'EXACT', negated: false }
  when /^\w+!=/
    { matchingMethod: 'EXACT', negated: true }
  when /^\w+\^/
    { matchingMethod: 'STARTSWITH', negated: false }
  when /^\w+!\^/
    { matchingMethod: 'STARTSWITH', negated: true }
  else
    raise(WavefrontCli::Exception::UnparseableSearchPattern, cond)
  end
end

#method_word_listObject

Take a list of do_ methods, remove the ‘do_’ from their name, and break them into arrays of ‘_’ separated words. The array is sorted by length, longest first.



207
208
209
210
# File 'lib/wavefront-cli/base.rb', line 207

def method_word_list
  do_methods = methods.select { |m| m.to_s.start_with?('do_') }
  do_methods.map { |m| m.to_s.split('_')[1..] }.sort_by(&:length)
end

#mk_credsHash

Make a wavefront-sdk credentials object from standard options.

Returns:

  • (Hash)

    containing ‘token` and `endpoint`.



133
134
135
136
137
# File 'lib/wavefront-cli/base.rb', line 133

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`.



145
146
147
148
149
150
151
152
153
154
# File 'lib/wavefront-cli/base.rb', line 145

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

  ret[:verbose] = options[:noop] ? true : options[:verbose]
  ret[:raw_response] = true if options[:format] == 'raw'

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

#name_of_do_method(word_list) ⇒ Object



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

def name_of_do_method(word_list)
  (%w[do] + word_list).join('_')
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



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

def no_api_response
  []
end

#ok_exit(message) ⇒ Object

Print a message and exit 0



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

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.



514
515
516
517
518
519
520
521
522
523
524
525
# File 'lib/wavefront-cli/base.rb', line 514

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



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

def options_and_exit
  ok_exit(options)
end

#parseable_output(output_format, resp) ⇒ Object



304
305
306
307
308
309
310
311
# File 'lib/wavefront-cli/base.rb', line 304

def parseable_output(output_format, resp)
  options[:class] = klass_word
  options[:hcl_fields] = hcl_fields
  cli_output_class(output_format).new(resp, options).run
rescue LoadError
  raise(WavefrontCli::Exception::UnsupportedOutput,
        unsupported_format_message(output_format))
end

#range_hashObject

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

rubocop:disable Metrics/MethodLength



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/wavefront-cli/base.rb', line 449

def range_hash
  offset_key = :offset

  if options[:all]
    limit  = :all
    offset = ALL_PAGE_SIZE
  elsif options[:cursor]
    offset_key = :cursor
    limit = options[:limit]
    offset = options[:cursor]
  else
    limit  = options[:limit]
    offset = options[:offset]
  end

  { limit: limit, offset_key => offset }
end

#require_sdk_classObject



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

def require_sdk_class
  require File.join('wavefront-sdk', @klass_word)
end

#runObject



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

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.



471
472
473
# File 'lib/wavefront-cli/base.rb', line 471

def search_key
  klass_word
end

#smart_delete(object_type = klass_word) ⇒ Object

Some objects support soft deleting. To handle that, call this method from do_delete



413
414
415
416
417
# File 'lib/wavefront-cli/base.rb', line 413

def smart_delete(object_type = klass_word)
  cannot_noop!
  puts smart_delete_message(object_type)
  wf.delete(options[:'<id>'])
end

#smart_delete_message(object_type) ⇒ Object



419
420
421
422
423
424
425
426
# File 'lib/wavefront-cli/base.rb', line 419

def smart_delete_message(object_type)
  desc = wf.describe(options[:'<id>'])
  word = desc.ok? ? 'Soft' : 'Permanently'
  format("%<soft_or_hard>s deleting %<object>s '%<id>s'",
         soft_or_hard: word,
         object: object_type,
         id: options[:'<id>'])
end

#status_error_handler(data, method) ⇒ Object



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

def status_error_handler(data, method)
  return if check_status(data.status)

  handle_error(method, data.status.code) if format_var == :human
  display_api_error(data.status)
end

#unsupported_format_message(output_format) ⇒ Object



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

def unsupported_format_message(output_format)
  format("The '%<command>s' command does not support '%<format>s' output.",
         command: options[:class], format: output_format)
end

#validate_idObject



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

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

#validate_inputObject



97
98
99
100
101
# File 'lib/wavefront-cli/base.rb', line 97

def validate_input
  validate_id if options[:'<id>']
  validate_tags if options[:'<tag>']
  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.



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

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



103
104
105
106
107
108
109
110
# File 'lib/wavefront-cli/base.rb', line 103

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

#validator_exceptionObject



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

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.



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

def validator_method
  :"wf_#{klass_word}_id?"
end

#warning_message(status) ⇒ Object



273
274
275
276
277
# File 'lib/wavefront-cli/base.rb', line 273

def warning_message(status)
  return unless status.code.between?(201, 299)

  puts format("API WARNING: '%<message>s'.", message: status.message)
end