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

#check_status(status) ⇒ Object



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

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



424
425
426
427
428
429
430
431
432
# File 'lib/wavefront-cli/base.rb', line 424

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.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/wavefront-cli/base.rb', line 161

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 a ‘response’ and ‘status’ structures.

  • method (String)

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



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

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



228
229
230
231
# File 'lib/wavefront-cli/base.rb', line 228

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

#display_no_api_response(data, method) ⇒ Object



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

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

#do_deleteObject



376
377
378
# File 'lib/wavefront-cli/base.rb', line 376

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

#do_describeObject



359
360
361
# File 'lib/wavefront-cli/base.rb', line 359

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

#do_importObject



363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/wavefront-cli/base.rb', line 363

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.



354
355
356
357
# File 'lib/wavefront-cli/base.rb', line 354

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

#do_search(cond = ) ⇒ Object



393
394
395
396
397
398
# File 'lib/wavefront-cli/base.rb', line 393

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



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

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

#do_tag_clearObject



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

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

#do_tag_deleteObject



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

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

#do_tag_setObject



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

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

#do_tagsObject



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

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

#do_undeleteObject



380
381
382
# File 'lib/wavefront-cli/base.rb', line 380

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

#do_updateObject



384
385
386
387
388
389
390
391
# File 'lib/wavefront-cli/base.rb', line 384

def do_update
  k, v = options[:'<key=value>'].split('=', 2)
  wf.update(options[:'<id>'], k => v)
rescue NoMethodError
  raise(WavefrontCli::Exception::UnsupportedOperation,
        'Updates require two API calls. We cannot do the second ' \
        'when -n is set.')
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



147
148
149
150
# File 'lib/wavefront-cli/base.rb', line 147

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.



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

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

#handle_response(resp, format, method) ⇒ Object



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

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



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

def hcl_fields
  []
end

#import_to_create(raw) ⇒ Object

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



457
458
459
# File 'lib/wavefront-cli/base.rb', line 457

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

#load_display_classObject



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

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



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/wavefront-cli/base.rb', line 311

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



337
338
339
340
341
342
343
344
345
346
347
# File 'lib/wavefront-cli/base.rb', line 337

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.



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

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.



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

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

  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

#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



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

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



403
404
405
406
407
408
409
410
411
412
413
# File 'lib/wavefront-cli/base.rb', line 403

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.



418
419
420
# File 'lib/wavefront-cli/base.rb', line 418

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 "'#{options[:'<id>']}' is not a valid #{klass_word} 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.



288
289
290
291
292
293
294
295
296
297
# File 'lib/wavefront-cli/base.rb', line 288

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