wavefront-cli 8.2.0 → 8.5.0

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.
@@ -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
+ id =~ /^\d{13}:.+/ ? 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 id =~ /^\d{13}:.+:\d+/
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
@@ -32,6 +32,10 @@ module WavefrontCli
32
32
  version_info(raw).sort_by { |p| Gem::Version.new(p[:version]) }.reverse
33
33
  end
34
34
 
35
+ def do_shutdown
36
+ wf.shutdown(options[:'<id>'])
37
+ end
38
+
35
39
  def version_info(raw)
36
40
  raw.response.items.map do |i|
37
41
  { id: i.id, version: i.version, name: i.name }
@@ -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
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- WF_CLI_VERSION = '8.2.0'
3
+ WF_CLI_VERSION = '8.5.0'
@@ -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
 
@@ -22,6 +22,18 @@ class WavefrontCliHelpTest < MiniTest::Test
22
22
  assert_match(/^ \w+ --help$/, e.message)
23
23
  end
24
24
 
25
+ def test_commands_no_args
26
+ SupportedCommands.new.all.each do |cmd|
27
+ _test_command_no_args(cmd)
28
+ end
29
+ end
30
+
31
+ def _test_command_no_args(cmd)
32
+ capture_io { WavefrontCliController.new([cmd]) }
33
+ rescue SystemExit => e
34
+ assert e.message.end_with?("wf #{cmd} --help")
35
+ end
36
+
25
37
  def test_version
26
38
  capture_io { WavefrontCliController.new(%w[--version]) }
27
39
  rescue SystemExit => e
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'tmpdir'
5
5
  require_relative '../support/command_base'
6
+ require_relative '../test_mixins/tag'
6
7
  require_relative '../../lib/wavefront-cli/event'
7
8
  require 'wavefront-sdk/support/mixins'
8
9
 
@@ -15,12 +16,11 @@ class EventEndToEndTest < EndToEndTest
15
16
  attr_reader :test_state_dir
16
17
 
17
18
  include Wavefront::Mixins
18
- include WavefrontCliTest::Describe
19
- include WavefrontCliTest::Delete
20
- # Ones above work, ones below don't
19
+ # include WavefrontCliTest::Describe
20
+ # include WavefrontCliTest::Delete
21
21
  # include WavefrontCliTest::Search
22
- # include WavefrontCliTest::Set
23
- # include WavefrontCliTest::Tags
22
+ # #include WavefrontCliTest::Set
23
+ # include WavefrontCliTest::Tag
24
24
 
25
25
  def before_setup
26
26
  @test_state_dir = Pathname.new(Dir.mktmpdir)
@@ -31,11 +31,6 @@ class EventEndToEndTest < EndToEndTest
31
31
  FileUtils.rm_r(test_state_dir)
32
32
  end
33
33
 
34
- def cmd_instance
35
- cmd_class.new(event_state_dir: TEST_EVENT_DIR)
36
- puts cmd_class
37
- end
38
-
39
34
  def test_list_no_options
40
35
  str = '/api/v2/event\?' \
41
36
  'earliestStartTimeEpochMillis=\d{13}+&' \
@@ -65,6 +60,22 @@ class EventEndToEndTest < EndToEndTest
65
60
  end
66
61
  end
67
62
 
63
+ def test_show_with_no_local_events
64
+ assert_exits_with('No locally recorded events.', 'show')
65
+ end
66
+
67
+ def test_show
68
+ setup_test_state_dir
69
+
70
+ out, err = capture_io do
71
+ assert_raises(SystemExit) { wf.new("event show -c #{CF}".split) }
72
+ end
73
+
74
+ assert_empty(err)
75
+ assert_equal("1568133440530:ev3:0\n1568133440520:ev2:0\n" \
76
+ "1568133440515:ev1:1\n1568133440510:ev1:0\n", out)
77
+ end
78
+
68
79
  def test_create
69
80
  mock_id = "#{start_time}:#{event_name}:1"
70
81
  state_file = state_dir + mock_id
@@ -100,8 +111,8 @@ class EventEndToEndTest < EndToEndTest
100
111
  refute state_file.exist?
101
112
 
102
113
  out, err = capture_io do
