wavefront-client 3.5.3 → 3.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +4 -2
- data/README-cli.md +84 -60
- data/bin/wavefront +84 -69
- data/lib/wavefront/alerting.rb +14 -3
- data/lib/wavefront/batch_writer.rb +7 -0
- data/lib/wavefront/cli.rb +15 -17
- data/lib/wavefront/cli/alerts.rb +15 -10
- data/lib/wavefront/cli/batch_write.rb +12 -3
- data/lib/wavefront/cli/events.rb +19 -3
- data/lib/wavefront/cli/sources.rb +22 -12
- data/lib/wavefront/cli/ts.rb +9 -1
- data/lib/wavefront/cli/write.rb +7 -0
- data/lib/wavefront/client.rb +9 -5
- data/lib/wavefront/client/version.rb +1 -1
- data/lib/wavefront/constants.rb +20 -0
- data/lib/wavefront/events.rb +26 -4
- data/lib/wavefront/metadata.rb +23 -6
- data/lib/wavefront/opt_handler.rb +61 -0
- data/spec/cli_spec.rb +584 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/wavefront/alerting_spec.rb +8 -9
- data/spec/wavefront/batch_writer_spec.rb +1 -1
- data/spec/wavefront/cli/alerts_spec.rb +5 -4
- data/spec/wavefront/cli/batch_write_spec.rb +4 -2
- data/spec/wavefront/cli/events_spec.rb +3 -2
- data/spec/wavefront/cli/sources_spec.rb +3 -2
- data/spec/wavefront/cli/write_spec.rb +4 -2
- data/spec/wavefront/cli_spec.rb +11 -43
- data/spec/wavefront/client_spec.rb +2 -2
- data/spec/wavefront/events_spec.rb +1 -1
- data/spec/wavefront/metadata_spec.rb +1 -1
- data/spec/wavefront/mixins_spec.rb +1 -1
- data/spec/wavefront/opt_handler_spec.rb +89 -0
- data/spec/wavefront/resources/conf.yaml +10 -0
- data/spec/wavefront/response_spec.rb +3 -3
- data/spec/wavefront/validators_spec.rb +1 -1
- data/spec/wavefront/writer_spec.rb +1 -1
- metadata +9 -3
- data/.ruby-version +0 -1
data/lib/wavefront/alerting.rb
CHANGED
@@ -17,6 +17,7 @@ See the License for the specific language governing permissions and
|
|
17
17
|
require "wavefront/client/version"
|
18
18
|
require "wavefront/constants"
|
19
19
|
require 'wavefront/mixins'
|
20
|
+
require 'wavefront/validators'
|
20
21
|
require 'rest_client'
|
21
22
|
require 'uri'
|
22
23
|
require 'logger'
|
@@ -28,11 +29,18 @@ module Wavefront
|
|
28
29
|
include Wavefront::Mixins
|
29
30
|
DEFAULT_PATH = '/api/alert/'
|
30
31
|
|
31
|
-
attr_reader :token
|
32
|
+
attr_reader :token, :noop, :verbose, :endpoint
|
32
33
|
|
33
|
-
def initialize(token, debug=false)
|
34
|
+
def initialize(token, host = DEFAULT_HOST, debug=false, options = {})
|
35
|
+
#
|
36
|
+
# Following existing convention, 'host' is the Wavefront API endpoint.
|
37
|
+
#
|
38
|
+
@headers = { :'X-AUTH-TOKEN' => token }
|
39
|
+
@endpoint = host
|
34
40
|
@token = token
|
35
41
|
debug(debug)
|
42
|
+
@noop = options[:noop]
|
43
|
+
@verbose = options[:verbose]
|
36
44
|
end
|
37
45
|
|
38
46
|
def active(options={})
|
@@ -76,7 +84,7 @@ module Wavefront
|
|
76
84
|
end
|
77
85
|
|
78
86
|
def get_alerts(path, options={})
|
79
|
-
options[:host] ||=
|
87
|
+
options[:host] ||= endpoint
|
80
88
|
options[:path] ||= DEFAULT_PATH
|
81
89
|
|
82
90
|
uri = URI::HTTPS.build(
|
@@ -85,6 +93,9 @@ module Wavefront
|
|
85
93
|
query: mk_qs(options),
|
86
94
|
)
|
87
95
|
|
96
|
+
puts "GET #{uri.to_s}" if (verbose || noop)
|
97
|
+
return if noop
|
98
|
+
|
88
99
|
RestClient.get(uri.to_s)
|
89
100
|
end
|
90
101
|
|
@@ -66,12 +66,15 @@ module Wavefront
|
|
66
66
|
rejected: 0,
|
67
67
|
unsent: 0,
|
68
68
|
}
|
69
|
+
|
69
70
|
@opts = setup_options(options, defaults)
|
70
71
|
|
71
72
|
if opts[:tags]
|
72
73
|
valid_tags?(opts[:tags])
|
73
74
|
@global_tags = opts[:tags]
|
74
75
|
end
|
76
|
+
|
77
|
+
debug(options[:debug])
|
75
78
|
end
|
76
79
|
|
77
80
|
def setup_options(user, defaults)
|
@@ -210,5 +213,9 @@ module Wavefront
|
|
210
213
|
puts 'Closing connection to proxy.' if opts[:verbose]
|
211
214
|
sock.close
|
212
215
|
end
|
216
|
+
|
217
|
+
def debug(enabled)
|
218
|
+
RestClient.log = 'stdout' if enabled
|
219
|
+
end
|
213
220
|
end
|
214
221
|
end
|
data/lib/wavefront/cli.rb
CHANGED
@@ -14,16 +14,19 @@ See the License for the specific language governing permissions and
|
|
14
14
|
|
15
15
|
=end
|
16
16
|
|
17
|
-
require '
|
17
|
+
require 'wavefront/constants'
|
18
18
|
|
19
19
|
module Wavefront
|
20
|
+
#
|
21
|
+
# Parent of all the CLI classes.
|
22
|
+
#
|
20
23
|
class Cli
|
21
|
-
|
22
|
-
attr_accessor :options, :arguments
|
24
|
+
attr_accessor :options, :arguments, :noop
|
23
25
|
|
24
26
|
def initialize(options, arguments)
|
25
27
|
@options = options
|
26
28
|
@arguments = arguments
|
29
|
+
@noop = options[:noop]
|
27
30
|
|
28
31
|
if options.include?(:help) && options[:help]
|
29
32
|
puts options
|
@@ -31,22 +34,17 @@ module Wavefront
|
|
31
34
|
end
|
32
35
|
end
|
33
36
|
|
34
|
-
def
|
37
|
+
def validate_opts
|
35
38
|
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
+
# There are things we need to have. If we don't have them,
|
40
|
+
# stop the user right now. Also, if we're in debug mode, print
|
41
|
+
# out a hash of options, which can be very useful when doing
|
42
|
+
# actual debugging. Some classes may have to override this
|
43
|
+
# method. The writer, for instance, uses a proxy and has no
|
44
|
+
# token.
|
39
45
|
#
|
40
|
-
|
41
|
-
|
42
|
-
return unless cf.exist?
|
43
|
-
|
44
|
-
pf = options[:profile] || 'default'
|
45
|
-
puts "using #{pf} profile from #{cf}" if options[:debug]
|
46
|
-
|
47
|
-
IniFile.load(cf)[pf].each_with_object({}) do |(k, v), memo|
|
48
|
-
memo[k.to_sym] = v
|
49
|
-
end
|
46
|
+
raise 'Please supply an API token.' unless options[:token]
|
47
|
+
raise 'Please supply an API endpoint.' unless options[:endpoint]
|
50
48
|
end
|
51
49
|
end
|
52
50
|
end
|
data/lib/wavefront/cli/alerts.rb
CHANGED
@@ -28,9 +28,11 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
28
28
|
raise 'Missing query.' if arguments.empty?
|
29
29
|
query = arguments[0].to_sym
|
30
30
|
|
31
|
-
wfa = Wavefront::Alerting.new(@options[:token]
|
31
|
+
wfa = Wavefront::Alerting.new(@options[:token], @options[:endpoint],
|
32
|
+
@options[:debug], {
|
33
|
+
noop: @options[:noop], verbose: @options[:verbose]})
|
32
34
|
valid_state?(wfa, query)
|
33
|
-
valid_format?(@options[:
|
35
|
+
valid_format?(@options[:alertformat].to_sym)
|
34
36
|
options = { host: @options[:endpoint] }
|
35
37
|
|
36
38
|
if @options[:shared]
|
@@ -43,11 +45,12 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
43
45
|
|
44
46
|
begin
|
45
47
|
result = wfa.send(query, options)
|
46
|
-
rescue
|
48
|
+
rescue => e
|
49
|
+
puts e if @options[:debug]
|
47
50
|
raise 'Unable to execute query.'
|
48
51
|
end
|
49
52
|
|
50
|
-
format_result(result, @options[:
|
53
|
+
format_result(result, @options[:alertformat].to_sym)
|
51
54
|
exit
|
52
55
|
end
|
53
56
|
|
@@ -56,6 +59,8 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
56
59
|
# Call a suitable method to display the output of the API call,
|
57
60
|
# which is JSON.
|
58
61
|
#
|
62
|
+
return if noop
|
63
|
+
|
59
64
|
case format
|
60
65
|
when :ruby
|
61
66
|
pp result
|
@@ -83,11 +88,10 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
83
88
|
# Check the alert type we've been given is valid. There needs to
|
84
89
|
# be a public method in the 'alerting' class for every one.
|
85
90
|
#
|
86
|
-
|
87
|
-
|
88
|
-
unless
|
89
|
-
raise
|
90
|
-
'.'
|
91
|
+
states = %w(active affected_by_maintenance all invalid snoozed)
|
92
|
+
|
93
|
+
unless states.include?(state.to_s)
|
94
|
+
raise "State must be one of: #{states.join(', ')}."
|
91
95
|
end
|
92
96
|
true
|
93
97
|
end
|
@@ -120,7 +124,7 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
120
124
|
else
|
121
125
|
human_line(key, alert[key])
|
122
126
|
end
|
123
|
-
end
|
127
|
+
end.<< ''
|
124
128
|
end
|
125
129
|
end
|
126
130
|
|
@@ -166,6 +170,7 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
166
170
|
#
|
167
171
|
# hanging indent long lines to fit in an 80-column terminal
|
168
172
|
#
|
173
|
+
return unless line
|
169
174
|
line.gsub(/(.{1,#{cols - offset}})(\s+|\Z)/, "\\1\n#{' ' *
|
170
175
|
offset}").rstrip
|
171
176
|
end
|
@@ -12,14 +12,23 @@ class Wavefront::Cli::BatchWrite < Wavefront::Cli
|
|
12
12
|
include Wavefront::Constants
|
13
13
|
include Wavefront::Mixins
|
14
14
|
|
15
|
+
def validate_opts
|
16
|
+
#
|
17
|
+
# Unlike all the API methods, we don't need a token here
|
18
|
+
#
|
19
|
+
abort 'Please supply a proxy endpoint.' unless options[:proxy]
|
20
|
+
end
|
21
|
+
|
15
22
|
def run
|
16
|
-
|
23
|
+
unless valid_format?(options[:infileformat])
|
24
|
+
raise 'Invalid format string.'
|
25
|
+
end
|
17
26
|
|
18
27
|
file = options[:'<file>']
|
19
28
|
setup_opts(options)
|
20
29
|
|
21
|
-
if options.key?(:
|
22
|
-
setup_fmt(options[:
|
30
|
+
if options.key?(:infileformat)
|
31
|
+
setup_fmt(options[:infileformat])
|
23
32
|
else
|
24
33
|
setup_fmt
|
25
34
|
end
|
data/lib/wavefront/cli/events.rb
CHANGED
@@ -27,7 +27,8 @@ require 'socket'
|
|
27
27
|
# in a timeseries API call.
|
28
28
|
#
|
29
29
|
class Wavefront::Cli::Events < Wavefront::Cli
|
30
|
-
attr_accessor :state_dir, :hosts, :hostname, :t_start, :t_end,
|
30
|
+
attr_accessor :state_dir, :hosts, :hostname, :t_start, :t_end,
|
31
|
+
:wf_event
|
31
32
|
|
32
33
|
include Wavefront::Constants
|
33
34
|
include Wavefront::Mixins
|
@@ -43,8 +44,11 @@ class Wavefront::Cli::Events < Wavefront::Cli
|
|
43
44
|
@hosts = prep_hosts(options[:host])
|
44
45
|
@t_start = prep_time(:start)
|
45
46
|
@t_end = prep_time(:end)
|
47
|
+
@noop = options[:noop]
|
46
48
|
|
47
|
-
@wf_event = Wavefront::Events.new(
|
49
|
+
@wf_event = Wavefront::Events.new(
|
50
|
+
options[:token], options[:endpoint], options[:debug],
|
51
|
+
{ verbose: options[:verbose], noop: options[:noop]})
|
48
52
|
|
49
53
|
if options[:create]
|
50
54
|
create_event_handler
|
@@ -77,7 +81,7 @@ class Wavefront::Cli::Events < Wavefront::Cli
|
|
77
81
|
raise 'Cannot delete event.'
|
78
82
|
end
|
79
83
|
|
80
|
-
puts 'Deleted event.'
|
84
|
+
puts 'Deleted event.' unless noop
|
81
85
|
end
|
82
86
|
|
83
87
|
def prep_time(t)
|
@@ -104,6 +108,8 @@ class Wavefront::Cli::Events < Wavefront::Cli
|
|
104
108
|
#
|
105
109
|
output = create_event
|
106
110
|
|
111
|
+
return if noop
|
112
|
+
|
107
113
|
unless options[:end] || options[:instant]
|
108
114
|
create_state_dir
|
109
115
|
create_state_file(output) unless options[:nostate]
|
@@ -211,6 +217,7 @@ class Wavefront::Cli::Events < Wavefront::Cli
|
|
211
217
|
#
|
212
218
|
# Returns an array of [timestamp, event_name]
|
213
219
|
#
|
220
|
+
return false unless state_dir.exist?
|
214
221
|
list = state_dir.children
|
215
222
|
list.select! { |f| f.basename.to_s.split('::').last == name } if name
|
216
223
|
return false if list.length == 0
|
@@ -249,4 +256,13 @@ class Wavefront::Cli::Events < Wavefront::Cli
|
|
249
256
|
|
250
257
|
puts "Event state recorded at #{fname}."
|
251
258
|
end
|
259
|
+
|
260
|
+
def validate_opts
|
261
|
+
#
|
262
|
+
# the 'show' sub-command does not make an API call
|
263
|
+
#
|
264
|
+
return true if options[:show]
|
265
|
+
abort 'Please supply an API token.' unless options[:token]
|
266
|
+
abort 'Please supply an API endpoint.' unless options[:endpoint]
|
267
|
+
end
|
252
268
|
end
|
@@ -7,18 +7,21 @@ require 'pp'
|
|
7
7
|
# Turn CLI input, from the 'sources' command, into metadata API calls
|
8
8
|
#
|
9
9
|
class Wavefront::Cli::Sources < Wavefront::Cli
|
10
|
-
attr_accessor :wf, :out_format, :show_hidden, :show_tags
|
10
|
+
attr_accessor :wf, :out_format, :show_hidden, :show_tags, :verbose
|
11
11
|
|
12
12
|
def setup_wf
|
13
13
|
@wf = Wavefront::Metadata.new(options[:token], options[:endpoint],
|
14
|
-
|
14
|
+
options[:debug],
|
15
|
+
{ verbose: options[:verbose],
|
16
|
+
noop: options[:noop]})
|
15
17
|
end
|
16
18
|
|
17
19
|
def run
|
18
20
|
setup_wf
|
19
|
-
@out_format = options[:
|
21
|
+
@out_format = options[:sourceformat].to_s
|
20
22
|
@show_hidden = options[:all]
|
21
23
|
@show_tags = options[:tags]
|
24
|
+
@verbose = options[:verbose]
|
22
25
|
|
23
26
|
begin
|
24
27
|
if options[:list]
|
@@ -49,17 +52,20 @@ class Wavefront::Cli::Sources < Wavefront::Cli
|
|
49
52
|
|
50
53
|
q = {
|
51
54
|
desc: false,
|
52
|
-
limit: limit,
|
55
|
+
limit: limit.to_i,
|
53
56
|
pattern: pattern
|
54
57
|
}
|
55
58
|
|
56
59
|
q[:lastEntityId] = start if start
|
57
60
|
|
58
|
-
|
61
|
+
res = wf.show_sources(q)
|
62
|
+
return if noop
|
63
|
+
display_data(res, 'list_source')
|
59
64
|
end
|
60
65
|
|
61
66
|
def describe_handler(hosts, desc)
|
62
67
|
hosts = [Socket.gethostname] if hosts.empty?
|
68
|
+
hosts = [hosts] if hosts.is_a?(String)
|
63
69
|
|
64
70
|
hosts.each do |h|
|
65
71
|
if desc.empty?
|
@@ -77,20 +83,22 @@ class Wavefront::Cli::Sources < Wavefront::Cli
|
|
77
83
|
end
|
78
84
|
|
79
85
|
def untag_handler(hosts)
|
80
|
-
hosts ||=
|
86
|
+
hosts ||= Socket.gethostname
|
87
|
+
hosts = [hosts] if hosts.is_a?(String)
|
81
88
|
|
82
89
|
hosts.each do |h|
|
83
|
-
puts "Removing all tags from '#{h}'"
|
90
|
+
puts "Removing all tags from '#{h}'" if verbose
|
84
91
|
wf.delete_tags(h)
|
85
92
|
end
|
86
93
|
end
|
87
94
|
|
88
95
|
def add_tag_handler(hosts, tags)
|
89
|
-
hosts ||=
|
96
|
+
hosts ||= Socket.gethostname
|
97
|
+
hosts = [hosts] if hosts.is_a?(String)
|
90
98
|
|
91
99
|
hosts.each do |h|
|
92
100
|
tags.each do |t|
|
93
|
-
puts "Tagging '#{h}' with '#{t}'"
|
101
|
+
puts "Tagging '#{h}' with '#{t}'" if verbose
|
94
102
|
begin
|
95
103
|
wf.set_tag(h, t)
|
96
104
|
rescue Wavefront::Exception::InvalidString
|
@@ -101,11 +109,12 @@ class Wavefront::Cli::Sources < Wavefront::Cli
|
|
101
109
|
end
|
102
110
|
|
103
111
|
def delete_tag_handler(hosts, tags)
|
104
|
-
hosts ||=
|
112
|
+
hosts ||= Socket.gethostname
|
113
|
+
hosts = [hosts] if hosts.is_a?(String)
|
105
114
|
|
106
115
|
hosts.each do |h|
|
107
116
|
tags.each do |t|
|
108
|
-
puts "Removing tag '#{t}' from '#{h}'"
|
117
|
+
puts "Removing tag '#{t}' from '#{h}'" if verbose
|
109
118
|
wf.delete_tag(h, t)
|
110
119
|
end
|
111
120
|
end
|
@@ -125,6 +134,7 @@ class Wavefront::Cli::Sources < Wavefront::Cli
|
|
125
134
|
end
|
126
135
|
|
127
136
|
def display_data(result, method)
|
137
|
+
return if noop
|
128
138
|
if out_format == 'human'
|
129
139
|
puts public_send('humanize_' + method, result)
|
130
140
|
elsif out_format == 'json'
|
@@ -146,7 +156,7 @@ class Wavefront::Cli::Sources < Wavefront::Cli
|
|
146
156
|
if options[:tagged]
|
147
157
|
skip = false
|
148
158
|
options[:tagged].each do |t|
|
149
|
-
unless s['userTags'].include?(t)
|
159
|
+
unless s.key?('userTags') && s['userTags'].include?(t)
|
150
160
|
skip = true
|
151
161
|
break
|
152
162
|
end
|
data/lib/wavefront/cli/ts.rb
CHANGED
@@ -55,8 +55,16 @@ class Wavefront::Cli::Ts < Wavefront::Cli
|
|
55
55
|
options[:end_time] = Time.at(parse_time(@options[:end]))
|
56
56
|
end
|
57
57
|
|
58
|
-
wave = Wavefront::Client.new(@options[:token], @options[:endpoint], @options[:debug])
|
58
|
+
wave = Wavefront::Client.new(@options[:token], @options[:endpoint], @options[:debug], { noop: @options[:noop], verbose: @options[:verbose]})
|
59
|
+
|
60
|
+
if noop
|
61
|
+
wave.query(query, granularity, options)
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
59
65
|
case options[:response_format]
|
66
|
+
when :json
|
67
|
+
pp wave.query(query, granularity, options)
|
60
68
|
when :raw
|
61
69
|
puts wave.query(query, granularity, options)
|
62
70
|
when :graphite
|
data/lib/wavefront/cli/write.rb
CHANGED
@@ -12,6 +12,13 @@ class Wavefront::Cli::Write < Wavefront::Cli
|
|
12
12
|
include Wavefront::Constants
|
13
13
|
include Wavefront::Mixins
|
14
14
|
|
15
|
+
def validate_opts
|
16
|
+
#
|
17
|
+
# Unlike all the API methods, we don't need a token here
|
18
|
+
#
|
19
|
+
abort 'Please supply a proxy endpoint.' unless options[:proxy]
|
20
|
+
end
|
21
|
+
|
15
22
|
def run
|
16
23
|
valid_value?(options[:'<value>'])
|
17
24
|
valid_metric?(options[:'<metric>'])
|
data/lib/wavefront/client.rb
CHANGED
@@ -28,16 +28,17 @@ module Wavefront
|
|
28
28
|
include Wavefront::Constants
|
29
29
|
DEFAULT_PATH = '/chart/api'
|
30
30
|
|
31
|
-
attr_reader :headers, :base_uri
|
31
|
+
attr_reader :headers, :base_uri, :noop, :verbose
|
32
32
|
|
33
|
-
def initialize(token, host=DEFAULT_HOST, debug=false)
|
33
|
+
def initialize(token, host=DEFAULT_HOST, debug=false, options = {})
|
34
|
+
@verbose = options[:verbose]
|
35
|
+
@noop = options[:noop]
|
34
36
|
@headers = {'X-AUTH-TOKEN' => token}
|
35
37
|
@base_uri = URI::HTTPS.build(:host => host, :path => DEFAULT_PATH)
|
36
38
|
debug(debug)
|
37
39
|
end
|
38
40
|
|
39
41
|
def query(query, granularity='m', options={})
|
40
|
-
|
41
42
|
options[:end_time] ||= Time.now.utc
|
42
43
|
options[:start_time] ||= options[:end_time] - DEFAULT_PERIOD_SECONDS
|
43
44
|
options[:response_format] ||= DEFAULT_FORMAT
|
@@ -48,7 +49,7 @@ module Wavefront
|
|
48
49
|
[ options[:start_time], options[:end_time] ].each { |o| raise Wavefront::Exception::InvalidTimeFormat unless o.is_a?(Time) }
|
49
50
|
raise Wavefront::Exception::InvalidGranularity unless GRANULARITIES.include?(granularity)
|
50
51
|
raise Wavefront::Exception::InvalidResponseFormat unless FORMATS.include?(options[:response_format])
|
51
|
-
raise InvalidPrefixLength unless options[:prefix_length].is_a?(
|
52
|
+
raise InvalidPrefixLength unless options[:prefix_length].is_a?(Integer)
|
52
53
|
|
53
54
|
args = {:params =>
|
54
55
|
{:q => query, :g => granularity, :n => 'Unknown',
|
@@ -62,11 +63,14 @@ module Wavefront
|
|
62
63
|
args[:params].merge!(options[:passthru])
|
63
64
|
end
|
64
65
|
|
66
|
+
puts "GET #{@base_uri.to_s}\nPARAMS #{args.to_s}" if (verbose || noop)
|
67
|
+
|
68
|
+
return if noop
|
69
|
+
|
65
70
|
response = RestClient.get @base_uri.to_s, args
|
66
71
|
|
67
72
|
klass = Object.const_get('Wavefront').const_get('Response').const_get(options[:response_format].to_s.capitalize)
|
68
73
|
return klass.new(response, options)
|
69
|
-
|
70
74
|
end
|
71
75
|
|
72
76
|
private
|