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
|
@@ -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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
139
|
+
# 把超时配置转换为有限正数。
|
|
140
|
+
def positive_number(value, name)
|
|
141
|
+
number = Float(value)
|
|
142
|
+
raise ArgumentError unless number.finite? && number.positive?
|
|
104
143
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
end
|
|
149
|
+
# 复用标准库的代理发现与排除规则,并拒绝无法兑现的代理协议。
|
|
150
|
+
def proxy_uri(options)
|
|
151
|
+
return if options[:no_proxy]
|
|
112
152
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
194
|
+
# 向 Net::HTTP 传入已解析代理,避免二次环境发现;凭据使用 URI 百分号解码。
|
|
195
|
+
def proxy_arguments
|
|
196
|
+
return [nil] unless @proxy_uri
|
|
143
197
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|