103
- assert_cmd_posts('create -d reason -H host1 -H host2 -g ' \
104
- "mytag #{event_name}",
114
+ assert_cmd_posts('create -d reason -H host1 -H host2 -g mytag ' \
115
+ "#{event_name}",
105
116
  '/api/v2/event',
106
117
  { name: event_name,
107
118
  startTime: a_ms_timestamp,
@@ -148,15 +159,6 @@ class EventEndToEndTest < EndToEndTest
148
159
  assert_match(/\ntags tag1\n tag2\n/, out)
149
160
  end
150
161
 
151
- def test_close_named_event
152
- quietly do
153
- assert_cmd_posts('close 1568133440520:ev2:0',
154
- '/api/v2/event/1568133440520:ev2:0/close')
155
- end
156
-
157
- assert_abort_on_missing_creds("close #{id}")
158
- end
159
-
160
162
  def test_close_with_no_local_events
161
163
  quietly { assert_cmd_posts("close #{id}", "/api/v2/event/#{id}/close") }
162
164
  assert_exits_with('No locally recorded events.', 'close')
@@ -212,26 +214,47 @@ class EventEndToEndTest < EndToEndTest
212
214
  end
213
215
  end
214
216
 
215
- def test_show
216
- setup_test_state_dir
217
+ def test_window_start
218
+ wfse = WavefrontCli::Event.new(start: wall_time[0], end: wall_time[1])
219
+ assert_kind_of(Numeric, wfse.window_start)
220
+ assert_equal(epoch_ms_time[0], wfse.window_start)
221
+ end
217
222
 
218
- out, err = capture_io do
219
- assert_raises(SystemExit) { wf.new("event show -c #{CF}".split) }
220
- end
223
+ def test_window_end
224
+ wfse = WavefrontCli::Event.new(start: wall_time[0], end: wall_time[1])
225
+ assert_kind_of(Numeric, wfse.window_end)
226
+ assert_equal(epoch_ms_time[1], wfse.window_end)
227
+ end
221
228
 
222
- assert_empty(err)
223
- assert_equal("1568133440530:ev3:0\n1568133440520:ev2:0\n" \
224
- "1568133440515:ev1:1\n1568133440510:ev1:0\n", out)
229
+ def test_list_args_defaults
230
+ wfe = WavefrontCli::Event.new({})
231
+ x = wfe.list_args
232
+ assert_instance_of(Array, x)
233
+ assert_equal(4, x.size)
234
+ assert_in_delta(((Time.now - 600).to_i * 1000), x[0], 1000)
235
+ assert_in_delta((Time.now.to_i * 1000), x[1], 1000)
236
+ assert_equal(100, x[2])
237
+ assert_nil(x[3])
238
+ end
239
+
240
+ def test_list_args_options
241
+ wfse = WavefrontCli::Event.new(limit: 55,
242
+ start: wall_time[0],
243
+ cursor: id,
244
+ end: wall_time[1])
245
+ x = wfse.list_args
246
+ assert_instance_of(Array, x)
247
+ assert_equal(4, x.size)
248
+ assert_equal(epoch_ms_time[0], x[0])
249
+ assert_equal(epoch_ms_time[1], x[1])
250
+ assert_equal(55, x[2])
251
+ assert_equal(id, x[3])
225
252
  end
226
253
 
227
254
  private
228
255
 
229
256
  def id
230
- '1481553823153:testev'
231
- end
232
-
233
- def event_name
234
- 'test_event'
257
+ '1481553823153:testev:0'
235
258
  end
236
259
 
237
260
  def invalid_id
@@ -242,18 +265,24 @@ class EventEndToEndTest < EndToEndTest
242
265
  'event'
243
266
  end
244
267
 
268
+ def event_name
269
+ 'test_event'
270
+ end
271
+
245
272
  def start_time
246
273
  1_481_553_823_153
247
274
  end
248
275
 
249
- def import_fields
250
- %i[method title creatorId triggers template]
276
+ def epoch_ms_time
277
+ wall_time.map { |t| (t.to_i * 1000) }
251
278
  end
252
279
 
253
280
  def state_dir
254
281
  test_state_dir + (Etc.getlogin || 'notty')
255
282
  end
256
283
 
284
+ # Puts some test events in the state directory
285
+ #
257
286
  def setup_test_state_dir
258
287
  FileUtils.mkdir_p(state_dir)
259
288
 
@@ -285,79 +314,10 @@ class EventEndToEndTest < EndToEndTest
285
314
  "https://#{perm[:endpoint]}/api/v2/event/#{mock_id}/close")
286
315
  .with(body: 'null')
287
316
  end
288
- end
289
-
290
- # Unit tests for class methods
291
- #
292
- class EventMethodTests < Minitest::Test
293
- attr_reader :wf, :wfse
294
-
295
- def setup
296
- @wf = WavefrontCli::Event.new({})
297
- @wfse = WavefrontCli::Event.new(start: wall_time[0],
298
- end: wall_time[1],
299
- limit: 55,
300
- cursor: '1481553823153:testev')
301
- end
302
-
303
- def test_create_dir_ok
304
- base = Pathname.new(Dir.mktmpdir)
305
- dir = base + 'testdir'
306
- refute dir.exist?
307
- wf.create_dir(dir)
308
- assert dir.exist?
309
- dir.unlink
310
- base.unlink
311
- end
312
-
313
- def test_create_dir_fail
314
- spy = Spy.on(FileUtils, :mkdir_p).and_return(false)
315
-
316
- assert_raises(WavefrontCli::Exception::SystemError) do
317
- wf.create_dir(Pathname.new('/any/old/directory'))
318
- end
319
-
320
- assert spy.has_been_called?
321
- spy.unhook
322
- end
323
-
324
- def test_list_args_defaults
325
- x = wf.list_args
326
- assert_instance_of(Array, x)
327
- assert_equal(4, x.size)
328
- assert_in_delta(((Time.now - 600).to_i * 1000), x[0], 1000)
329
- assert_in_delta((Time.now.to_i * 1000), x[1], 1000)
330
- assert_equal(100, x[2])
331
- assert_nil(x[3])
332
- end
333
-
334
- def test_list_args_options
335
- x = wfse.list_args
336
- assert_instance_of(Array, x)
337
- assert_equal(4, x.size)
338
- assert_equal(epoch_ms_time[0], x[0])
339
- assert_equal(epoch_ms_time[1], x[1])
340
- assert_equal(55, x[2])
341
- assert_equal('1481553823153:testev', x[3])
342
- end
343
-
344
- def test_window_start
345
- assert_kind_of(Numeric, wf.window_start)
346
- assert_equal(epoch_ms_time[0], wfse.window_start)
347
- end
348
-
349
- def test_window_end
350
- assert_kind_of(Numeric, wf.window_end)
351
- assert_equal(epoch_ms_time[1], wfse.window_end)
352
- end
353
317
 
354
- private
355
-
356
- def wall_time
357
- [Time.at(1_568_112_000), Time.at(1_568_112_999)]
358
- end
359
-
360
- def epoch_ms_time
361
- wall_time.map { |t| (t.to_i * 1000) }
318
+ # Event searching uses a cursor, not an offset
319
+ #
320
+ def cannot_handle_offsets
321
+ true
362
322
  end
363
323
  end