zabbix_manager 5.1.5 → 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 +55 -9
- data/LICENSE +1 -1
- data/README.md +241 -24
- data/lib/zabbix_manager/classes/actions.rb +19 -17
- data/lib/zabbix_manager/classes/applications.rb +11 -22
- data/lib/zabbix_manager/classes/configurations.rb +20 -10
- data/lib/zabbix_manager/classes/discovery_rules.rb +23 -0
- data/lib/zabbix_manager/classes/errors.rb +18 -27
- data/lib/zabbix_manager/classes/events.rb +8 -3
- data/lib/zabbix_manager/classes/graphs.rb +48 -37
- data/lib/zabbix_manager/classes/host_groups.rb +64 -0
- data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
- data/lib/zabbix_manager/classes/hosts.rb +232 -76
- data/lib/zabbix_manager/classes/http_tests.rb +32 -0
- data/lib/zabbix_manager/classes/items.rb +206 -72
- data/lib/zabbix_manager/classes/maintenance.rb +8 -3
- data/lib/zabbix_manager/classes/media_types.rb +18 -0
- data/lib/zabbix_manager/classes/problems.rb +51 -62
- data/lib/zabbix_manager/classes/proxies.rb +45 -14
- data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
- data/lib/zabbix_manager/classes/roles.rb +46 -41
- data/lib/zabbix_manager/classes/screens.rb +36 -24
- data/lib/zabbix_manager/classes/scripts.rb +16 -12
- data/lib/zabbix_manager/classes/server.rb +8 -1
- data/lib/zabbix_manager/classes/templates.rb +26 -51
- data/lib/zabbix_manager/classes/triggers.rb +251 -67
- 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 +16 -23
- data/lib/zabbix_manager/classes/value_maps.rb +52 -0
- data/lib/zabbix_manager/client.rb +183 -137
- data/lib/zabbix_manager/configuration.rb +107 -0
- data/lib/zabbix_manager/http_transport.rb +240 -0
- data/lib/zabbix_manager/log_sanitizer.rb +58 -0
- 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 +265 -0
- 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 +110 -145
- data/zabbix_manager.gemspec +12 -15
- metadata +67 -30
- data/lib/zabbix_manager/basic/basic_alias.rb +0 -20
- data/lib/zabbix_manager/basic/basic_func.rb +0 -84
- data/lib/zabbix_manager/basic/basic_init.rb +0 -35
- data/lib/zabbix_manager/basic/basic_logic.rb +0 -195
- data/lib/zabbix_manager/classes/drules.rb +0 -41
- data/lib/zabbix_manager/classes/hostgroups.rb +0 -20
- data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -29
- data/lib/zabbix_manager/classes/httptests.rb +0 -40
- data/lib/zabbix_manager/classes/mediatypes.rb +0 -79
- data/lib/zabbix_manager/classes/unusable.rb +0 -11
- data/lib/zabbix_manager/classes/usergroups.rb +0 -53
- data/lib/zabbix_manager/classes/usermacros.rb +0 -136
- data/lib/zabbix_manager/classes/valuemaps.rb +0 -32
|
@@ -1,190 +1,236 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "net/http"
|
|
4
3
|
require "json"
|
|
5
|
-
require "
|
|
4
|
+
require "logger"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/logger"
|
|
7
|
+
require "active_support/tagged_logging"
|
|
6
8
|
|
|
7
9
|
class ZabbixManager
|
|
10
|
+
# 持有一个已认证连接,管理 JSON-RPC 请求的完整生命周期。
|
|
8
11
|
class Client
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
SUPPORTED_MAJOR_VERSIONS = (4..7)
|
|
13
|
+
UNAUTHENTICATED_METHODS = %w[apiinfo.version user.login].freeze
|
|
14
|
+
UPSERT_MUTEX_STRIPES = 64
|
|
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
|
|
26
|
+
@logger = build_logger
|
|
27
|
+
@id_mutex = Mutex.new
|
|
28
|
+
@next_id = 0
|
|
29
|
+
@upsert_mutexes = Array.new(UPSERT_MUTEX_STRIPES) { Mutex.new }
|
|
30
|
+
@transport = HttpTransport.new(@settings)
|
|
31
|
+
validate_token_transport!
|
|
32
|
+
@api_version = api_request(method: "apiinfo.version")
|
|
33
|
+
validate_api_version!
|
|
34
|
+
validate_http_auth_compatibility!
|
|
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
|
|
11
40
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@
|
|
41
|
+
log(:info, "client.connected", version: @api_version, credential_type: @credential_type)
|
|
42
|
+
log(:warn, "tls.verification_disabled", url: @transport.safe_url) if insecure_https?
|
|
43
|
+
log(:warn, "token.sent_over_http", url: @transport.safe_url) if insecure_token_transport?
|
|
44
|
+
rescue StandardError
|
|
45
|
+
@transport&.close
|
|
46
|
+
raise
|
|
15
47
|
end
|
|
16
48
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
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)
|
|
58
|
+
started_at = monotonic_time
|
|
59
|
+
message = request_message(method, params)
|
|
60
|
+
log(:debug, "request.started", method: method)
|
|
61
|
+
body = @transport.request(JSON.generate(message), bearer_token: bearer_token_for(method))
|
|
62
|
+
result = parse_response(body, message.fetch(:id))
|
|
63
|
+
log(:info, "request.completed", method: method, duration_ms: elapsed_ms(started_at))
|
|
64
|
+
result
|
|
65
|
+
rescue StandardError => error
|
|
66
|
+
log(:warn, "request.failed", method: method, duration_ms: elapsed_ms(started_at), error: error.class.name)
|
|
67
|
+
raise
|
|
20
68
|
end
|
|
21
69
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
70
|
+
# 注销密码认证会话;API token 仅需清理本地状态。
|
|
71
|
+
# 远端注销失败时仍清除本地认证令牌并关闭传输连接。
|
|
72
|
+
# @return [Boolean] 服务器注销结果;API token 模式返回 true
|
|
73
|
+
# @raise [ApiError, TransportError] 远端会话注销失败
|
|
74
|
+
def logout
|
|
75
|
+
@credential_type == :user_session && @auth_token ? api_request(method: "user.logout", params: []) : true
|
|
76
|
+
ensure
|
|
77
|
+
@auth_token = nil
|
|
78
|
+
close
|
|
25
79
|
end
|
|
26
80
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
81
|
+
# 关闭连接但不撤销凭据;后续请求按需重新连接。
|
|
82
|
+
# @return [true]
|
|
83
|
+
def close
|
|
84
|
+
@transport.close
|
|
30
85
|
end
|
|
31
86
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
password: @options[:password]
|
|
40
|
-
}
|
|
41
|
-
)
|
|
42
|
-
end
|
|
87
|
+
# 延迟构造日志元数据;日志失败不能改变远端写入结果。
|
|
88
|
+
# @param level [Symbol] Ruby Logger 日志级别方法
|
|
89
|
+
# @param event [String] 稳定的事件名称
|
|
90
|
+
# @param data [Hash] 序列化之前经过脱敏的元数据
|
|
91
|
+
# @return [Object, nil] 日志返回值;日志关闭或失败时返回 nil
|
|
92
|
+
def log(level, event, data = {})
|
|
93
|
+
return unless @logger
|
|
43
94
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
100
|
+
end
|
|
48
101
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
102
|
+
# @return [String] 仅包含脱敏配置的诊断摘要
|
|
103
|
+
def inspect
|
|
104
|
+
"#<#{self.class} options=#{@options.inspect}>"
|
|
105
|
+
end
|
|
53
106
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
107
|
+
# 应用层锁适配器可将本地串行化扩展到多个工作进程。
|
|
108
|
+
# 锁不可重入;调用方不能在块内再次获取 upsert 锁。
|
|
109
|
+
# @param key [String] 被保护操作的稳定、非秘密身份
|
|
110
|
+
# @yield 需要串行执行的读取、修改和写入操作
|
|
111
|
+
# @return [Object] 块的返回值
|
|
112
|
+
def with_upsert_lock(key, &block)
|
|
113
|
+
@upsert_mutexes[key.hash % UPSERT_MUTEX_STRIPES].synchronize do
|
|
114
|
+
coordinator = @settings[:upsert_lock]
|
|
115
|
+
coordinator ? coordinator.call(key, &block) : yield
|
|
60
116
|
end
|
|
117
|
+
end
|
|
61
118
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def sanitized_options
|
|
122
|
+
LogSanitizer.sanitize(@settings.except(:logger, :upsert_lock)).tap do |safe|
|
|
123
|
+
safe[:uncertain_write_delays].freeze
|
|
124
|
+
%i[url proxy].each do |key|
|
|
125
|
+
safe[key] = LogSanitizer.sanitize_url(@settings[key]) if @settings[key]
|
|
69
126
|
end
|
|
70
127
|
end
|
|
71
|
-
|
|
72
|
-
# 进行用户鉴权,获取令牌
|
|
73
|
-
@auth_hash = auth
|
|
74
|
-
log "[DEBUG] Auth token: #{@auth_hash}"
|
|
75
128
|
end
|
|
76
129
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
payload = {
|
|
81
|
-
method: body[:method],
|
|
82
|
-
params: body[:params],
|
|
83
|
-
id: id,
|
|
84
|
-
jsonrpc: "2.0"
|
|
85
|
-
}
|
|
130
|
+
def build_logger
|
|
131
|
+
source = @settings[:logger]
|
|
132
|
+
return if source.nil? && @settings[:log_level].nil?
|
|
86
133
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
137
|
+
end
|
|
138
|
+
rescue ArgumentError, TypeError
|
|
139
|
+
raise Invalid, "log_level must be a Ruby Logger severity", cause: nil
|
|
92
140
|
end
|
|
93
141
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
log "[DEBUG] Timeout for request set to #{timeout} seconds"
|
|
101
|
-
|
|
102
|
-
# 创建 Net::HTTP 对象,支持代理设置
|
|
103
|
-
http =
|
|
104
|
-
if @proxy_uri
|
|
105
|
-
Net::HTTP.Proxy(@proxy_host, @proxy_port, @proxy_user, @proxy_pass).new(uri.host, uri.port)
|
|
106
|
-
else
|
|
107
|
-
Net::HTTP.new(uri.host, uri.port)
|
|
108
|
-
end
|
|
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"
|
|
145
|
+
end
|
|
146
|
+
raise Invalid, "params must be a Hash or Array" unless params.is_a?(Hash) || params.is_a?(Array)
|
|
147
|
+
end
|
|
109
148
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
153
|
end
|
|
154
|
+
end
|
|
115
155
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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"
|
|
160
|
+
end
|
|
161
|
+
unless response.key?("result") ^ response.key?("error")
|
|
162
|
+
raise ProtocolError, "Invalid JSON-RPC response: expected exactly one result or error"
|
|
163
|
+
end
|
|
119
164
|
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
122
170
|
|
|
123
|
-
|
|
124
|
-
|
|
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"
|
|
175
|
+
end
|
|
125
176
|
|
|
126
|
-
#
|
|
127
|
-
|
|
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
|
|
128
181
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
132
186
|
|
|
133
|
-
|
|
134
|
-
|
|
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]
|
|
135
192
|
|
|
136
|
-
|
|
137
|
-
|
|
193
|
+
log(:warn, "version.unsupported", version: @api_version)
|
|
194
|
+
end
|
|
138
195
|
|
|
139
|
-
|
|
196
|
+
def validate_http_auth_compatibility!
|
|
197
|
+
return unless api_major_version >= 7 && @settings[:http_user]
|
|
140
198
|
|
|
141
|
-
|
|
142
|
-
response.body
|
|
199
|
+
raise Invalid, "HTTP Basic authentication cannot be combined with Zabbix 7 Bearer authentication"
|
|
143
200
|
end
|
|
144
201
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# 发起 HTTP 请求并解析 JSON 格式的响应
|
|
148
|
-
result = JSON.parse(http_request(body))
|
|
202
|
+
def validate_token_transport!
|
|
203
|
+
return unless insecure_token_transport? && @settings[:allow_insecure_http] != true
|
|
149
204
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# 构建错误消息,包括服务器返回的错误信息和请求的详细信息
|
|
153
|
-
msg = <<~MOJO
|
|
205
|
+
raise Invalid, "api_token requires HTTPS unless allow_insecure_http is true"
|
|
206
|
+
end
|
|
154
207
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
---------------------------------------------------------------
|
|
208
|
+
def insecure_token_transport?
|
|
209
|
+
@settings[:api_token] && @transport.uri.scheme == "http"
|
|
210
|
+
end
|
|
159
211
|
|
|
160
|
-
|
|
212
|
+
def insecure_https?
|
|
213
|
+
@transport.uri.scheme == "https" && @settings[:verify_ssl] != true
|
|
214
|
+
end
|
|
161
215
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
else
|
|
165
|
-
# 如果没有错误信息,则返回响应结果中的 "result" 部分
|
|
166
|
-
result["result"]
|
|
167
|
-
end
|
|
216
|
+
def api_major_version
|
|
217
|
+
@api_version.to_s.split(".", 2).first.to_i
|
|
168
218
|
end
|
|
169
219
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
data = JSON.parse(body)
|
|
220
|
+
def bearer_token_for(method)
|
|
221
|
+
@auth_token if authenticated_method?(method) && api_major_version >= 7
|
|
222
|
+
end
|
|
174
223
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
end
|
|
224
|
+
def authenticated_method?(method)
|
|
225
|
+
!UNAUTHENTICATED_METHODS.include?(method)
|
|
226
|
+
end
|
|
179
227
|
|
|
180
|
-
|
|
181
|
-
|
|
228
|
+
def monotonic_time
|
|
229
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
182
230
|
end
|
|
183
231
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# 调用之前定义的 json_payload 方法,将请求信息转换为 JSON 字符串
|
|
187
|
-
_request json_payload(body)
|
|
232
|
+
def elapsed_ms(started_at)
|
|
233
|
+
((monotonic_time - started_at) * 1000).round(2) if started_at
|
|
188
234
|
end
|
|
189
235
|
end
|
|
190
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
|