zabbix_manager 5.1.6 → 6.0.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/README.md +73 -45
  4. data/lib/zabbix_manager/classes/actions.rb +3 -11
  5. data/lib/zabbix_manager/classes/applications.rb +2 -8
  6. data/lib/zabbix_manager/classes/configurations.rb +4 -9
  7. data/lib/zabbix_manager/classes/{drules.rb → discovery_rules.rb} +2 -8
  8. data/lib/zabbix_manager/classes/errors.rb +3 -0
  9. data/lib/zabbix_manager/classes/events.rb +2 -1
  10. data/lib/zabbix_manager/classes/graphs.rb +19 -15
  11. data/lib/zabbix_manager/classes/{hostgroups.rb → host_groups.rb} +5 -11
  12. data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
  13. data/lib/zabbix_manager/classes/hosts.rb +139 -89
  14. data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
  15. data/lib/zabbix_manager/classes/items.rb +139 -92
  16. data/lib/zabbix_manager/classes/maintenance.rb +2 -1
  17. data/lib/zabbix_manager/classes/{mediatypes.rb → media_types.rb} +2 -7
  18. data/lib/zabbix_manager/classes/problems.rb +11 -40
  19. data/lib/zabbix_manager/classes/proxies.rb +7 -15
  20. data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
  21. data/lib/zabbix_manager/classes/roles.rb +13 -19
  22. data/lib/zabbix_manager/classes/screens.rb +11 -30
  23. data/lib/zabbix_manager/classes/scripts.rb +2 -8
  24. data/lib/zabbix_manager/classes/server.rb +1 -0
  25. data/lib/zabbix_manager/classes/templates.rb +2 -65
  26. data/lib/zabbix_manager/classes/triggers.rb +62 -53
  27. data/lib/zabbix_manager/classes/user_groups.rb +63 -0
  28. data/lib/zabbix_manager/classes/user_macros.rb +111 -0
  29. data/lib/zabbix_manager/classes/users.rb +15 -54
  30. data/lib/zabbix_manager/classes/value_maps.rb +52 -0
  31. data/lib/zabbix_manager/client.rb +162 -279
  32. data/lib/zabbix_manager/configuration.rb +115 -0
  33. data/lib/zabbix_manager/http_transport.rb +145 -97
  34. data/lib/zabbix_manager/log_sanitizer.rb +18 -29
  35. data/lib/zabbix_manager/monitoring/device.rb +394 -0
  36. data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
  37. data/lib/zabbix_manager/monitoring/line.rb +176 -0
  38. data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
  39. data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
  40. data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
  41. data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
  42. data/lib/zabbix_manager/monitoring/traffic_items.rb +133 -0
  43. data/lib/zabbix_manager/monitoring/validation.rb +92 -0
  44. data/lib/zabbix_manager/monitoring.rb +216 -642
  45. data/lib/zabbix_manager/resource.rb +262 -0
  46. data/lib/zabbix_manager/traffic.rb +239 -0
  47. data/lib/zabbix_manager/version.rb +1 -1
  48. data/lib/zabbix_manager.rb +66 -179
  49. data/zabbix_manager.gemspec +6 -22
  50. metadata +41 -190
  51. data/lib/zabbix_manager/basic/basic_alias.rb +0 -42
  52. data/lib/zabbix_manager/basic/basic_func.rb +0 -55
  53. data/lib/zabbix_manager/basic/basic_init.rb +0 -50
  54. data/lib/zabbix_manager/basic/basic_logic.rb +0 -239
  55. data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -189
  56. data/lib/zabbix_manager/classes/usergroups.rb +0 -75
  57. data/lib/zabbix_manager/classes/usermacros.rb +0 -222
  58. data/lib/zabbix_manager/classes/valuemaps.rb +0 -26
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZabbixManager
4
+ # 原生值映射资源;所属范围与映射规则由调用方提供。
5
+ class ValueMaps < Resource
6
+ # 返回 Zabbix API 中值映射对象的方法名前缀。
7
+ #
8
+ # @return [String] 值映射对象的方法名前缀
9
+ def method_name
10
+ "valuemap"
11
+ end
12
+
13
+ # 返回用于唯一识别值映射的业务字段名。
14
+ #
15
+ # @return [String] 值映射对象的业务标识字段名
16
+ def identify
17
+ "name"
18
+ end
19
+
20
+ # 5.4 起值映射属于主机或模板;旧版仍按全局名称定位。
21
+ # @return [Hash] 带版本边界的精确身份条件
22
+ def identity_filter(data)
23
+ attributes = data.deep_symbolize_keys
24
+ identity = super(attributes)
25
+ if host_scoped?
26
+ raise Invalid, "hostid is required for a host-scoped value map" unless attributes.key?(:hostid)
27
+
28
+ identity[:hostid] = normalized_ids([attributes[:hostid]]).first
29
+ end
30
+ identity
31
+ end
32
+
33
+ # 名称查询始终经过完整身份检查,避免跨主机合法重名造成误选。
34
+ # @return [Integer, nil] 唯一值映射 ID
35
+ def get_id(data)
36
+ super(identity_filter(data))
37
+ end
38
+
39
+ # hostid 用于身份查询和创建,不能用于移动已有值映射。
40
+ # @return [Integer] 已确认的值映射 ID
41
+ def update(data)
42
+ attributes = data.deep_symbolize_keys
43
+ super(host_scoped? ? attributes.except(:hostid) : attributes)
44
+ end
45
+
46
+ private
47
+
48
+ def host_scoped?
49
+ Gem::Version.new(@client.api_version) >= Gem::Version.new("5.4")
50
+ end
51
+ end
52
+ end
@@ -2,38 +2,45 @@
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).freeze
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
- DEFAULT_UNCERTAIN_WRITE_DELAYS = [0, 0.25, 1, 2].freeze
13
-
14
- attr_reader :options, :sanitized_options, :logger
15
-
16
- # 构建按服务端版本选择认证方式并复用 HTTP 会话的客户端。
17
- # @return [Client]
18
- # @api public
19
- def initialize(options = {})
20
- @raw_options = normalize_options(options)
21
- validate_credentials!
22
- validate_uncertain_write_delays!
23
- @sanitized_options = sanitize_options(@raw_options).freeze
24
- @options = @sanitized_options
15
+ LOG_LEVELS = { debug: Logger::DEBUG, info: Logger::INFO, warn: Logger::WARN,
16
+ error: Logger::ERROR, fatal: Logger::FATAL, unknown: Logger::UNKNOWN }.freeze
17
+ private_constant :LOG_LEVELS
18
+
19
+ attr_reader :options, :logger, :api_version
20
+
21
+ # 校验配置后立即查询服务器版本并完成认证。
22
+ # 初始化失败时关闭连接;成功后的客户端生命周期由调用方负责。
23
+ # @param options [Hash] 显式连接、凭据、日志及锁配置
24
+ # @raise [Invalid] 连接配置无效或相互冲突
25
+ # @raise [ApiError, TransportError, ProtocolError] 版本查询或认证失败
26
+ def initialize(**options)
27
+ @settings = Configuration.parse(options)
28
+ @options = sanitized_options.freeze
25
29
  @logger = build_logger
