wavefront-cli 8.3.0 → 8.5.1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +37 -0
  3. data/.github/workflows/test.yml +23 -0
  4. data/.rubocop.yml +10 -6
  5. data/HISTORY.md +21 -1
  6. data/lib/wavefront-cli/base.rb +3 -0
  7. data/lib/wavefront-cli/commands/.rubocop.yml +2 -13
  8. data/lib/wavefront-cli/commands/event.rb +8 -6
  9. data/lib/wavefront-cli/commands/serviceaccount.rb +6 -4
  10. data/lib/wavefront-cli/controller.rb +9 -0
  11. data/lib/wavefront-cli/display/base.rb +3 -2
  12. data/lib/wavefront-cli/display/printer/sparkline.rb +1 -1
  13. data/lib/wavefront-cli/display/serviceaccount.rb +12 -4
  14. data/lib/wavefront-cli/event.rb +50 -166
  15. data/lib/wavefront-cli/event_store.rb +177 -0
  16. data/lib/wavefront-cli/exception.rb +21 -0
  17. data/lib/wavefront-cli/exception_handler.rb +4 -0
  18. data/lib/wavefront-cli/opt_handler.rb +1 -1
  19. data/lib/wavefront-cli/query.rb +1 -1
  20. data/lib/wavefront-cli/serviceaccount.rb +16 -6
  21. data/lib/wavefront-cli/settings.rb +3 -4
  22. data/lib/wavefront-cli/stdlib/string.rb +1 -1
  23. data/lib/wavefront-cli/version.rb +1 -1
  24. data/lib/wavefront-cli/write.rb +1 -1
  25. data/spec/.rubocop.yml +2 -17
  26. data/spec/spec_helper.rb +0 -1
  27. data/spec/support/minitest_assertions.rb +2 -2
  28. data/spec/wavefront-cli/commands/base_spec.rb +2 -2
  29. data/spec/wavefront-cli/commands/config_spec.rb +1 -1
  30. data/spec/wavefront-cli/controller_spec.rb +14 -0
  31. data/spec/wavefront-cli/event_spec.rb +69 -109
  32. data/spec/wavefront-cli/event_store_spec.rb +186 -0
  33. data/spec/wavefront-cli/opt_handler_spec.rb +3 -3
  34. data/spec/wavefront-cli/serviceaccount_spec.rb +53 -21
  35. data/wavefront-cli.gemspec +5 -2
  36. metadata +62 -10
  37. data/.travis.yml +0 -20
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etc'
4
+ require 'fileutils'
5
+ require 'open3'
6
+ require 'json'
7
+ require_relative 'constants'
8
+ require_relative 'exception'
9
+
10
+ module WavefrontCli
11
+ #
12
+ # Encapsulation of everything needed to manage the locally stored state of
13
+ # events opened by the CLI. This is our own addition, entirely separate from
14
+ # Wavefront's API.
15
+ #
16
+ # When the user creates an open-ended event (i.e. one that does not have and
17
+ # end time, and is not instantaneous) a state file is created in a local
18
+ # directory. (*)
19
+ #
20
+ # That directory is defined by the EVENT_STATE_DIR constant, but may be
21
+ # overriden with an option in the constructor. The tests do this.
22
+ #
23
+ # (*) The user may specifically request that no state file be created with
24
+ # the --nostate flag.
25
+ #
26
+ class EventStore
27
+ include WavefrontCli::Constants
28
+
29
+ attr_reader :dir, :options
30
+
31
+ # @param state_dir [Pathname] override the default dir for testing
32
+ #
33
+ def initialize(options, state_dir = nil)
34
+ @options = options
35
+ @dir = event_state_dir(state_dir) + (Etc.getlogin || 'notty')
36
+ create_dir(dir)
37
+ end
38
+
39
+ def state_file_needed?
40
+ !(options[:nostate] || options[:end] || options[:instant])
41
+ end
42
+
43
+ def event_file(id)
44
+ /^\d{13}:.+/.match?(id) ? dir + id : nil
45
+ end
46
+
47
+ # We can override the temp directory with the WF_EVENT_STATE_DIR env var.
48
+ # This is primarily for testing, though someone may find a valid use for
49
+ # it.
50
+ #
51
+ def event_state_dir(state_dir = nil)
52
+ if ENV['WF_EVENT_STATE_DIR']
53
+ Pathname.new(ENV['WF_EVENT_STATE_DIR'])
54
+ elsif state_dir.nil?
55
+ EVENT_STATE_DIR
56
+ else
57
+ Pathname.new(state_dir)
58
+ end
59
+ end
60
+
61
+ # @param id [String,Nil] if this is falsey, returns the event on the top
62
+ # of the state stack, removing its state file. If it's an exact event
63
+ # ID, simply pass that ID back, NOT removing the state file. This is
64
+ # okay: the state file is cleaned up by WavefrontCli::Event when an
65
+ # event is closed. If it's a name but not an ID, return the ID of the
66
+ # most recent event with the given name.
67
+ # @return [String] the name of the most recent suitable event from the
68
+ # local stack directory.
69
+ #
70
+ def event(id)
71
+ if !id
72
+ pop_event!
73
+ elsif /^\d{13}:.+:\d+/.match?(id)
74
+ id
75
+ else
76
+ pop_event!(id)
77
+ end
78
+ end
79
+
80
+ # List events on the local stack
81
+ #
82
+ def list
83
+ events = dir.children
84
+ abort 'No locally recorded events.' if events.empty?
85
+
86
+ events
87
+ rescue Errno::ENOENT
88
+ raise(WavefrontCli::Exception::SystemError,
89
+ 'There is no event state directory on this host.')
90
+ end
91
+
92
+ # Run a command, stream stderr and stdout to the screen (they
93
+ # get combined -- could be an issue for someone somewhere) and
94
+ # return the command's exit code
95
+ #
96
+ def run_wrapped_cmd(cmd)
97
+ separator = '-' * (TW - 4)
98
+
99
+ puts "Command output follows, on STDERR:\n#{separator}"
100
+ ret = nil
101
+
102
+ Open3.popen2e(cmd) do |_in, out, thr|
103
+ # rubocop:disable Lint/AssignmentInCondition
104
+ while l = out.gets do warn l end
105
+ # rubocop:enable Lint/AssignmentInCondition
106
+ ret = thr.value.exitstatus
107
+ end
108
+
109
+ puts separator
110
+ ret
111
+ end
112
+
113
+ # Write a state file. We put the hosts bound to the event into the file.
114
+ # These aren't currently used by anything in the CLI, but they might be
115
+ # useful to someone, somewhere, someday.
116
+ # @return [Nil]
117
+ #
118
+ def create!(id)
119
+ return unless state_file_needed?
120
+
121
+ fname = dir + id
122
+ File.open(fname, 'w') { |fh| fh.puts(event_file_data) }
123
+ puts "Event state recorded at #{fname}."
124
+ rescue StandardError
125
+ puts 'NOTICE: event was created but state file was not.'
126
+ end
127
+
128
+ # Record event data in the state file. We don't currently use it, but it
129
+ # might be useful to someone someday.
130
+ # @return [String]
131
+ #
132
+ def event_file_data
133
+ { hosts: options[:host],
134
+ description: options[:desc],
135
+ severity: options[:severity],
136
+ tags: options[:evtag] }.to_json
137
+ end
138
+
139
+ def create_dir(state_dir)
140
+ FileUtils.mkdir_p(state_dir)
141
+ raise unless state_dir.exist? &&
142
+ state_dir.directory? &&
143
+ state_dir.writable?
144
+ rescue StandardError
145
+ raise(WavefrontCli::Exception::SystemError,
146
+ "Cannot create writable system directory at '#{state_dir}'.")
147
+ end
148
+
149
+ # Get the last event this script created. If you supply a name, you get
150
+ # the last event with that name. If not, you get the last event. Note the
151
+ # '!': this method (potentially) has side effects.
152
+ # @param name [String] name of event. This is the middle part of the real
153
+ # event name: the only part supplied by the user.
154
+ # @return [Array[timestamp, event_name]]
155
+ #
156
+ def pop_event!(name = nil)
157
+ return false unless dir.exist?
158
+
159
+ list = local_events_with_name(name)
160
+ return false if list.empty?
161
+
162
+ ev_file = list.max
163
+ File.unlink(ev_file)
164
+ ev_file.basename.to_s
165
+ end
166
+
167
+ # Event names are of the form `1609860826095:name:0`
168
+ # @param name [String] the user-specified (middle) portion of an event ID
169
+ # @return [Array[String]] list of matching events
170
+ #
171
+ def local_events_with_name(name = nil)
172
+ return list unless name
173
+
174
+ list.select { |f| f.basename.to_s.split(':')[1] == name }
175
+ end
176
+ end
177
+ end
@@ -7,26 +7,47 @@ module WavefrontCli
7
7
  #
