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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -9
  3. data/LICENSE +1 -1
  4. data/README.md +241 -24
  5. data/lib/zabbix_manager/classes/actions.rb +19 -17
  6. data/lib/zabbix_manager/classes/applications.rb +11 -22
  7. data/lib/zabbix_manager/classes/configurations.rb +20 -10
  8. data/lib/zabbix_manager/classes/discovery_rules.rb +23 -0
  9. data/lib/zabbix_manager/classes/errors.rb +18 -27
  10. data/lib/zabbix_manager/classes/events.rb +8 -3
  11. data/lib/zabbix_manager/classes/graphs.rb +48 -37
  12. data/lib/zabbix_manager/classes/host_groups.rb +64 -0
  13. data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
  14. data/lib/zabbix_manager/classes/hosts.rb +232 -76
  15. data/lib/zabbix_manager/classes/http_tests.rb +32 -0
  16. data/lib/zabbix_manager/classes/items.rb +206 -72
  17. data/lib/zabbix_manager/classes/maintenance.rb +8 -3
  18. data/lib/zabbix_manager/classes/media_types.rb +18 -0
  19. data/lib/zabbix_manager/classes/problems.rb +51 -62
  20. data/lib/zabbix_manager/classes/proxies.rb +45 -14
  21. data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
  22. data/lib/zabbix_manager/classes/roles.rb +46 -41
  23. data/lib/zabbix_manager/classes/screens.rb +36 -24
  24. data/lib/zabbix_manager/classes/scripts.rb +16 -12
  25. data/lib/zabbix_manager/classes/server.rb +8 -1
  26. data/lib/zabbix_manager/classes/templates.rb +26 -51
  27. data/lib/zabbix_manager/classes/triggers.rb +251 -67
  28. data/lib/zabbix_manager/classes/user_groups.rb +63 -0
  29. data/lib/zabbix_manager/classes/user_macros.rb +110 -0
  30. data/lib/zabbix_manager/classes/users.rb +16 -23
  31. data/lib/zabbix_manager/classes/value_maps.rb +52 -0
  32. data/lib/zabbix_manager/client.rb +183 -137
  33. data/lib/zabbix_manager/configuration.rb +107 -0
  34. data/lib/zabbix_manager/http_transport.rb +240 -0
  35. data/lib/zabbix_manager/log_sanitizer.rb +58 -0
  36. data/lib/zabbix_manager/monitoring/device.rb +393 -0
  37. data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
  38. data/lib/zabbix_manager/monitoring/line.rb +175 -0
  39. data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
  40. data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
  41. data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
  42. data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
  43. data/lib/zabbix_manager/monitoring/traffic_items.rb +132 -0
  44. data/lib/zabbix_manager/monitoring/validation.rb +92 -0
  45. data/lib/zabbix_manager/monitoring.rb +265 -0
  46. data/lib/zabbix_manager/resource.rb +235 -0
  47. data/lib/zabbix_manager/traffic.rb +238 -0
  48. data/lib/zabbix_manager/version.rb +1 -1
  49. data/lib/zabbix_manager.rb +110 -145
  50. data/zabbix_manager.gemspec +12 -15
  51. metadata +67 -30
  52. data/lib/zabbix_manager/basic/basic_alias.rb +0 -20
  53. data/lib/zabbix_manager/basic/basic_func.rb +0 -84
  54. data/lib/zabbix_manager/basic/basic_init.rb +0 -35
  55. data/lib/zabbix_manager/basic/basic_logic.rb +0 -195
  56. data/lib/zabbix_manager/classes/drules.rb +0 -41
  57. data/lib/zabbix_manager/classes/hostgroups.rb +0 -20
  58. data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -29
  59. data/lib/zabbix_manager/classes/httptests.rb +0 -40
  60. data/lib/zabbix_manager/classes/mediatypes.rb +0 -79
  61. data/lib/zabbix_manager/classes/unusable.rb +0 -11
  62. data/lib/zabbix_manager/classes/usergroups.rb +0 -53
  63. data/lib/zabbix_manager/classes/usermacros.rb +0 -136
  64. 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 "openssl"
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
- attr_reader :options
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
- def id
14
- @id ||= rand 100_00
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
- # 获取 Zabbix API 版本
18
- def api_version
19
- api_request(method: "apiinfo.version", params: {})
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
- # 是否启用 debug 模式
23
- def debug?
24
- !@options || @options[:debug]
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
- # Debug 打印信息
28
- def log(message)
29
- puts message if debug?
81
+ # 关闭连接但不撤销凭据;后续请求按需重新连接。
82
+ # @return [true]
83
+ def close
84
+ @transport.close
30
85
  end
31
86
 