26
30
  @id_mutex = Mutex.new
27
31
  @next_id = 0
28
32
  @upsert_mutexes = Array.new(UPSERT_MUTEX_STRIPES) { Mutex.new }
29
- validate_upsert_lock!
30
- @transport = HttpTransport.new(@raw_options)
33
+ @transport = HttpTransport.new(@settings)
31
34
  validate_token_transport!
32
- @api_version = api_version
35
+ @api_version = api_request(method: "apiinfo.version")
33
36
  validate_api_version!
34
37
  validate_http_auth_compatibility!
35
- @credential_type = @raw_options[:api_token] ? :api_token : :user_session
36
- @auth_token = @raw_options[:api_token] || auth
38
+ @credential_type = @settings[:api_token] ? :api_token : :user_session
39
+ @auth_token = @settings[:api_token] || login
40
+ unless @auth_token.is_a?(String) && !@auth_token.empty?
41
+ raise ProtocolError, "Invalid authentication response"
42
+ end
43
+
37
44
  log(:info, "client.connected", version: @api_version, credential_type: @credential_type)
38
45
  log(:warn, "tls.verification_disabled", url: @transport.safe_url) if insecure_https?
39
46
  log(:warn, "token.sent_over_http", url: @transport.safe_url) if insecure_token_transport?
