zabbix_manager 5.1.4 → 5.1.6
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 +25 -9
- data/LICENSE +1 -1
- data/README.md +202 -8
- data/lib/zabbix_manager/basic/basic_alias.rb +25 -3
- data/lib/zabbix_manager/basic/basic_func.rb +32 -61
- data/lib/zabbix_manager/basic/basic_init.rb +28 -13
- data/lib/zabbix_manager/basic/basic_logic.rb +172 -128
- data/lib/zabbix_manager/classes/actions.rb +21 -11
- data/lib/zabbix_manager/classes/applications.rb +12 -17
- data/lib/zabbix_manager/classes/configurations.rb +20 -5
- data/lib/zabbix_manager/classes/drules.rb +11 -23
- data/lib/zabbix_manager/classes/errors.rb +16 -28
- data/lib/zabbix_manager/classes/events.rb +6 -2
- data/lib/zabbix_manager/classes/graphs.rb +41 -34
- data/lib/zabbix_manager/classes/hostgroups.rb +53 -3
- data/lib/zabbix_manager/classes/hostinterfaces.rb +172 -12
- data/lib/zabbix_manager/classes/hosts.rb +197 -68
- data/lib/zabbix_manager/classes/httptests.rb +18 -20
- data/lib/zabbix_manager/classes/items.rb +199 -72
- data/lib/zabbix_manager/classes/maintenance.rb +6 -2
- data/lib/zabbix_manager/classes/mediatypes.rb +7 -63
- data/lib/zabbix_manager/classes/problems.rb +75 -57
- data/lib/zabbix_manager/classes/proxies.rb +48 -7
- data/lib/zabbix_manager/classes/roles.rb +47 -36
- data/lib/zabbix_manager/classes/screens.rb +45 -14
- data/lib/zabbix_manager/classes/scripts.rb +17 -7
- data/lib/zabbix_manager/classes/server.rb +7 -1
- data/lib/zabbix_manager/classes/templates.rb +59 -21
- data/lib/zabbix_manager/classes/triggers.rb +253 -66
- data/lib/zabbix_manager/classes/usergroups.rb +28 -6
- data/lib/zabbix_manager/classes/usermacros.rb +119 -33
- data/lib/zabbix_manager/classes/users.rb +39 -7
- data/lib/zabbix_manager/classes/valuemaps.rb +13 -19
- data/lib/zabbix_manager/client.rb +311 -135
- data/lib/zabbix_manager/http_transport.rb +192 -0
- data/lib/zabbix_manager/log_sanitizer.rb +68 -0
- data/lib/zabbix_manager/monitoring.rb +691 -0
- data/lib/zabbix_manager/version.rb +1 -1
- data/lib/zabbix_manager.rb +118 -40
- data/zabbix_manager.gemspec +21 -8
- metadata +208 -22
- data/lib/zabbix_manager/classes/unusable.rb +0 -11
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "timeout"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
class ZabbixManager
|
|
10
|
+
class HttpTransport
|
|
11
|
+
DEFAULT_TIMEOUT = 60
|
|
12
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT = 30
|
|
13
|
+
NETWORK_ERRORS = [
|
|
14
|
+
Timeout::Error, EOFError, IOError, SystemCallError, SocketError, OpenSSL::SSL::SSLError,
|
|
15
|
+
Net::ProtocolError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :uri
|
|
19
|
+
|
|
20
|
+
# 解析连接、超时和代理配置,并准备持久会话。
|
|
21
|
+
def initialize(options)
|
|
22
|
+
@options = options
|
|
23
|
+
@uri = parse_uri(options.fetch(:url))
|
|
24
|
+
@timeout = positive_number(options.fetch(:timeout, DEFAULT_TIMEOUT), :timeout)
|
|
25
|
+
@open_timeout = positive_number(options.fetch(:open_timeout, @timeout), :open_timeout)
|
|
26
|
+
@read_timeout = positive_number(options.fetch(:read_timeout, @timeout), :read_timeout)
|
|
27
|
+
@write_timeout = positive_number(options.fetch(:write_timeout, @timeout), :write_timeout)
|
|
28
|
+
@keep_alive_timeout = positive_number(
|
|
29
|
+
options.fetch(:keep_alive_timeout, DEFAULT_KEEP_ALIVE_TIMEOUT), :keep_alive_timeout
|
|
30
|
+
)
|
|
31
|
+
@proxy_uri = proxy_uri(options)
|
|
32
|
+
@mutex = Mutex.new
|
|
33
|
+
@http = nil
|
|
34
|
+
@pid = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 串行发送请求,失败时关闭连接但不自动重放写操作。
|
|
38
|
+
def request(body, bearer_token: nil)
|
|
39
|
+
@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
|
|
46
|
+
rescue TransportError
|
|
47
|
+
disconnect
|
|
48
|
+
raise
|
|
49
|
+
rescue *NETWORK_ERRORS => e
|
|
50
|
+
disconnect
|
|
51
|
+
raise TransportError, "#{e.class}: request failed for #{safe_url}"
|
|
52
|
+
rescue StandardError
|
|
53
|
+
disconnect
|
|
54
|
+
raise
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 显式关闭当前持久连接。
|
|
59
|
+
def close
|
|
60
|
+
@mutex.synchronize { disconnect }
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 返回不包含 URL 凭据和查询串的安全地址。
|
|
65
|
+
def safe_url
|
|
66
|
+
port = @uri.port == @uri.default_port ? nil : ":#{@uri.port}"
|
|
67
|
+
"#{@uri.scheme}://#{@uri.host}#{port}#{@uri.path}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 返回不包含代理凭据和请求配置的传输层摘要
|
|
71
|
+
# @return [String]
|
|
72
|
+
# @api private
|
|
73
|
+
def inspect
|
|
74
|
+
"#<#{self.class} url=#{safe_url.inspect} persistent=true>"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
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"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
uri
|
|
87
|
+
rescue URI::InvalidURIError
|
|
88
|
+
raise Invalid, "url must be an absolute HTTP or HTTPS URL"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 把超时配置转换为正数。
|
|
92
|
+
def positive_number(value, name)
|
|
93
|
+
number = Float(value)
|
|
94
|
+
raise Invalid unless number.positive?
|
|
95
|
+
|
|
96
|
+
number
|
|
97
|
+
rescue ArgumentError, TypeError
|
|
98
|
+
raise Invalid, "#{name} must be greater than zero"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 按显式配置和环境变量顺序解析代理。
|
|
102
|
+
def proxy_uri(options)
|
|
103
|
+
return if options[:no_proxy]
|
|
104
|
+
|
|
105
|
+
value = options[:proxy] || environment_proxy
|
|
106
|
+
return if value.nil? || value.empty?
|
|
107
|
+
|
|
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
|
|
112
|
+
|
|
113
|
+
uri
|
|
114
|
+
rescue URI::InvalidURIError
|
|
115
|
+
raise Invalid, "proxy must be a valid URL"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# 根据目标协议选择对应的环境代理。
|
|
119
|
+
def environment_proxy
|
|
120
|
+
return ENV["http_proxy"] || ENV["HTTP_PROXY"] unless @uri.scheme == "https"
|
|
121
|
+
|
|
122
|
+
ENV["https_proxy"] || ENV["HTTPS_PROXY"] || ENV["http_proxy"] || ENV["HTTP_PROXY"]
|
|
123
|
+
end
|
|
124
|
+
|
|
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
|
|
133
|
+
|
|
134
|
+
# 子进程不能向父进程继承的 TLS 套接字发送 close_notify。
|
|
135
|
+
def abandon_inherited_connection
|
|
136
|
+
@http = nil
|
|
137
|
+
@pid = nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 创建并配置 Net::HTTP 实例。
|
|
141
|
+
def build_http
|
|
142
|
+
http = @proxy_uri ? build_proxy_http : Net::HTTP.new(@uri.host, @uri.port)
|
|
143
|
+
|
|
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
|
|
151
|
+
|
|
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
|
|
161
|
+
|
|
162
|
+
# 配置 HTTPS;证书校验默认关闭,按调用方选项开启。
|
|
163
|
+
def configure_tls(http)
|
|
164
|
+
return unless @uri.scheme == "https"
|
|
165
|
+
|
|
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]
|
|
169
|
+
end
|
|
170
|
+
|
|
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
|
|
181
|
+
|
|
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
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
class ZabbixManager
|
|
6
|
+
module LogSanitizer
|
|
7
|
+
REDACTED = "[FILTERED]"
|
|
8
|
+
MAX_STRING_LENGTH = 2_000
|
|
9
|
+
SENSITIVE_KEY_NAMES = "auth|authorization|api_token|community|cookie|http_password|password|passwd|" \
|
|
10
|
+
"privatekey|secret|sessionid|snmp_community|snmpv3_authpassphrase|" \
|
|
11
|
+
"snmpv3_privpassphrase|tls_psk|tls_psk_identity|token"
|
|
12
|
+
SENSITIVE_KEYS = /\A(?:#{SENSITIVE_KEY_NAMES})\z/i.freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# 递归脱敏结构化对象,并限制字符串长度。
|
|
17
|
+
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
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 移除 URL 中的用户名、密码、查询串和片段。
|
|
33
|
+
def sanitize_url(value)
|
|
34
|
+
uri = URI.parse(value.to_s)
|
|
35
|
+
uri.user = nil
|
|
36
|
+
uri.password = nil
|
|
37
|
+
uri.query = nil
|
|
38
|
+
uri.fragment = nil
|
|
39
|
+
uri.to_s
|
|
40
|
+
rescue URI::InvalidURIError
|
|
41
|
+
sanitize_string(value.to_s)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 清理字符串中的认证头和常见敏感键值。
|
|
45
|
+
def sanitize_string(value)
|
|
46
|
+
sanitized = value.dup
|
|
47
|
+
sanitized.gsub!(/\b(Bearer|Basic)\s+[^\s,;]+/i, "\\1 #{REDACTED}")
|
|
48
|
+
sanitized.gsub!(
|
|
49
|
+
/((?:authorization)["']?\s*(?:=>|:|=)\s*)[^,;}\]]+/i,
|
|
50
|
+
"\\1#{REDACTED}"
|
|
51
|
+
)
|
|
52
|
+
sanitized.gsub!(
|
|
53
|
+
/((?:#{SENSITIVE_KEY_NAMES})["']?\s*(?:=>|:|=)\s*)(["'])(.*?)\2/i,
|
|
54
|
+
"\\1\\2#{REDACTED}\\2"
|
|
55
|
+
)
|
|
56
|
+
sanitized.gsub!(
|
|
57
|
+
/((?:#{SENSITIVE_KEY_NAMES})["']?\s*(?:=>|:|=)\s*)(?!["'])[^,;}\]]+/i,
|
|
58
|
+
"\\1#{REDACTED}"
|
|
59
|
+
)
|
|
60
|
+
sanitized.length > MAX_STRING_LENGTH ? "#{sanitized[0, MAX_STRING_LENGTH]}...[TRUNCATED]" : sanitized
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 判断字段名是否属于敏感配置。
|
|
64
|
+
def sensitive_key?(key)
|
|
65
|
+
key.to_s.match?(SENSITIVE_KEYS)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|