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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +69 -46
  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 +135 -89
  14. data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
  15. data/lib/zabbix_manager/classes/items.rb +91 -87
  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 +5 -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 +42 -45
  27. data/lib/zabbix_manager/classes/user_groups.rb +63 -0
  28. data/lib/zabbix_manager/classes/user_macros.rb +110 -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 +148 -277
  32. data/lib/zabbix_manager/configuration.rb +107 -0
  33. data/lib/zabbix_manager/http_transport.rb +145 -97
  34. data/lib/zabbix_manager/log_sanitizer.rb +9 -19
  35. data/lib/zabbix_manager/monitoring/device.rb +393 -0
  36. data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
  37. data/lib/zabbix_manager/monitoring/line.rb +175 -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 +132 -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 +235 -0
  46. data/lib/zabbix_manager/traffic.rb +238 -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
@@ -7,9 +7,15 @@ require "timeout"
7
7
  require "uri"
8
8
 
9
9
  class ZabbixManager
10
+ # 持有串行复用的 Net::HTTP 会话,负责代理、TLS、连接关闭及网络错误转换。
11
+ # 超时配置作用于连接、读取和写入阶段;传输失败不重放请求。
10
12
  class HttpTransport
11
13
  DEFAULT_TIMEOUT = 60
12
14
  DEFAULT_KEEP_ALIVE_TIMEOUT = 30