@@ -42,324 +49,200 @@ class ZabbixManager
42
49
  raise
43
50
  end
44
51
 
45
- # 分配线程安全的 JSON-RPC 请求编号。
46
- # @return [Integer]
47
- # @api semipublic
48
- def id
49
- @id_mutex.synchronize { @next_id += 1 }
50
- end
51
-
52
- # 返回首次查询后缓存的远端 Zabbix API 版本。
53
- # @return [String]
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)
52
+ # 执行一次请求,不重放失败或结果未确认的变更。
53
+ # @param method [String] 原生 API 方法,例如 "history.get"
54
+ # @param params [Hash, Array] 原生 Zabbix 参数,不将参数值写入日志
55
+ # @return [Object] 解码后的 JSON-RPC 结果,保留服务器返回的标量类型
56
+ # @raise [Invalid] 方法名或参数容器无效
57
+ # @raise [ApiError] 服务器明确拒绝请求
58
+ # @raise [TransportError, ProtocolError] 未收到能够确认结果的有效响应
59
+ def api_request(method:, params: {})
60
+ method = method.dup.freeze if method.is_a?(String)
61
+ validate_request!(method, params)
131
62
  started_at = monotonic_time
132
- log(:debug, "request.started", method: method)
133
- result = _request(message_json(body))
134
- log(:info, "request.completed", method: method, duration_ms: elapsed_ms(started_at))
63
+ message = request_message(method, params)
64
+ log(:debug, "request.started", method: method, request_id: message[:id])
65
+ body = @transport.request(JSON.generate(message), bearer_token: bearer_token_for(method))
66
+ result = parse_response(body, message.fetch(:id))
67
+ log(:info, "request.completed", method: method, request_id: message[:id], duration_ms: elapsed_ms(started_at))
135
68
  result
136
- rescue StandardError => e
137
- log(:warn, "request.failed", method: method, duration_ms: elapsed_ms(started_at), error: e.class.name)
69
+ rescue StandardError => error
70
+ if started_at
71
+ log(:warn, "request.failed", method: method, request_id: message&.fetch(:id),
72
+ duration_ms: elapsed_ms(started_at), error: error.class.name)
73
+ end
138
74
  raise
139
75
  end
140
- alias manager_request api_request
141
76
 
142
- # 注销用户名会话并关闭持久连接;API 令牌无需远端注销。
143
- # @return [Object] logout result
144
- # @api public
77
+ # 注销密码认证会话;API token 仅需清理本地状态。
78
+ # 远端注销失败时仍清除本地认证令牌并关闭传输连接。
79
+ # @return [Boolean] 服务器注销结果;API token 模式返回 true
80
+ # @raise [ApiError, TransportError] 远端会话注销失败
145
81
  def logout
146
- result = @credential_type == :user_session && @auth_token ? api_request(method: "user.logout", params: []) : true
147
- result
82
+ @credential_type == :user_session && @auth_token ? api_request(method: "user.logout", params: []) : true
148
83
  ensure
149
84
  @auth_token = nil
150
85
  close
151
86
  end
152
87
 
153
- # 关闭持久 HTTP 连接。
154
- # @return [Object]
155
- # @api public
88
+ # 关闭连接但不撤销凭据;后续请求按需重新连接。
89
+ # @return [true]
156
90
  def close
157
91
  @transport.close
158
92
  end
159
93
 
