zabbix_manager 5.1.6 → 6.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +69 -46
- data/lib/zabbix_manager/classes/actions.rb +3 -11
- data/lib/zabbix_manager/classes/applications.rb +2 -8
- data/lib/zabbix_manager/classes/configurations.rb +4 -9
- data/lib/zabbix_manager/classes/{drules.rb → discovery_rules.rb} +2 -8
- data/lib/zabbix_manager/classes/errors.rb +3 -0
- data/lib/zabbix_manager/classes/events.rb +2 -1
- data/lib/zabbix_manager/classes/graphs.rb +19 -15
- data/lib/zabbix_manager/classes/{hostgroups.rb → host_groups.rb} +5 -11
- data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
- data/lib/zabbix_manager/classes/hosts.rb +135 -89
- data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
- data/lib/zabbix_manager/classes/items.rb +91 -87
- data/lib/zabbix_manager/classes/maintenance.rb +2 -1
- data/lib/zabbix_manager/classes/{mediatypes.rb → media_types.rb} +2 -7
- data/lib/zabbix_manager/classes/problems.rb +11 -40
- data/lib/zabbix_manager/classes/proxies.rb +5 -15
- data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
- data/lib/zabbix_manager/classes/roles.rb +13 -19
- data/lib/zabbix_manager/classes/screens.rb +11 -30
- data/lib/zabbix_manager/classes/scripts.rb +2 -8
- data/lib/zabbix_manager/classes/server.rb +1 -0
- data/lib/zabbix_manager/classes/templates.rb +2 -65
- data/lib/zabbix_manager/classes/triggers.rb +42 -45
- data/lib/zabbix_manager/classes/user_groups.rb +63 -0
- data/lib/zabbix_manager/classes/user_macros.rb +110 -0
- data/lib/zabbix_manager/classes/users.rb +15 -54
- data/lib/zabbix_manager/classes/value_maps.rb +52 -0
- data/lib/zabbix_manager/client.rb +148 -277
- data/lib/zabbix_manager/configuration.rb +107 -0
- data/lib/zabbix_manager/http_transport.rb +145 -97
- data/lib/zabbix_manager/log_sanitizer.rb +9 -19
- data/lib/zabbix_manager/monitoring/device.rb +393 -0
- data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
- data/lib/zabbix_manager/monitoring/line.rb +175 -0
- data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
- data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
- data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
- data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
- data/lib/zabbix_manager/monitoring/traffic_items.rb +132 -0
- data/lib/zabbix_manager/monitoring/validation.rb +92 -0
- data/lib/zabbix_manager/monitoring.rb +216 -642
- data/lib/zabbix_manager/resource.rb +235 -0
- data/lib/zabbix_manager/traffic.rb +238 -0
- data/lib/zabbix_manager/version.rb +1 -1
- data/lib/zabbix_manager.rb +66 -179
- data/zabbix_manager.gemspec +6 -22
- metadata +41 -190
- data/lib/zabbix_manager/basic/basic_alias.rb +0 -42
- data/lib/zabbix_manager/basic/basic_func.rb +0 -55
- data/lib/zabbix_manager/basic/basic_init.rb +0 -50
- data/lib/zabbix_manager/basic/basic_logic.rb +0 -239
- data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -189
- data/lib/zabbix_manager/classes/usergroups.rb +0 -75
- data/lib/zabbix_manager/classes/usermacros.rb +0 -222
- data/lib/zabbix_manager/classes/valuemaps.rb +0 -26
|
@@ -2,38 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "logger"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/logger"
|
|
7
|
+
require "active_support/tagged_logging"
|
|
5
8
|
|
|
6
9
|
class ZabbixManager
|
|
10
|
+
# 持有一个已认证连接,管理 JSON-RPC 请求的完整生命周期。
|
|
7
11
|
class Client
|
|
8
|
-
SUPPORTED_MAJOR_VERSIONS = (4..7)
|
|
12
|
+
SUPPORTED_MAJOR_VERSIONS = (4..7)
|
|
9
13
|
UNAUTHENTICATED_METHODS = %w[apiinfo.version user.login].freeze
|
|
10
|
-
BLANK_NORMALIZED_OPTIONS = %i[api_token username user password http_user http_password].freeze
|
|
11
14
|
UPSERT_MUTEX_STRIPES = 64
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
# @
|
|
18
|
-
# @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@sanitized_options = sanitize_options(@raw_options).freeze
|
|
24
|
-
@options = @sanitized_options
|
|
15
|
+
|
|
16
|
+
attr_reader :options, :logger, :api_version
|
|
17
|
+
|
|
18
|
+
# 校验配置后立即查询服务器版本并完成认证。
|
|
19
|
+
# 初始化失败时关闭连接;成功后的客户端生命周期由调用方负责。
|
|
20
|
+
# @param options [Hash] 显式连接、凭据、日志及锁配置
|
|
21
|
+
# @raise [Invalid] 连接配置无效或相互冲突
|
|
22
|
+
# @raise [ApiError, TransportError, ProtocolError] 版本查询或认证失败
|
|
23
|
+
def initialize(**options)
|
|
24
|
+
@settings = Configuration.parse(options)
|
|
25
|
+
@options = sanitized_options.freeze
|
|
25
26
|
@logger = build_logger
|
|
26
27
|
@id_mutex = Mutex.new
|
|
27
28
|
@next_id = 0
|
|
28
29
|
@upsert_mutexes = Array.new(UPSERT_MUTEX_STRIPES) { Mutex.new }
|
|
29
|
-
|
|
30
|
-
@transport = HttpTransport.new(@raw_options)
|
|
30
|
+
@transport = HttpTransport.new(@settings)
|
|
31
31
|
validate_token_transport!
|
|
32
|
-
@api_version =
|
|
32
|
+
@api_version = api_request(method: "apiinfo.version")
|
|
33
33
|
validate_api_version!
|
|
34
34
|
validate_http_auth_compatibility!
|
|
35
|
-
@credential_type = @
|
|
36
|
-
@auth_token = @
|
|
35
|
+
@credential_type = @settings[:api_token] ? :api_token : :user_session
|
|
36
|
+
@auth_token = @settings[:api_token] || login
|
|
37
|
+
unless @auth_token.is_a?(String) && !@auth_token.empty?
|
|
38
|
+
raise ProtocolError, "Invalid authentication response"
|
|
39
|
+
end
|
|
40
|
+
|
|
37
41
|
log(:info, "client.connected", version: @api_version, credential_type: @credential_type)
|
|
38
42
|
log(:warn, "tls.verification_disabled", url: @transport.safe_url) if insecure_https?
|
|
39
43
|
log(:warn, "token.sent_over_http", url: @transport.safe_url) if insecure_token_transport?
|
|
@@ -42,324 +46,191 @@ class ZabbixManager
|
|
|
42
46
|
raise
|
|
43
47
|
end
|
|
44
48
|
|
|
45
|
-
#
|
|
46
|
-
# @
|
|
47
|
-
# @
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# @api public
|
|
55
|
-
def api_version
|
|
56
|
-
@api_version ||= api_request(method: "apiinfo.version", params: {})
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# 使用用户名和密码创建 Zabbix 会话。
|
|
60
|
-
# @return [String] Zabbix session token
|
|
61
|
-
# @api semipublic
|
|
62
|
-
def auth
|
|
63
|
-
api_request(
|
|
64
|
-
method: "user.login",
|
|
65
|
-
params: {
|
|
66
|
-
login_parameter => username,
|
|
67
|
-
password: @raw_options[:password]
|
|
68
|
-
}
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# 判断是否启用调试日志。
|
|
73
|
-
# @return [Boolean]
|
|
74
|
-
# @api semipublic
|
|
75
|
-
def debug?
|
|
76
|
-
@raw_options[:debug] == true
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# 序列化 JSON-RPC 请求并按版本加入请求体认证。
|
|
80
|
-
# @return [String]
|
|
81
|
-
# @api semipublic
|
|
82
|
-
def message_json(body)
|
|
83
|
-
method = fetch_body_value(body, :method)
|
|
84
|
-
message = {
|
|
85
|
-
method: method,
|
|
86
|
-
params: fetch_body_value(body, :params) || {},
|
|
87
|
-
id: id,
|
|
88
|
-
jsonrpc: "2.0"
|
|
89
|
-
}
|
|
90
|
-
message[:auth] = @auth_token if @auth_token && body_authentication?(method)
|
|
91
|
-
JSON.generate(message)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# 通过持久连接发送请求,并按版本加入 Bearer 令牌。
|
|
95
|
-
# @return [String] raw response body
|
|
96
|
-
# @api semipublic
|
|
97
|
-
def http_request(body)
|
|
98
|
-
method = JSON.parse(body).fetch("method")
|
|
99
|
-
@transport.request(body, bearer_token: bearer_token_for(method))
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# 解析 JSON-RPC 响应并统一转换协议错误。
|
|
103
|
-
# @return [Object]
|
|
104
|
-
# @api semipublic
|
|
105
|
-
def _request(body)
|
|
106
|
-
parsed = JSON.parse(http_request(body))
|
|
107
|
-
raise_api_error(parsed, body) if parsed["error"]
|
|
108
|
-
unless parsed.key?("result")
|
|
109
|
-
raise ApiError.new("Invalid JSON-RPC response: missing result", LogSanitizer.sanitize(parsed))
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
parsed["result"]
|
|
113
|
-
rescue JSON::ParserError => e
|
|
114
|
-
raise ApiError, "Invalid JSON response: #{e.message}"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# 格式化请求摘要并隐藏全部参数值。
|
|
118
|
-
# @return [String]
|
|
119
|
-
# @api semipublic
|
|
120
|
-
def pretty_body(body)
|
|
121
|
-
parsed = JSON.parse(body)
|
|
122
|
-
parsed["params"] = LogSanitizer::REDACTED if parsed.key?("params")
|
|
123
|
-
JSON.pretty_generate(LogSanitizer.sanitize(parsed))
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# 执行 Zabbix JSON-RPC 方法并记录脱敏耗时日志。
|
|
127
|
-
# @return [Object] decoded result value
|
|
128
|
-
# @api public
|
|
129
|
-
def api_request(body)
|
|
130
|
-
method = fetch_body_value(body, :method)
|
|
49
|
+
# 执行一次请求,不重放失败或结果未确认的变更。
|
|
50
|
+
# @param method [String] 原生 API 方法,例如 "history.get"
|
|
51
|
+
# @param params [Hash, Array] 原生 Zabbix 参数,不将参数值写入日志
|
|
52
|
+
# @return [Object] 解码后的 JSON-RPC 结果,保留服务器返回的标量类型
|
|
53
|
+
# @raise [Invalid] 方法名或参数容器无效
|
|
54
|
+
# @raise [ApiError] 服务器明确拒绝请求
|
|
55
|
+
# @raise [TransportError, ProtocolError] 未收到能够确认结果的有效响应
|
|
56
|
+
def api_request(method:, params: {})
|
|
57
|
+
validate_request!(method, params)
|
|
131
58
|
started_at = monotonic_time
|
|
59
|
+
message = request_message(method, params)
|
|
132
60
|
log(:debug, "request.started", method: method)
|
|
133
|
-
|
|
61
|
+
body = @transport.request(JSON.generate(message), bearer_token: bearer_token_for(method))
|
|
62
|
+
result = parse_response(body, message.fetch(:id))
|
|
134
63
|
log(:info, "request.completed", method: method, duration_ms: elapsed_ms(started_at))
|
|
135
64
|
result
|
|
136
|
-
rescue StandardError =>
|
|
137
|
-
log(:warn, "request.failed", method: method, duration_ms: elapsed_ms(started_at), error:
|
|
65
|
+
rescue StandardError => error
|
|
66
|
+
log(:warn, "request.failed", method: method, duration_ms: elapsed_ms(started_at), error: error.class.name)
|
|
138
67
|
raise
|
|
139
68
|
end
|
|
140
|
-
alias manager_request api_request
|
|
141
69
|
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
# @
|
|
70
|
+
# 注销密码认证会话;API token 仅需清理本地状态。
|
|
71
|
+
# 远端注销失败时仍清除本地认证令牌并关闭传输连接。
|
|
72
|
+
# @return [Boolean] 服务器注销结果;API token 模式返回 true
|
|
73
|
+
# @raise [ApiError, TransportError] 远端会话注销失败
|
|
145
74
|
def logout
|
|
146
|
-
|
|
147
|
-
result
|
|
75
|
+
@credential_type == :user_session && @auth_token ? api_request(method: "user.logout", params: []) : true
|
|
148
76
|
ensure
|
|
149
77
|
@auth_token = nil
|
|
150
78
|
close
|
|
151
79
|
end
|
|
152
80
|
|
|
153
|
-
#
|
|
154
|
-
# @return [
|
|
155
|
-
# @api public
|
|
81
|
+
# 关闭连接但不撤销凭据;后续请求按需重新连接。
|
|
82
|
+
# @return [true]
|
|
156
83
|
def close
|
|
157
84
|
@transport.close
|
|
158
85
|
end
|
|
159
86
|
|
|
160
|
-
#
|
|
161
|
-
# @
|
|
162
|
-
# @
|
|
87
|
+
# 延迟构造日志元数据;日志失败不能改变远端写入结果。
|
|
88
|
+
# @param level [Symbol] Ruby Logger 日志级别方法
|
|
89
|
+
# @param event [String] 稳定的事件名称
|
|
90
|
+
# @param data [Hash] 序列化之前经过脱敏的元数据
|
|
91
|
+
# @return [Object, nil] 日志返回值;日志关闭或失败时返回 nil
|
|
163
92
|
def log(level, event, data = {})
|
|
164
93
|
return unless @logger
|
|
165
94
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
95
|
+
@logger.tagged("zabbix_manager") do
|
|
96
|
+
@logger.public_send(level) { JSON.generate(LogSanitizer.sanitize(data.merge(event: event))) }
|
|
97
|
+
end
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
169
100
|
end
|
|
170
101
|
|
|
171
|
-
#
|
|
172
|
-
# @return [String]
|
|
173
|
-
# @api public
|
|
102
|
+
# @return [String] 仅包含脱敏配置的诊断摘要
|
|
174
103
|
def inspect
|
|
175
|
-
"#<#{self.class} options=#{@
|
|
104
|
+
"#<#{self.class} options=#{@options.inspect}>"
|
|
176
105
|
end
|
|
177
106
|
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
# @
|
|
107
|
+
# 应用层锁适配器可将本地串行化扩展到多个工作进程。
|
|
108
|
+
# 锁不可重入;调用方不能在块内再次获取 upsert 锁。
|
|
109
|
+
# @param key [String] 被保护操作的稳定、非秘密身份
|
|
110
|
+
# @yield 需要串行执行的读取、修改和写入操作
|
|
111
|
+
# @return [Object] 块的返回值
|
|
181
112
|
def with_upsert_lock(key, &block)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
coordinator = @raw_options[:upsert_lock]
|
|
113
|
+
@upsert_mutexes[key.hash % UPSERT_MUTEX_STRIPES].synchronize do
|
|
114
|
+
coordinator = @settings[:upsert_lock]
|
|
185
115
|
coordinator ? coordinator.call(key, &block) : yield
|
|
186
116
|
end
|
|
187
117
|
end
|
|
188
118
|
|
|
189
119
|
private
|
|
190
120
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def normalize_options(options)
|
|
195
|
-
unless options.respond_to?(:each_pair)
|
|
196
|
-
raise Invalid, "options must be a hash-like object"
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
normalized = options.to_h.transform_keys(&:to_sym)
|
|
200
|
-
BLANK_NORMALIZED_OPTIONS.each do |key|
|
|
201
|
-
value = normalized[key].presence
|
|
202
|
-
value ? normalized[key] = value : normalized.delete(key)
|
|
203
|
-
end
|
|
204
|
-
normalized
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# 脱敏普通凭据,并清理 URL 和代理地址中的 userinfo 与查询串。
|
|
208
|
-
def sanitize_options(options)
|
|
209
|
-
sanitized = LogSanitizer.sanitize(options)
|
|
121
|
+
def sanitized_options
|
|
122
|
+
LogSanitizer.sanitize(@settings.except(:logger, :upsert_lock)).tap do |safe|
|
|
123
|
+
safe[:uncertain_write_delays].freeze
|
|
210
124
|
%i[url proxy].each do |key|
|
|
211
|
-
|
|
125
|
+
safe[key] = LogSanitizer.sanitize_url(@settings[key]) if @settings[key]
|
|
212
126
|
end
|
|
213
|
-
sanitized
|
|
214
127
|
end
|
|
128
|
+
end
|
|
215
129
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def validate_credentials!
|
|
220
|
-
raise Invalid, "url is required" if @raw_options[:url].nil? || @raw_options[:url].to_s.empty?
|
|
221
|
-
|
|
222
|
-
token_present = @raw_options[:api_token].present?
|
|
223
|
-
username_present = username.present?
|
|
224
|
-
password_present = @raw_options[:password].present?
|
|
225
|
-
if token_present && (username_present || password_present)
|
|
226
|
-
raise Invalid, "api_token cannot be combined with username/password"
|
|
227
|
-
end
|
|
228
|
-
return if token_present || (username_present && password_present)
|
|
130
|
+
def build_logger
|
|
131
|
+
source = @settings[:logger]
|
|
132
|
+
return if source.nil? && @settings[:log_level].nil?
|
|
229
133
|
|
|
230
|
-
|
|
134
|
+
source ||= ActiveSupport::Logger.new($stderr)
|
|
135
|
+
ActiveSupport::TaggedLogging.new(source).tap do |logger|
|
|
136
|
+
logger.level = @settings[:log_level] if @settings.key?(:log_level)
|
|
231
137
|
end
|
|
138
|
+
rescue ArgumentError, TypeError
|
|
139
|
+
raise Invalid, "log_level must be a Ruby Logger severity", cause: nil
|
|
140
|
+
end
|
|
232
141
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
delays = Array(values).map { |value| Float(value) }
|
|
237
|
-
unless delays.any? && delays.length <= 10 && delays.all? { |delay| delay >= 0 } && delays.sum <= 60
|
|
238
|
-
raise Invalid, "uncertain_write_delays must contain 1 to 10 non-negative seconds totaling at most 60"
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
@raw_options[:uncertain_write_delays] = delays.freeze
|
|
242
|
-
rescue ArgumentError, TypeError
|
|
243
|
-
raise Invalid, "uncertain_write_delays must contain numeric seconds"
|
|
142
|
+
def validate_request!(method, params)
|
|
143
|
+
unless method.is_a?(String) && method.match?(/\A[a-z][a-z0-9_]*\.[a-z][a-zA-Z0-9_]*\z/)
|
|
144
|
+
raise Invalid, "method must be a Zabbix API method name"
|
|
244
145
|
end
|
|
146
|
+
raise Invalid, "params must be a Hash or Array" unless params.is_a?(Hash) || params.is_a?(Array)
|
|
147
|
+
end
|
|
245
148
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
lock = @raw_options[:upsert_lock]
|
|
251
|
-
return if lock.nil? || lock.respond_to?(:call)
|
|
252
|
-
|
|
253
|
-
raise Invalid, "upsert_lock must respond to call(key, &block)"
|
|
149
|
+
def request_message(method, params)
|
|
150
|
+
request_id = @id_mutex.synchronize { @next_id += 1 }
|
|
151
|
+
{ method: method, params: params, id: request_id, jsonrpc: "2.0" }.tap do |message|
|
|
152
|
+
message[:auth] = @auth_token if @auth_token && authenticated_method?(method) && api_major_version < 7
|
|
254
153
|
end
|
|
154
|
+
end
|
|
255
155
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
match = @api_version.to_s.match(/\A(\d+)\.(\d+)\.(\d+)\z/)
|
|
261
|
-
supported = match && SUPPORTED_MAJOR_VERSIONS.cover?(match[1].to_i)
|
|
262
|
-
return if supported
|
|
263
|
-
|
|
264
|
-
message = "Zabbix API version #{@api_version.inspect} is not supported"
|
|
265
|
-
if @raw_options[:ignore_version]
|
|
266
|
-
log(:warn, "version.unsupported", version: @api_version)
|
|
267
|
-
else
|
|
268
|
-
raise ApiError, message
|
|
269
|
-
end
|
|
156
|
+
def parse_response(body, request_id)
|
|
157
|
+
response = JSON.parse(body)
|
|
158
|
+
unless response.is_a?(Hash) && response["jsonrpc"] == "2.0" && response["id"] == request_id
|
|
159
|
+
raise ProtocolError, "Invalid JSON-RPC response envelope or request ID"
|
|
270
160
|
end
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
# @return [void]
|
|
274
|
-
# @api private
|
|
275
|
-
def validate_http_auth_compatibility!
|
|
276
|
-
return unless api_major_version >= 7 && @raw_options[:http_user].present?
|
|
277
|
-
|
|
278
|
-
raise Invalid, "HTTP Basic authentication cannot be combined with Zabbix 7 Bearer authentication"
|
|
161
|
+
unless response.key?("result") ^ response.key?("error")
|
|
162
|
+
raise ProtocolError, "Invalid JSON-RPC response: expected exactly one result or error"
|
|
279
163
|
end
|
|
280
164
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return if @raw_options[:allow_insecure_http] == true
|
|
165
|
+
raise_api_error(response) if response.key?("error")
|
|
166
|
+
response.fetch("result")
|
|
167
|
+
rescue JSON::ParserError, TypeError
|
|
168
|
+
raise ProtocolError, "Invalid JSON response", cause: nil
|
|
169
|
+
end
|
|
287
170
|
|
|
288
|
-
|
|
171
|
+
def raise_api_error(response)
|
|
172
|
+
error = response["error"]
|
|
173
|
+
unless error.is_a?(Hash) && error["code"].is_a?(Integer) && error["message"].is_a?(String)
|
|
174
|
+
raise ProtocolError, "Invalid JSON-RPC error response"
|
|
289
175
|
end
|
|
290
176
|
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
@raw_options[:api_token].present? && @transport.uri.scheme == "http"
|
|
296
|
-
end
|
|
177
|
+
# 服务器文本可能回显任意请求值,包括宏及秘密。
|
|
178
|
+
safe_response = response.slice("jsonrpc", "id").merge("error" => error.slice("code"))
|
|
179
|
+
raise ApiError.new("Zabbix API error #{error.fetch("code")}", safe_response)
|
|
180
|
+
end
|
|
297
181
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
182
|
+
def login
|
|
183
|
+
parameter = api_major_version >= 6 ? :username : :user
|
|
184
|
+
api_request(method: "user.login", params: { parameter => @settings[:username], password: @settings[:password] })
|
|
185
|
+
end
|
|
302
186
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
187
|
+
def validate_api_version!
|
|
188
|
+
match = @api_version.is_a?(String) && @api_version.match(/\A(\d+)\.(\d+)\.(\d+)\z/)
|
|
189
|
+
raise ProtocolError, "Invalid API version response" unless match
|
|
190
|
+
return if SUPPORTED_MAJOR_VERSIONS.cover?(match[1].to_i)
|
|
191
|
+
raise ApiError, "Zabbix API version is not supported" unless @settings[:ignore_version]
|
|
307
192
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
@raw_options[:username] || @raw_options[:user]
|
|
311
|
-
end
|
|
193
|
+
log(:warn, "version.unsupported", version: @api_version)
|
|
194
|
+
end
|
|
312
195
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
authenticated_method?(method) && api_major_version < 7
|
|
316
|
-
end
|
|
196
|
+
def validate_http_auth_compatibility!
|
|
197
|
+
return unless api_major_version >= 7 && @settings[:http_user]
|
|
317
198
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
@auth_token if authenticated_method?(method) && api_major_version >= 7
|
|
321
|
-
end
|
|
199
|
+
raise Invalid, "HTTP Basic authentication cannot be combined with Zabbix 7 Bearer authentication"
|
|
200
|
+
end
|
|
322
201
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
!UNAUTHENTICATED_METHODS.include?(method.to_s)
|
|
326
|
-
end
|
|
202
|
+
def validate_token_transport!
|
|
203
|
+
return unless insecure_token_transport? && @settings[:allow_insecure_http] != true
|
|
327
204
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
body[key] || body[key.to_s]
|
|
331
|
-
end
|
|
205
|
+
raise Invalid, "api_token requires HTTPS unless allow_insecure_http is true"
|
|
206
|
+
end
|
|
332
207
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
error = safe_response["error"]
|
|
337
|
-
message = "Zabbix API error #{error["code"]}: #{error["message"]}"
|
|
338
|
-
message = "#{message} (#{error["data"]})" if error["data"]
|
|
339
|
-
raise ApiError.new("#{message}\nRequest:\n#{pretty_body(request_body)}", safe_response)
|
|
340
|
-
end
|
|
208
|
+
def insecure_token_transport?
|
|
209
|
+
@settings[:api_token] && @transport.uri.scheme == "http"
|
|
210
|
+
end
|
|
341
211
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return unless debug?
|
|
212
|
+
def insecure_https?
|
|
213
|
+
@transport.uri.scheme == "https" && @settings[:verify_ssl] != true
|
|
214
|
+
end
|
|
346
215
|
|
|
347
|
-
|
|
348
|
-
|
|
216
|
+
def api_major_version
|
|
217
|
+
@api_version.to_s.split(".", 2).first.to_i
|
|
218
|
+
end
|
|
349
219
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
end
|
|
220
|
+
def bearer_token_for(method)
|
|
221
|
+
@auth_token if authenticated_method?(method) && api_major_version >= 7
|
|
222
|
+
end
|
|
354
223
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
end
|
|
224
|
+
def authenticated_method?(method)
|
|
225
|
+
!UNAUTHENTICATED_METHODS.include?(method)
|
|
226
|
+
end
|
|
359
227
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
228
|
+
def monotonic_time
|
|
229
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def elapsed_ms(started_at)
|
|
233
|
+
((monotonic_time - started_at) * 1000).round(2) if started_at
|
|
234
|
+
end
|
|
364
235
|
end
|
|
365
236
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
# 在首次网络访问之前统一校验连接配置。
|
|
5
|
+
class Configuration
|
|
6
|
+
ENVIRONMENT_KEYS = {
|
|
7
|
+
url: "ZABBIX_URL", api_token: "ZABBIX_API_TOKEN",
|
|
8
|
+
username: "ZABBIX_USERNAME", password: "ZABBIX_PASSWORD"
|
|
9
|
+
}.freeze
|
|
10
|
+
CREDENTIALS = %i[api_token username password http_user http_password].freeze
|
|
11
|
+
BOOLEAN_OPTIONS = %i[verify_ssl allow_insecure_http no_proxy ignore_version].freeze
|
|
12
|
+
OPTIONS = (CREDENTIALS + BOOLEAN_OPTIONS + %i[
|
|
13
|
+
url proxy ca_file timeout request_timeout max_response_bytes
|
|
14
|
+
open_timeout read_timeout write_timeout keep_alive_timeout
|
|
15
|
+
logger log_level upsert_lock uncertain_write_delays
|
|
16
|
+
]).freeze
|
|
17
|
+
DEFAULT_UNCERTAIN_WRITE_DELAYS = [0, 0.25, 1, 2].freeze
|
|
18
|
+
|
|
19
|
+
# 仅从指定环境映射读取连接身份字段。
|
|
20
|
+
# 显式选项优先于环境值,包含用于清除凭据的 nil。
|
|
21
|
+
def self.from_env(env, overrides)
|
|
22
|
+
ENVIRONMENT_KEYS.to_h { |key, name| [key, env[name]] }.merge(overrides)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 返回已校验的配置,不修改调用方持有的值。
|
|
26
|
+
def self.parse(options)
|
|
27
|
+
new(options).to_h
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(options)
|
|
31
|
+
raise Invalid, "options must be a Hash" unless options.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
@options = options.symbolize_keys
|
|
34
|
+
@options.assert_valid_keys(*OPTIONS)
|
|
35
|
+
@options.transform_values! { |value| value.is_a?(String) ? value.dup.freeze : value }
|
|
36
|
+
validate_credentials!
|
|
37
|
+
validate_booleans!
|
|
38
|
+
validate_delays!
|
|
39
|
+
validate_collaborators!
|
|
40
|
+
rescue ArgumentError => error
|
|
41
|
+
raise Invalid, error.message, cause: nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
@options.freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inspect
|
|
49
|
+
"#<#{self.class}>"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def validate_credentials!
|
|
55
|
+
CREDENTIALS.each do |key|
|
|
56
|
+
value = @options[key]
|
|
57
|
+
raise Invalid, "#{key} must be a String" unless value.nil? || value.is_a?(String)
|
|
58
|
+
|
|
59
|
+
@options.delete(key) if value.blank?
|
|
60
|
+
end
|
|
61
|
+
raise Invalid, "url is required" if @options[:url].blank?
|
|
62
|
+
|
|
63
|
+
token = @options[:api_token]
|
|
64
|
+
username = @options[:username]
|
|
65
|
+
password = @options[:password]
|
|
66
|
+
if token && (username || password)
|
|
67
|
+
raise Invalid, "api_token cannot be combined with username/password"
|
|
68
|
+
end
|
|
69
|
+
raise Invalid, "provide api_token or both username and password" unless token || (username && password)
|
|
70
|
+
return unless @options[:http_password] && !@options[:http_user]
|
|
71
|
+
|
|
72
|
+
raise Invalid, "http_password requires http_user"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_booleans!
|
|
76
|
+
BOOLEAN_OPTIONS.each do |key|
|
|
77
|
+
next unless @options.key?(key)
|
|
78
|
+
next if [true, false].include?(@options[key])
|
|
79
|
+
|
|
80
|
+
raise Invalid, "#{key} must be true or false"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_delays!
|
|
85
|
+
values = @options.fetch(:uncertain_write_delays, DEFAULT_UNCERTAIN_WRITE_DELAYS)
|
|
86
|
+
raise Invalid, "uncertain_write_delays must be an Array" unless values.is_a?(Array)
|
|
87
|
+
|
|
88
|
+
delays = values.map { |value| Float(value, exception: false) }
|
|
89
|
+
finite_nonnegative = delays.all? { |value| value && value.finite? && value >= 0 }
|
|
90
|
+
unless (1..10).cover?(delays.size) && finite_nonnegative && delays.sum <= 60
|
|
91
|
+
raise Invalid, "uncertain_write_delays must contain 1 to 10 non-negative seconds totaling at most 60"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@options[:uncertain_write_delays] = delays.freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_collaborators!
|
|
98
|
+
lock = @options[:upsert_lock]
|
|
99
|
+
raise Invalid, "upsert_lock must respond to call(key, &block)" if lock && !lock.respond_to?(:call)
|
|
100
|
+
|
|
101
|
+
logger = @options[:logger]
|
|
102
|
+
return if logger.nil? || %i[debug info warn formatter formatter=].all? { |method| logger.respond_to?(method) }
|
|
103
|
+
|
|
104
|
+
raise Invalid, "logger must support the Ruby Logger interface"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|