8
8
  class Exception
9
9
  class CredentialError < RuntimeError; end
10
+
10
11
  class MandatoryValue < RuntimeError; end
12
+
11
13
  class ConfigFileNotFound < IOError; end
14
+
12
15
  class FileNotFound < IOError; end
16
+
13
17
  class ImpossibleSearch < RuntimeError; end
18
+
14
19
  class InsufficientData < RuntimeError; end
20
+
15
21
  class InvalidInput < RuntimeError; end
22
+
16
23
  class InvalidQuery < RuntimeError; end
24
+
17
25
  class InvalidValue < RuntimeError; end
26
+
18
27
  class ProfileExists < RuntimeError; end
28
+
19
29
  class ProfileNotFound < RuntimeError; end
30
+
20
31
  class SystemError < RuntimeError; end
32
+
21
33
  class UnhandledCommand < RuntimeError; end
34
+
22
35
  class UnparseableInput < RuntimeError; end
36
+
23
37
  class UnparseableResponse < RuntimeError; end
38
+
24
39
  class UnparseableSearchPattern < RuntimeError; end
40
+
25
41
  class UnsupportedFileFormat < RuntimeError; end
42
+
26
43
  class UnsupportedNoop < RuntimeError; end
44
+
27
45
  class UnsupportedOperation < RuntimeError; end