160
- # 输出结构化且已脱敏的客户端事件。
161
- # @return [void]
162
- # @api semipublic
94
+ # 延迟构造日志元数据;日志失败不能改变远端写入结果。
95
+ # @param level [Symbol] Ruby Logger 日志级别方法
96
+ # @param event [String] 稳定的事件名称
97
+ # @param data [Hash] 序列化之前经过脱敏的元数据
98
+ # @return [Object, nil] 日志返回值;日志关闭或失败时返回 nil
163
99
  def log(level, event, data = {})
164
100
  return unless @logger
165
101
 
166
- sanitized = LogSanitizer.sanitize(data)
167
- details = sanitized.map { |key, value| "#{key}=#{value.inspect}" }.join(" ")
168
- @logger.public_send(level, ["[zabbix_manager]", event, details].reject(&:empty?).join(" "))
102
+ severity = LOG_LEVELS[level]
103
+ return if severity && @logger.respond_to?(:level) && @logger.level > severity
104
+
105
+ @logger.tagged("zabbix_manager") do
106
+ @logger.public_send(level) { JSON.generate(LogSanitizer.sanitize(data.merge(event: event))) }
107
+ end
108
+ rescue StandardError
109
+ nil
169
110
  end
170
111
 
171
- # 返回不包含凭据的客户端摘要,避免对象检查时泄漏配置
172
- # @return [String]
173
- # @api public
112
+ # @return [String] 仅包含脱敏配置的诊断摘要
174
113
  def inspect
175
- "#<#{self.class} options=#{@sanitized_options.inspect}>"
114
+ "#<#{self.class} options=#{@options.inspect}>"
176
115
  end
177
116
 
178
- # 以业务键串行执行幂等写入,并可接入跨进程锁实现
179
- # @return [Object] 代码块结果
180
- # @api semipublic
117
+ # 应用层锁适配器可将本地串行化扩展到多个工作进程。
118
+ # 锁不可重入;调用方不能在块内再次获取 upsert 锁。
119
+ # @param key [String] 被保护操作的稳定、非秘密身份
120
+ # @yield 需要串行执行的读取、修改和写入操作
121
+ # @return [Object] 块的返回值
181
122
  def with_upsert_lock(key, &block)
182
- mutex = @upsert_mutexes[key.hash % UPSERT_MUTEX_STRIPES]
183
- mutex.synchronize do
184
- coordinator = @raw_options[:upsert_lock]
123
+ @upsert_mutexes[key.hash % UPSERT_MUTEX_STRIPES].synchronize do
124
+ coordinator = @settings[:upsert_lock]
185
125
  coordinator ? coordinator.call(key, &block) : yield
186
126
  end
187
127
  end
188
128
 
189
129
  private
190
130
 
191
- # 统一配置键并把页面提交的空凭据归一化为 nil
192
- # @return [Hash]
193
- # @api private
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)
131
+ def sanitized_options
132
+ LogSanitizer.sanitize(@settings.except(:logger, :upsert_lock)).tap do |safe|
133
+ safe[:uncertain_write_delays].freeze
210
134
  %i[url proxy].each do |key|
211
- sanitized[key] = LogSanitizer.sanitize_url(options[key]) if options[key]
135
+ safe[key] = LogSanitizer.sanitize_url(@settings[key]) if @settings[key]
212
136
  end
213
- sanitized
214
137
  end
138
+ end
215
139
 
216
- # 校验页面配置只能选择 API 令牌或用户名密码之一。
217
- # @return [void]
218
- # @api private
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)
140
+ def build_logger
141
+ source = @settings[:logger]
142
+ return if source.nil? && @settings[:log_level].nil?
229
143
 
230
- raise Invalid, "provide api_token or both username and password"
144
+ source ||= ActiveSupport::Logger.new($stderr)
145
+ ActiveSupport::TaggedLogging.new(source).tap do |logger|
146
+ logger.level = @settings[:log_level] if @settings.key?(:log_level)
231
147
  end
148
+ rescue ArgumentError, TypeError
149
+ raise Invalid, "log_level must be a Ruby Logger severity", cause: nil
150
+ end
232
151
 