15
+ DEFAULT_MAX_RESPONSE_BYTES = 64 * 1024 * 1024
16
+ # 仅用于中止单次网络操作,不包裹业务编排或应用层事务。
17
+ class RequestDeadline < Timeout::Error; end
18
+ private_constant :RequestDeadline
13
19
  NETWORK_ERRORS = [
14
20
  Timeout::Error, EOFError, IOError, SystemCallError, SocketError, OpenSSL::SSL::SSLError,
15
21
  Net::ProtocolError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError
@@ -18,10 +24,19 @@ class ZabbixManager
18
24
  attr_reader :uri
19
25
 
20
26
  # 解析连接、超时和代理配置,并准备持久会话。
27
+ # @param options [Hash] URL、超时、TLS 和代理设置;不会在初始化时连接
28
+ # @raise [Invalid] URL、超时或布尔选项无效
21
29
  def initialize(options)
22
- @options = options
30
+ @options = options.dup.freeze
31
+ validate_boolean_options!
23
32
  @uri = parse_uri(options.fetch(:url))
24
33
  @timeout = positive_number(options.fetch(:timeout, DEFAULT_TIMEOUT), :timeout)
34
+ @request_timeout = positive_number(options.fetch(:request_timeout, @timeout), :request_timeout)
35
+ @max_response_bytes = options.fetch(:max_response_bytes, DEFAULT_MAX_RESPONSE_BYTES)
36
+ unless @max_response_bytes.is_a?(Integer) && @max_response_bytes.positive?
37
+ raise Invalid, "max_response_bytes must be a positive integer"
38
+ end
39
+
25
40
  @open_timeout = positive_number(options.fetch(:open_timeout, @timeout), :open_timeout)
26
41
  @read_timeout = positive_number(options.fetch(:read_timeout, @timeout), :read_timeout)
27
42
  @write_timeout = positive_number(options.fetch(:write_timeout, @timeout), :write_timeout)
@@ -35,20 +50,21 @@ class ZabbixManager
35
50
  end
36
51
 
37
52
  # 串行发送请求,失败时关闭连接但不自动重放写操作。
53
+ # @param body [String] 已序列化的 JSON-RPC 请求
54
+ # @param bearer_token [String, nil] 当前请求的 API 认证凭据
55
+ # @return [String] HTTP 成功响应正文,由 Client 验证 JSON-RPC 协议
56
+ # @raise [TransportError] HTTP 状态失败、连接中断或超时;写入可能已生效
38
57
  def request(body, bearer_token: nil)
58
+ request = build_request(body, bearer_token)
39
59
  @mutex.synchronize do
40
- response = active_http.request(build_request(body, bearer_token))
41
- unless response.is_a?(Net::HTTPSuccess)
42
- raise TransportError, "HTTP Error: #{response.code} on #{safe_url}"
43
- end
44
-
45
- response.body
60
+ # Net::HTTP 原生阶段超时仍生效;额外期限只覆盖连接、发送及读取,不重放 POST。
61
+ Timeout.timeout(@request_timeout, RequestDeadline) { read_response(request) }
46
62
  rescue TransportError
47
63
  disconnect
48
64
  raise
49
65
  rescue *NETWORK_ERRORS => e
50
66
  disconnect
51
- raise TransportError, "#{e.class}: request failed for #{safe_url}"
67
+ raise TransportError, "#{e.class}: request failed for #{safe_url}", cause: nil
52
68
  rescue StandardError
53
69
  disconnect
54
70
  raise
@@ -56,12 +72,14 @@ class ZabbixManager
56
72
  end
57
73
 
58
74
  # 显式关闭当前持久连接。
75
+ # @return [true] 重复关闭也成功,下一次请求按需重新连接
59
76
  def close
60
77
  @mutex.synchronize { disconnect }
61
78
  true
62
79
  end
63
80
 
64
81
  # 返回不包含 URL 凭据和查询串的安全地址。
82
+ # @return [String]
65
83
  def safe_url
66
84
  port = @uri.port == @uri.default_port ? nil : ":#{@uri.port}"
67
85
  "#{@uri.scheme}://#{@uri.host}#{port}#{@uri.path}"
@@ -76,117 +94,147 @@ class ZabbixManager
76
94
 
77
95
  private
78
96
 
79
- # 校验并解析 HTTP 或 HTTPS API 地址。
80
- def parse_uri(value)
81
- uri = URI.parse(value.to_s)
82
- unless %w[http https].include?(uri.scheme) && uri.host && !uri.host.empty?
83
- raise Invalid, "url must be an absolute HTTP or HTTPS URL"
97
+ # 流式累计解压后的响应字节,避免在检查上限之前缓冲整个正文。
98
+ def read_response(request)
99
+ body = +""
100
+ active_http.request(request) do |response|
101
+ unless response.is_a?(Net::HTTPSuccess)
102
+ raise TransportError, "HTTP Error: #{response.code} on #{safe_url}"
84
103
  end
85
104
 
86
- uri
87
- rescue URI::InvalidURIError
88
- raise Invalid, "url must be an absolute HTTP or HTTPS URL"
105
+ response.read_body do |chunk|
106
+ if body.bytesize + chunk.bytesize > @max_response_bytes
107
+ raise TransportError, "HTTP response exceeds max_response_bytes on #{safe_url}"
108
+ end
109
+
110
+ body << chunk
111
+ end
89
112
  end
113
+ body
114
+ end
90
115
 
91
- # 把超时配置转换为正数。
92
- def positive_number(value, name)
93
- number = Float(value)
94
- raise Invalid unless number.positive?
116
+ # 校验并解析 HTTP 或 HTTPS API 地址。
117
+ def parse_uri(value)
118
+ uri = URI.parse(value)
119
+ unless %w[http https].include?(uri.scheme) && uri.hostname && !uri.hostname.empty? &&
120
+ (1..65_535).cover?(uri.port)
121
+ raise Invalid, "url must be an absolute HTTP or HTTPS URL with a valid port"
122
+ end
123
+
124
+ uri.freeze
125
+ rescue URI::InvalidURIError, TypeError
126
+ raise Invalid, "url must be an absolute HTTP or HTTPS URL", cause: nil
127
+ end
95
128
 
96
- number
97
- rescue ArgumentError, TypeError
98
- raise Invalid, "#{name} must be greater than zero"
129
+ # 防止字符串布尔值改变 TLS 校验或代理开关。
130
+ def validate_boolean_options!
131
+ %i[verify_ssl no_proxy].each do |name|
132
+ next unless @options.key?(name)
133
+ next if [true, false].include?(@options[name])
134
+
135
+ raise Invalid, "#{name} must be true or false"
99
136
  end
137
+ end
100
138
 
101
- # 按显式配置和环境变量顺序解析代理。
102
- def proxy_uri(options)
103
- return if options[:no_proxy]
139
+ # 把超时配置转换为有限正数。
140
+ def positive_number(value, name)
141
+ number = Float(value)
142
+ raise ArgumentError unless number.finite? && number.positive?
104
143
 
105
- value = options[:proxy] || environment_proxy
106
- return if value.nil? || value.empty?
144
+ number
145
+ rescue ArgumentError, TypeError
146
+ raise Invalid, "#{name} must be a finite number greater than zero", cause: nil
147
+ end
107
148
 
108
- uri = URI.parse(value)
109
- unless %w[http https].include?(uri.scheme) && uri.host.present?
110
- raise Invalid, "proxy must be an absolute HTTP or HTTPS URL"
111
- end
149
+ # 复用标准库的代理发现与排除规则,并拒绝无法兑现的代理协议。
150
+ def proxy_uri(options)
151
+ return if options[:no_proxy]
112
152
 
113
- uri
114
- rescue URI::InvalidURIError
115
- raise Invalid, "proxy must be a valid URL"
153
+ value = options[:proxy]
154
+ uri = value.nil? ? URI::HTTP.build(host: @uri.hostname, port: @uri.port).find_proxy : URI.parse(value)
155
+ return unless uri
156
+ unless uri.is_a?(URI::HTTP) && uri.scheme == "http" && uri.hostname &&
157
+ (1..65_535).cover?(uri.port) && ["", "/"].include?(uri.path) && !uri.query && !uri.fragment
158
+ raise Invalid, "proxy must be an absolute HTTP URL without a path, query or fragment"
116
159
  end
117
160
 
118
- # 根据目标协议选择对应的环境代理。
119
- def environment_proxy
120
- return ENV["http_proxy"] || ENV["HTTP_PROXY"] unless @uri.scheme == "https"
161
+ uri.freeze
162
+ rescue URI::InvalidURIError, TypeError
163
+ raise Invalid, "proxy must be an absolute HTTP URL", cause: nil
164
+ end
121
165
 
122
- ENV["https_proxy"] || ENV["HTTPS_PROXY"] || ENV["http_proxy"] || ENV["HTTP_PROXY"]
123
- end
166
+ # 复用已启动连接,并在 fork 后放弃继承套接字。
167
+ def active_http
168
+ abandon_inherited_connection if @pid && @pid != Process.pid
169
+ @http ||= build_http
170
+ @http.start unless @http.started?
171
+ @pid = Process.pid
172
+ @http
173
+ end
124
174
 
125
- # 复用已启动连接,并在 fork 后放弃继承套接字。
126
- def active_http
127
- abandon_inherited_connection if @pid && @pid != Process.pid
128
- @http ||= build_http
129
- @http.start unless @http.started?
130
- @pid = Process.pid
131
- @http
132
- end
175
+ # 子进程不能向父进程继承的 TLS 套接字发送 close_notify。
176
+ def abandon_inherited_connection
177
+ @http = nil
178
+ @pid = nil
179
+ end
133
180
 
134
- # 子进程不能向父进程继承的 TLS 套接字发送 close_notify。
135
- def abandon_inherited_connection
136
- @http = nil
137
- @pid = nil
138
- end
181
+ # 创建并配置 Net::HTTP 实例。
182
+ def build_http
183
+ http = Net::HTTP.new(@uri.hostname, @uri.port, *proxy_arguments)
184
+
185
+ http.open_timeout = @open_timeout
186
+ http.read_timeout = @read_timeout
187
+ http.write_timeout = @write_timeout
188
+ http.keep_alive_timeout = @keep_alive_timeout
189
+ http.max_retries = 0
190
+ configure_tls(http)
191
+ http
192
+ end
139
193
 
140
- # 创建并配置 Net::HTTP 实例。
141
- def build_http
142
- http = @proxy_uri ? build_proxy_http : Net::HTTP.new(@uri.host, @uri.port)
194
+ # 向 Net::HTTP 传入已解析代理,避免二次环境发现;凭据使用 URI 百分号解码。
195
+ def proxy_arguments
196
+ return [nil] unless @proxy_uri
143
197
 
144
- http.open_timeout = @open_timeout
145
- http.read_timeout = @read_timeout
146
- http.write_timeout = @write_timeout if http.respond_to?(:write_timeout=)
147
- http.keep_alive_timeout = @keep_alive_timeout if http.respond_to?(:keep_alive_timeout=)
148
- configure_tls(http)
149
- http
150
- end
198
+ [
199
+ @proxy_uri.hostname,
200
+ @proxy_uri.port,
201
+ @proxy_uri.user && URI::DEFAULT_PARSER.unescape(@proxy_uri.user),
202
+ @proxy_uri.password && URI::DEFAULT_PARSER.unescape(@proxy_uri.password)
203
+ ]
204
+ end
151
205
 
152
- # 创建使用显式代理的 Net::HTTP 实例。
153
- def build_proxy_http
154
- Net::HTTP::Proxy(
155
- @proxy_uri.host,
156
- @proxy_uri.port,
157
- @proxy_uri.user,
158
- @proxy_uri.password
159
- ).new(@uri.host, @uri.port)
160
- end
206
+ # 配置 HTTPS;证书校验默认关闭,按调用方选项开启。
207
+ def configure_tls(http)
208
+ return unless @uri.scheme == "https"
161
209
 
162
- # 配置 HTTPS;证书校验默认关闭,按调用方选项开启。
163
- def configure_tls(http)
164
- return unless @uri.scheme == "https"
210
+ http.use_ssl = true
211
+ http.verify_mode = @options[:verify_ssl] == true ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
212
+ http.ca_file = @options[:ca_file] if @options[:ca_file]
213
+ end
165
214
 
166
- http.use_ssl = true
167
- http.verify_mode = @options[:verify_ssl] == true ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
168
- http.ca_file = @options[:ca_file] if @options[:ca_file]
215
+ # 构建 JSON-RPC POST,并在需要时写入 Bearer 或 Basic 认证。
216
+ def build_request(body, bearer_token)
217
+ if bearer_token && @options[:http_user]
218
+ raise Invalid, "HTTP Basic authentication cannot be combined with Bearer authentication"
169
219
  end
170
220
 
171
- # 构建 JSON-RPC POST,并在需要时写入 Bearer 或 Basic 认证。
172
- def build_request(body, bearer_token)
173
- request = Net::HTTP::Post.new(@uri.request_uri)
174
- request["Content-Type"] = "application/json-rpc"
175
- request["Accept"] = "application/json"
176
- request["Authorization"] = "Bearer #{bearer_token}" if bearer_token
177
- request.basic_auth(@options[:http_user], @options[:http_password]) if @options[:http_user]
178
- request.body = body
179
- request
180
- end
221
+ request = Net::HTTP::Post.new(@uri.request_uri)
222
+ request["Content-Type"] = "application/json-rpc"
223
+ request["Accept"] = "application/json"
224
+ request["Authorization"] = "Bearer #{bearer_token}" if bearer_token
225
+ request.basic_auth(@options[:http_user], @options[:http_password]) if @options[:http_user]
226
+ request.body = body
227
+ request
228
+ end
181
229
 
182
- # 安全结束连接并清理当前进程的会话状态。
183
- def disconnect
184
- @http.finish if @http&.started?
185
- rescue IOError, SystemCallError
186
- nil
187
- ensure
188
- @http = nil
189
- @pid = nil
190
- end
230
+ # 安全结束连接并清理当前进程的会话状态。
231
+ def disconnect
232
+ @http.finish if @pid == Process.pid && @http&.started?
233
+ rescue *NETWORK_ERRORS
234
+ nil
235
+ ensure
236
+ @http = nil
237
+ @pid = nil
238
+ end
191
239
  end
192
240
  end
@@ -1,32 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
+ require "active_support/parameter_filter"
4
5
 
5
6
  class ZabbixManager
7
+ # 日志与诊断数据的脱敏边界;过滤已知敏感字段、认证头和 URL 凭据。
8
+ # 自由文本过滤不能识别任意秘密,因此客户端不记录请求参数或服务端原始错误文本。
6
9
  module LogSanitizer
7
10
  REDACTED = "[FILTERED]"
8
11
  MAX_STRING_LENGTH = 2_000
9
12
  SENSITIVE_KEY_NAMES = "auth|authorization|api_token|community|cookie|http_password|password|passwd|" \
10
13
  "privatekey|secret|sessionid|snmp_community|snmpv3_authpassphrase|" \
11
14
  "snmpv3_privpassphrase|tls_psk|tls_psk_identity|token"
12
- SENSITIVE_KEYS = /\A(?:#{SENSITIVE_KEY_NAMES})\z/i.freeze
15
+ SENSITIVE_KEYS = /\A(?:#{SENSITIVE_KEY_NAMES})\z/i
16
+ FILTER = ActiveSupport::ParameterFilter.new(
17
+ [SENSITIVE_KEYS, ->(_key, value) { value.replace(sanitize_string(value)) if value.is_a?(String) }]
18
+ ).freeze
13
19
 
14
20
  module_function
15
21
 
16
22
  # 递归脱敏结构化对象,并限制字符串长度。
17
23
  def sanitize(value)
18
- case value
19
- when Hash
20
- value.each_with_object({}) do |(key, child), result|
21
- result[key] = sensitive_key?(key) ? REDACTED : sanitize(child)
22
- end
23
- when Array
24
- value.map { |child| sanitize(child) }
25
- when String
26
- sanitize_string(value)
27
- else
28
- value
29
- end
24
+ FILTER.filter_param("value", value)
30
25
  end
31
26
 
32
27
  # 移除 URL 中的用户名、密码、查询串和片段。
@@ -38,7 +33,7 @@ class ZabbixManager
38
33
  uri.fragment = nil
39
34
  uri.to_s
40
35
  rescue URI::InvalidURIError
41
- sanitize_string(value.to_s)
36
+ REDACTED
42
37
  end
43
38
 
44
39
  # 清理字符串中的认证头和常见敏感键值。
@@ -59,10 +54,5 @@ class ZabbixManager
59
54
  )
60
55
  sanitized.length > MAX_STRING_LENGTH ? "#{sanitized[0, MAX_STRING_LENGTH]}...[TRUNCATED]" : sanitized
61
56
  end
62
-
63
- # 判断字段名是否属于敏感配置。
64
- def sensitive_key?(key)
65
- key.to_s.match?(SENSITIVE_KEYS)
66
- end
67
57
  end
68
58
  end