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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +37 -0
- data/.github/workflows/test.yml +23 -0
- data/.rubocop.yml +10 -6
- data/HISTORY.md +21 -1
- data/lib/wavefront-cli/base.rb +3 -0
- data/lib/wavefront-cli/commands/.rubocop.yml +2 -13
- data/lib/wavefront-cli/commands/event.rb +8 -6
- data/lib/wavefront-cli/commands/serviceaccount.rb +6 -4
- data/lib/wavefront-cli/controller.rb +9 -0
- data/lib/wavefront-cli/display/base.rb +3 -2
- data/lib/wavefront-cli/display/printer/sparkline.rb +1 -1
- data/lib/wavefront-cli/display/serviceaccount.rb +12 -4
- data/lib/wavefront-cli/event.rb +50 -166
- data/lib/wavefront-cli/event_store.rb +177 -0
- data/lib/wavefront-cli/exception.rb +21 -0
- data/lib/wavefront-cli/exception_handler.rb +4 -0
- data/lib/wavefront-cli/opt_handler.rb +1 -1
- data/lib/wavefront-cli/query.rb +1 -1
- data/lib/wavefront-cli/serviceaccount.rb +16 -6
- data/lib/wavefront-cli/settings.rb +3 -4
- data/lib/wavefront-cli/stdlib/string.rb +1 -1
- data/lib/wavefront-cli/version.rb +1 -1
- data/lib/wavefront-cli/write.rb +1 -1
- data/spec/.rubocop.yml +2 -17
- data/spec/spec_helper.rb +0 -1
- data/spec/support/minitest_assertions.rb +2 -2
- data/spec/wavefront-cli/commands/base_spec.rb +2 -2
- data/spec/wavefront-cli/commands/config_spec.rb +1 -1
- data/spec/wavefront-cli/controller_spec.rb +14 -0
- data/spec/wavefront-cli/event_spec.rb +69 -109
- data/spec/wavefront-cli/event_store_spec.rb +186 -0
- data/spec/wavefront-cli/opt_handler_spec.rb +3 -3
- data/spec/wavefront-cli/serviceaccount_spec.rb +53 -21
- data/wavefront-cli.gemspec +5 -2
- metadata +62 -10
- 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.
|
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."
|
data/lib/wavefront-cli/query.rb
CHANGED
@@ -23,7 +23,8 @@ module WavefrontCli
|
|
23
23
|
end
|
24
24
|
|
25
25
|
alias do_groups do_describe
|
26
|
-
alias
|
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
|
-
|
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
|
-
|
165
|
+
ingestionPolicyId: options[:policy],
|
164
166
|
tokens: options[:usertoken],
|
165
|
-
|
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
|
183
|
-
options[:
|
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
|
-
|
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
|
|
data/lib/wavefront-cli/write.rb
CHANGED
@@ -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
|
378
|
+
return true if /^[dmstTv]+$/.match?(fmt)
|
379
379
|
|
380
380
|
raise(WavefrontCli::Exception::UnparseableInput,
|
381
381
|
'unsupported field in format string')
|
data/spec/.rubocop.yml
CHANGED
@@ -1,23 +1,8 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -214,9 +214,9 @@ module Minitest
|
|
214
214
|
private
|
215
215
|
|
216
216
|
def mk_headers(token = nil)
|
217
|
-
{
|
217
|
+
{ Accept: /.*/,
|
218
218
|
'Accept-Encoding': /.*/,
|
219
|
-
|
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
|
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").
|
91
|
+
assert_equal(1, wf.options.split("\n").count(&:empty?))
|
92
92
|
end
|
93
93
|
|
94
94
|
def test_opt_row
|