233
- # 校验传输结果不确定时的只读回查退避窗口。
234
- def validate_uncertain_write_delays!
235
- values = @raw_options.fetch(:uncertain_write_delays, DEFAULT_UNCERTAIN_WRITE_DELAYS)
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"
152
+ def validate_request!(method, params)
153
+ valid_method = method.is_a?(String) && method.valid_encoding? && method.ascii_only? &&
154
+ method.match?(/\A[a-z][a-z0-9_]*\.[a-z][a-zA-Z0-9_]*\z/)
155
+ unless valid_method
156
+ raise Invalid, "method must be a Zabbix API method name"
244
157
  end
158
+ raise Invalid, "params must be a Hash or Array" unless params.is_a?(Hash) || params.is_a?(Array)
159
+ end
245
160
 
246
- # 校验可选的跨进程幂等锁适配器。
247
- # @return [void]
248
- # @api private
249
- def validate_upsert_lock!
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)"
161
+ def request_message(method, params)
162
+ request_id = @id_mutex.synchronize { @next_id += 1 }
163
+ { method: method, params: params, id: request_id, jsonrpc: "2.0" }.tap do |message|
164
+ message[:auth] = @auth_token if @auth_token && authenticated_method?(method) && api_major_version < 7
254
165
  end
166
+ end
255
167
 
256
- # 校验服务端版本格式及支持范围。
257
- # @return [void]
258
- # @api private
259
- def validate_api_version!
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
168
+ def parse_response(body, request_id)
169
+ response = JSON.parse(body)
170
+ unless response.is_a?(Hash) && response["jsonrpc"] == "2.0" && response["id"] == request_id
171
+ raise ProtocolError, "Invalid JSON-RPC response envelope or request ID"
270
172
  end
271
-
272
- # 拒绝 Zabbix 7 Bearer 认证与 HTTP Basic 复用同一认证头。
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"
173
+ unless response.key?("result") ^ response.key?("error")
174
+ raise ProtocolError, "Invalid JSON-RPC response: expected exactly one result or error"
279
175
  end
280
176
 
281
- # 默认阻止 API 令牌通过明文 HTTP 发送
282
- # @return [void]
283
- # @api private
284
- def validate_token_transport!
285
- return unless @raw_options[:api_token] && insecure_token_transport?
286
- return if @raw_options[:allow_insecure_http] == true
177
+ raise_api_error(response) if response.key?("error")
178
+ response.fetch("result")
179
+ rescue JSON::ParserError, TypeError
180
+ raise ProtocolError, "Invalid JSON response", cause: nil
181
+ end
287
182
 
288
- raise Invalid, "api_token requires HTTPS unless allow_insecure_http is true"
183
+ def raise_api_error(response)
184
+ error = response["error"]
185
+ unless error.is_a?(Hash) && error["code"].is_a?(Integer) && error["message"].is_a?(String)
186
+ raise ProtocolError, "Invalid JSON-RPC error response"
289
187
  end
290
188
 
291
- # 判断当前令牌连接是否使用明文 HTTP
292
- # @return [Boolean]
293
- # @api private
294
- def insecure_token_transport?
295
- @raw_options[:api_token].present? && @transport.uri.scheme == "http"
296
- end
189
+ # 服务器文本可能回显任意请求值,包括宏及秘密。
190
+ safe_response = response.slice("jsonrpc", "id").merge("error" => error.slice("code"))
191
+ raise ApiError.new("Zabbix API error #{error.fetch("code")}", safe_response)
192
+ end
297
193
 
298
- # 提取已缓存 API 版本的主版本号。
299
- def api_major_version
300
- @api_version.to_s.split(".", 2).first.to_i
301
- end
194
+ def login
195
+ parameter = api_major_version >= 6 ? :username : :user
196
+ api_request(method: "user.login", params: { parameter => @settings[:username], password: @settings[:password] })
197
+ end
302
198
 