46
+
28
47
  class UnsupportedOutput < RuntimeError; end
48
+
29
49
  class UserGroupNotFound < RuntimeError; end
50
+
30
51
  class UserError < RuntimeError; end
31
52
  end
32
53
  end
@@ -8,6 +8,7 @@ module WavefrontCli
8
8
  # rubocop:disable Metrics/MethodLength
9
9
  # rubocop:disable Metrics/AbcSize
10
10
  # rubocop:disable Metrics/CyclomaticComplexity
11
+ # rubocop:disable Metrics/PerceivedComplexity
11
12
  def exception_handler(exception)
12
13
  case exception
13
14
  when WavefrontCli::Exception::UnhandledCommand
@@ -26,6 +27,8 @@ module WavefrontCli
26
27
  abort 'Connection timed out.'
27
28
  when Wavefront::Exception::InvalidPermission
28
29
  abort "'#{exception}' is not a valid Wavefront permission."
30
+ when Wavefront::Exception::InvalidTimestamp
31
+ abort "'#{exception}' is not a parseable time."
29
32
  when Wavefront::Exception::InvalidUserGroupId
30
33
  abort "'#{exception}' is not a valid user group ID."
31
34
  when Wavefront::Exception::InvalidAccountId
@@ -80,6 +83,7 @@ module WavefrontCli
80
83
  abort
81
84
  end
82
85
  end
86
+ # rubocop:enable Metrics/PerceivedComplexity
83
87
  # rubocop:enable Metrics/MethodLength