32
- # 用户鉴权方法,登录成功即可获取令牌
33
- def auth
34
- # 发起 "user.login" 方法的 API 请求, 并传递用户信息(用户名和密码)作为参数
35
- api_request(
36
- method: "user.login",
37
- params: {
38
- user: @options[:user],
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
- # HTTP 客户端初始化方法,同时进行用户权限签证
45
- def initialize(options = {})
46
- # 设置实例变量 @options 以便在整个类中访问
47
- @options = options
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
- raise ZbxError, "必须正确提供 :url 属性" unless @options.key?(:url)
51
- raise ZbxError, "必须正确提供 :user 属性" unless @options.key?(:user)
52
- raise ZbxError, "必须正确提供 :password 属性" unless @options.key?(:password)
102
+ # @return [String] 仅包含脱敏配置的诊断摘要
103
+ def inspect
104
+ "#<#{self.class} options=#{@options.inspect}>"
105
+ end
53
106
 
54
- # 配置代理信息
55
- if !ENV["http_proxy"].nil? && options[:no_proxy] != true
56
- @proxy_uri = URI.parse(ENV["http_proxy"])
57
- @proxy_host = @proxy_uri.host
58
- @proxy_port = @proxy_uri.port
59
- @proxy_user, @proxy_pass = @proxy_uri.userinfo&.split(/:/) if @proxy_uri.userinfo
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
- # 检查 Zabbix API 版本是否受支持
63
- unless api_version.match?(/^[4-7]\.\d+\.\d+$/)
64
- message = "Zabbix API版本:#{api_version} 不受此版本的 zabbix_manager 支持"
65
- if @options[:ignore_version]
66
- log "[WARNING] #{message}"
67
- else
68
- raise ZbxError, message
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
- # 定义一个方法,用于将 Ruby 数据结构转换为 JSON 字符串,并根据条件添加令牌
78
- def json_payload(body)
79
- # 构建基本的 JSON 负载,包括方法名、参数、请求 ID 和 JSON-RPC 版本
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
- # 检查请求方法是否是 "apiinfo.version" 或 "user.login",如果不是,则添加令牌信息
88
- payload[:auth] = @auth_hash unless body[:method] == "apiinfo.version" || body[:method] == "user.login"
89
-
90
- # 将构建好的负载转换为 JSON 字符串并返回
91
- JSON(payload)
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
- # 实例化 Net::HTTP 对象,并发起 API 请求的方法
95
- def http_request(body)
96
- uri = URI.parse(@options[:url])
97
-
98
- # 设置超时时间,默认为 60 秒
99
- timeout = @options[:timeout].nil? ? 60 : @options[:timeout]
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
- # 配置 HTTPS 请求
111
- if uri.scheme == "https"
112
- http.use_ssl = true
113
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
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
- http.open_timeout = timeout
118
- http.read_timeout = timeout
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
- # 创建 POST 请求
121
- request = Net::HTTP::Post.new(uri.request_uri)
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
- request.basic_auth(@options[:http_user], @options[:http_password]) if @options[:http_user]
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
- # 设置请求头的 Content-Type 为 application/json-rpc
127
- request.add_field("Content-Type", "application/json-rpc")
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
- # 设置请求体为传入的 JSON 字符串
130
- request.body = body
131
- log "[DEBUG] HTTP request params: #{pretty_body(request.body)}"
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
- response = http.request(request)
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
- # 检查响应状态码,如果不是 200,则抛出 HttpError 异常
137
- raise HttpError.new("HTTP Error: #{response.code} on #{@options[:url]}", response) unless response.code == "200"
193
+ log(:warn, "version.unsupported", version: @api_version)
194
+ end
138
195
 
139
- log "[DEBUG] HTTP response answer: #{pretty_body(response.body)}"
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
- # 对 API 响应结果进行解析的方法
146
- def _request(body)
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
- # 如果响应结果中包含错误信息,则抛出 ZbxError 异常
151
- if result["error"]
152
- # 构建错误消息,包括服务器返回的错误信息和请求的详细信息
153
- msg = <<~MOJO
205
+ raise Invalid, "api_token requires HTTPS unless allow_insecure_http is true"
206
+ end
154
207
 
155
- ---------------------------------------------------------------
156
- Server answer API error: #{JSON.pretty_unparse(result["error"])}
157
- on request: #{pretty_body(body)}
158
- ---------------------------------------------------------------
208
+ def insecure_token_transport?
209
+ @settings[:api_token] && @transport.uri.scheme == "http"
210
+ end
159
211
 
160
- MOJO
212
+ def insecure_https?
213
+ @transport.uri.scheme == "https" && @settings[:verify_ssl] != true
214
+ end
161
215
 
162
- # 抛出自定义异常 ZbxError,并传递错误消息和完整的响应结果
163
- raise ZbxError.new(msg, result)
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
- # 将 Ruby 数据结构美化成 JSON 字符串后打印输出的方法
171
- def pretty_body(body)
172
- # 解析 JSON 字符串
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
- if data["params"].is_a?(Hash) && data["params"].key?("password")
177
- data["params"]["password"] = "***"
178
- end
224
+ def authenticated_method?(method)
225
+ !UNAUTHENTICATED_METHODS.include?(method)
226
+ end
179
227
 
180
- # 将美化后的数据转换为 JSON 字符串并返回
181
- JSON.pretty_unparse(data)
228
+ def monotonic_time
229
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
182
230
  end
183
231
 
184
- # 通用的 API 调用发起方法
185
- def api_request(body)
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