303
- # 根据服务端版本选择登录用户名字段。
304
- def login_parameter
305
- api_major_version >= 6 ? :username : :user
306
- end
199
+ def validate_api_version!
200
+ match = @api_version.is_a?(String) && @api_version.match(/\A(\d+)\.(\d+)\.(\d+)\z/)
201
+ raise ProtocolError, "Invalid API version response" unless match
202
+ return if SUPPORTED_MAJOR_VERSIONS.cover?(match[1].to_i)
203
+ raise ApiError, "Zabbix API version is not supported" unless @settings[:ignore_version]
307
204
 
308
- # 返回统一后的登录用户名。
309
- def username
310
- @raw_options[:username] || @raw_options[:user]
311
- end
205
+ log(:warn, "version.unsupported", version: @api_version)
206
+ end
312
207
 
313
- # 判断当前方法是否应在 JSON-RPC 请求体中携带认证值。
314
- def body_authentication?(method)
315
- authenticated_method?(method) && api_major_version < 7
316
- end
208
+ def validate_http_auth_compatibility!
209
+ return unless api_major_version >= 7 && @settings[:http_user]
317
210
 
318
- # 判断当前方法是否应使用 Bearer 认证头。
319
- def bearer_token_for(method)
320
- @auth_token if authenticated_method?(method) && api_major_version >= 7
321
- end
211
+ raise Invalid, "HTTP Basic authentication cannot be combined with Zabbix 7 Bearer authentication"
212
+ end
322
213
 
323
- # 排除版本查询和登录方法,判断请求是否需要认证。
324
- def authenticated_method?(method)
325
- !UNAUTHENTICATED_METHODS.include?(method.to_s)
326
- end
214
+ def validate_token_transport!
215
+ return unless insecure_token_transport? && @settings[:allow_insecure_http] != true
327
216
 
328
- # 同时读取符号键和字符串键的请求字段。
329
- def fetch_body_value(body, key)
330
- body[key] || body[key.to_s]
331
- end
217
+ raise Invalid, "api_token requires HTTPS unless allow_insecure_http is true"
218
+ end
332
219
 
333
- # 使用脱敏响应和请求摘要构造 API 异常。
334
- def raise_api_error(parsed, request_body)
335
- safe_response = LogSanitizer.sanitize(parsed)
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
220
+ def insecure_token_transport?
221
+ @settings[:api_token] && @transport.uri.scheme == "http"
222
+ end
341
223
 
342
- # 优先使用调用方日志器,调试模式下才创建默认日志器。
343
- def build_logger
344
- return @raw_options[:logger] if @raw_options[:logger]
345
- return unless debug?
224
+ def insecure_https?
225
+ @transport.uri.scheme == "https" && @settings[:verify_ssl] != true
226
+ end
346
227
 
347
- Logger.new($stdout).tap { |value| value.level = Logger::DEBUG }
348
- end
228
+ def api_major_version
229
+ @api_version.to_s.split(".", 2).first.to_i
230
+ end
349
231
 
350
- # 判断 HTTPS 是否按项目要求关闭证书校验。
351
- def insecure_https?
352
- @transport.uri.scheme == "https" && @raw_options[:verify_ssl] != true
353
- end
232
+ def bearer_token_for(method)
233
+ @auth_token if authenticated_method?(method) && api_major_version >= 7
234
+ end
354
235
 
355
- # 读取不受系统时间校准影响的单调时钟。
356
- def monotonic_time
357
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
358
- end
236
+ def authenticated_method?(method)
237
+ !UNAUTHENTICATED_METHODS.include?(method)
238
+ end
359
239
 
360
- # 计算并格式化请求耗时毫秒数。
361
- def elapsed_ms(started_at)
362
- ((monotonic_time - started_at) * 1_000).round(1)
363
- end
240
+ def monotonic_time
241
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
242
+ end
243
+
244
+ def elapsed_ms(started_at)
245
+ ((monotonic_time - started_at) * 1000).round(2) if started_at
246
+ end
364
247
  end
365
248
  end