84
88
  # rubocop:enable Metrics/AbcSize
85
89
  # rubocop:enable Metrics/CyclomaticComplexity
@@ -29,7 +29,7 @@ module WavefrontCli
29
29
 
30
30
  def initialize(cli_opts = {})
31
31
  cred_opts = setup_cred_opts(cli_opts)
32
- cli_opts.reject! { |_k, v| v.nil? }
32
+ cli_opts.compact!
33
33
  @opts = DEFAULT_OPTS.merge(load_profile(cred_opts)).merge(cli_opts)
34
34
  rescue WavefrontCli::Exception::ConfigFileNotFound => e
35
35
  abort "Configuration file '#{e}' not found."
@@ -126,7 +126,7 @@ module WavefrontCli
126
126
  end
127
127
  end
128
128
 
129
- def handle_errcode_404(_status)
129
+ def handle_errcode404(_status)
130
130
  'Perhaps metric does not exist for given host.'
131
131
  end
132
132
  end
@@ -23,7 +23,8 @@ module WavefrontCli
23
23
  end
24
24
 
25
25
  alias do_groups do_describe
26
- alias do_permissions do_describe
26
+ alias do_ingestionpolicy do_describe
27
+ alias do_roles do_describe
27
28
 
28
29
  def do_create
29
30
  wf_user_id?(options[:'<id>'])
@@ -95,7 +96,7 @@ module WavefrontCli
95
96
  def extra_validation
96
97
  validate_groups
97
98
  validate_tokens
98
- validate_perms
99
+ validate_ingestion_policy
99
100
  end
100
101
 
101
102
  def validator_exception
@@ -157,15 +158,18 @@ module WavefrontCli
157
158
  !options[:inactive]
158
159
  end
159
160
 
161
+ # rubocop:disable Metrics/AbcSize
160
162
  def user_body
161
163
  { identifier: options[:'<id>'],
162
164
  active: active_account?,
163
- groups: options[:permission],
165
+ ingestionPolicyId: options[:policy],
164
166
  tokens: options[:usertoken],
165
- userGroups: options[:group] }.tap do |b|
167
+ roles: options[:role],
168
+ userGroups: options[:group] }.compact.tap do |b|
166
169
  b[:description] = options[:desc] if options[:desc]
167
170
  end
168
171
  end
172
+ # rubocop:enable Metrics/AbcSize
169
173
 
170
174
  def item_dump_call
171
175
  wf.list.response
@@ -175,12 +179,18 @@ module WavefrontCli
175
179
  options[:group].each { |g| wf_usergroup_id?(g) }
176
180
  end
177
181
 
182
+ def validate_roles
183
+ options[:role].each { |r| wf_role_id?(r) }
184
+ end
185
+
178
186
  def validate_tokens
179
187
  options[:usertoken].each { |t| wf_apitoken_id?(t) }
180
188
  end
181
189
 
182
- def validate_perms
183
- options[:permission].each { |p| wf_permission?(p) }
190
+ def validate_ingestion_policy
191
+ return true unless options[:policy]
192
+
193
+ wf_ingestionpolicy_id?(options[:policy])
184
194
  end
185
195
 
186
196
  def descriptive_name
@@ -7,6 +7,8 @@ module WavefrontCli
7
7
  # CLI coverage for the v2 'settings' API.
8
8
  #
9
9
  class Settings < WavefrontCli::Base
10
+ JOBS = %w[invitePermissions defaultUserGroups].freeze
11
+
10
12
  def do_list_permissions
11
13
  wf.permissions
12
14
  end
@@ -24,10 +26,7 @@ module WavefrontCli
24
26
  k, v = o.split('=', 2)
25
27
  next unless v && !v.empty?
26
28
 
27
- if %w[invitePermissions defaultUserGroups].include?(k)
28
- v = v.include?(',') ? v.split(',') : [v]
29
- end
30
-
29
+ v = v.include?(',') ? v.split(',') : [v] if JOBS.include?(k)
31
30
  a[k] = v
32
31
  end
33
32
 
@@ -50,7 +50,7 @@ class String
50
50
  # undesirable line breaking. This puts it back
51
51
  #
52
52
  def restored
53
- tr('^', ' ').chomp("\n")
53
+ tr('^', ' ').chomp
54
54
  end
55
55
 
56
56
  # Fold long value lines in two-column output. The returned string
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- WF_CLI_VERSION = '8.3.0'
3
+ WF_CLI_VERSION = '8.5.1'
@@ -375,7 +375,7 @@ module WavefrontCli
375
375
  end
376
376
 
377
377
  def format_string_is_all_valid_chars?(fmt)
378
- return true if fmt =~ /^[dmstTv]+$/
378
+ return true if /^[dmstTv]+$/.match?(fmt)
379
379
 
380
380
  raise(WavefrontCli::Exception::UnparseableInput,
381
381
  'unsupported field in format string')
@@ -1,23 +1,8 @@
1
1
  ---
2
- AllCops:
3
- NewCops: enable
2
+ inherit_from:
3
+ - ../.rubocop.yml
4
4
 
5
5
  Metrics/MethodLength:
6
6
  Max: 30
7
-
8
7
  Metrics/AbcSize:
9
8
  Max: 45
10
-
11
- Metrics/ClassLength:
12
- Max: 300
13
-
14
- # Is nothing sacred?
15
- Layout/LineLength:
16
- Max: 80
17
-
18
- Style/IfUnlessModifier:
19
- Enabled: false # because it wants to make lines >80 chars
20
- Style/StringConcatenation:
21
- Enabled: false
22
- Style/OptionalBooleanParameter:
23
- Enabled: false
@@ -22,7 +22,6 @@ unless defined?(CMD)
22
22
  'Content-Type': 'application/json', Accept: 'application/json'
23
23
  }.freeze
24
24
  BAD_TAG = '*BAD_TAG*'
25
- TW = 80
26
25
  HOME_CONFIG = Pathname.new(ENV['HOME']) + '.wavefront'
27
26
  end
28
27
 
@@ -214,9 +214,9 @@ module Minitest
214
214
  private
215
215
 
216
216
  def mk_headers(token = nil)
217
- { 'Accept': /.*/,
217
+ { Accept: /.*/,
218
218
  'Accept-Encoding': /.*/,
219
- 'Authorization': 'Bearer ' + (token || '0123456789-ABCDEF'),
219
+ Authorization: 'Bearer ' + (token || '0123456789-ABCDEF'),
220
220
  'User-Agent': "wavefront-cli-#{WF_CLI_VERSION}" }
221
221
  end
222
222
 
@@ -72,7 +72,7 @@ class WavefrontCommmandBaseTest < MiniTest::Test
72
72
  next if skip_cmd && c.match(skip_cmd)
73
73
 
74
74
  assert_match(/^ \w+/, c)
75
- assert_includes(c, CMN) unless c =~ /--help$/
75
+ assert_includes(c, CMN) unless /--help$/.match?(c)
76
76
  end
77
77
  end
78
78
 
@@ -88,7 +88,7 @@ class WavefrontCommmandBaseTest < MiniTest::Test
88
88
  refute o.end_with?('.')
89
89
  end
90
90
 
91
- assert_equal(1, wf.options.split("\n").select(&:empty?).size)
91
+ assert_equal(1, wf.options.split("\n").count(&:empty?))
92
92
  end
93
93
 
94
94
  def test_opt_row
@@ -28,7 +28,7 @@ class WavefrontCommmandConfigTest < WavefrontCommmandBaseTest
28
28
  refute o.end_with?('.')
29
29
  end
30
30
 
31
- assert_equal(wf.options.split("\n").select(&:empty?).size, 0)
31
+ assert_equal(wf.options.split("\n").count(&:empty?), 0)
32
32
  end
33
33
 
34
34
  def test_commands