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,106 +1,290 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ZabbixManager
4
- class Triggers < Basic
5
- # 与 Zabbix API 交互的触发器方法名称
4
+ # 触发器与依赖对账;受管身份使用标签,创建结果不明时只回读、不重放。
5
+ class Triggers < Resource
6
+ DEFAULT_UNCERTAIN_WRITE_DELAYS = [0, 0.25, 1, 2].freeze
7
+ MAX_MANAGED_KEY_LENGTH = 200
8
+ # 返回触发器对应的 Zabbix API 模块名。
9
+ #
10
+ # @return [String]
6
11
  def method_name
7
12
  "trigger"
8
13
  end
9
14
 
10
- # 用于通过 Zabbix API 标识特定触发器对象的 id 字段名称
15
+ # 使用描述作为通用 CRUD 的业务标识。
16
+ #
17
+ # @return [String]
11
18
  def identify
12
19
  "description"
13
20
  end
14
21
 
15
- # 创建 Zabbix 触发器的默认选项
16
- def default_options
17
- {
18
- recovery_mode: 1,
19
- type: 0,
20
- manual_close: 1
22
+ # 按触发器 ID 查询完整对象及关联项和函数。
23
+ #
24
+ # @param data [Hash] 包含目标触发器 ID 字段及其值
25
+ # @raise [ApiError] Zabbix API 明确返回业务错误
26
+ # @raise [TransportError] HTTP 或网络请求失败,可能无法确认远端结果
27
+ # @return [Array<Hash>] 匹配的触发器及其关联对象
28
+ def dump_by_id(data)
29
+ @client.api_request(
30
+ method: "trigger.get",
31
+ params: {
32
+ filter: {
33
+ key.to_sym => data[key.to_sym]
34
+ },
35
+ output: "extend",
36
+ selectItems: "extend",
37
+ selectFunctions: "extend"
38
+ }
39
+ )
40
+ end
41
+
42
+ # 查询主机触发器,并按需返回监控项、函数、依赖和标签。
43
+ # @return [Array<Hash>]
44
+ def for_host(hostid, output: "extend", select_items: nil, select_functions: nil,
45
+ select_dependencies: nil, select_tags: nil, filter: nil)
46
+ raise Invalid, "hostid is required" if hostid.blank?
47
+
48
+ params = { hostids: hostid, output: output }
49
+ params[:selectItems] = select_items if select_items
50
+ params[:selectFunctions] = select_functions if select_functions
51
+ params[:selectDependencies] = select_dependencies if select_dependencies
52
+ params[:selectTags] = select_tags if select_tags
53
+ params[:filter] = filter if filter.present?
54
+ @client.api_request(method: "trigger.get", params: params)
55
+ end
56
+
57
+ # 按主机、描述和可选标签查询唯一触发器。
58
+ # @return [Hash, nil]
59
+ def find_for_host(hostid:, description:, tags: nil)
60
+ params = {
61
+ hostids: hostid,
62
+ filter: { description: description },
63
+ output: ["triggerid", "description"],
64
+ selectTags: "extend"
21
65
  }
66
+ params[:tags] = tags if tags.present?
67
+ result = @client.api_request(
68
+ method: "trigger.get",
69
+ params: params
70
+ )
71
+ raise Conflict, "multiple triggers match #{description} on host #{hostid}" if result.length > 1
72
+
73
+ result.first
22
74
  end
23
75
 
24
- # 通过键值对从 Zabbix API 转储触发器对象数据
25
- def dump_by_id(data)
26
- log "[DEBUG] 调用 dump_by_id 方法,参数: #{data.inspect}"
27
-
28
- get_raw(
29
- {
30
- triggerids: data[keys.to_sym],
31
- output: "extend",
32
- select_items: "extend",
33
- select_functions: "extend"
76
+ # 按管理标签查询唯一触发器。
77
+ # 展开表达式并读取依赖,供不确定写入后的完整期望状态核对。
78
+ # @param hostid [Integer, String] 触发器所属主机
79
+ # @param managed_key [String] 稳定、非秘密的受管身份
80
+ # @return [Hash, nil]
81
+ def find_managed_for_host(hostid:, managed_key:)
82
+ result = @client.api_request(
83
+ method: "trigger.get",
84
+ params: {
85
+ hostids: hostid,
86
+ tags: [{ tag: "zabbix_manager_id", value: managed_key, operator: 1 }],
87
+ output: "extend",
88
+ expandExpression: true,
89
+ selectDependencies: ["triggerid"],
90
+ selectTags: "extend"
34
91
  }
35
92
  )
93
+ raise Conflict, "multiple triggers use managed key #{managed_key}" if result.length > 1
94
+
95
+ result.first
36
96
  end
37
97
 
38
- # 安全地使用 Zabbix API 更新触发器对象,通过删除和替换触发器
39
- def safe_update(data)
40
- log "[DEBUG] 调用 safe_update 方法,参数: #{data.inspect}"
98
+ # 按触发器 ID 查询唯一对象,并可返回当前依赖集合。
99
+ # @return [Hash, nil]
100
+ def find_by_id(triggerid, select_dependencies: nil)
101
+ id = normalized_ids([triggerid]).first
102
+ params = { triggerids: id, output: "extend" }
103
+ params[:selectDependencies] = select_dependencies if select_dependencies
104
+ result = @client.api_request(method: "trigger.get", params: params)
105
+ raise Conflict, "multiple triggers use triggerid #{id}" if result.length > 1
41
106
 
42
- dump = {}
43
- item_id = data[key.to_sym].to_i
44
- dump_by_id(key.to_sym => data[key.to_sym]).each do |item|
45
- dump = symbolize_keys(item) if item[key].to_i == data[key.to_sym].to_i
46
- end
107
+ result.first
108
+ end
109
+
110
+ # 按稳定管理键串行创建或更新主机触发器。
111
+ # 可通过 Client 的 upsert_lock 接入跨进程锁。
112
+ # @return [Integer]
113
+ def upsert_for_host(data)
114
+ attributes = data.deep_symbolize_keys
115
+ lock_key = "trigger:#{attributes[:hostid]}:#{attributes[:managed_key] || attributes[:description]}"
116
+ @client.with_upsert_lock(lock_key) { perform_upsert_for_host(attributes) }
117
+ end
47
118
 
48
- expression = "#{dump[:items][0][:key_]}.#{dump[:functions][0][:function]}(#{dump[:functions][0][:parameter]})"
49
- dump[:expression] = dump[:expression].gsub(/\{(\d*)\}/, "{#{expression}}") # TODO: ugly regexp
50
- dump.delete(:functions)
51
- dump.delete(:items)
119
+ # 批量启用或停用触发器。
120
+ # @return [Array<Integer>]
121
+ def set_status(hostid:, triggerids:, enabled:)
122
+ validate_boolean!(enabled, "enabled")
123
+ ids = normalized_ids(triggerids)
124
+ verify_host_ownership!(hostid, ids)
125
+ result = @client.api_request(
126
+ method: "trigger.update",
127
+ params: ids.map { |triggerid| { triggerid: triggerid, status: enabled ? 0 : 1 } }
128
+ )
129
+ response_ids(result, expected: ids)
130
+ end
52
131
 
53
- old_expression = data[:expression]
54
- data[:expression] = data[:expression].gsub(/\{.*:/, "{") # TODO: ugly regexp
55
- data.delete(:templateid)
132
+ # 批量删除明确指定的触发器。
133
+ # @return [Array<Integer>]
134
+ def delete_many(hostid:, triggerids:)
135
+ ids = normalized_ids(triggerids)
136
+ verify_host_ownership!(hostid, ids)
137
+ result = @client.api_request(method: "trigger.delete", params: ids)
138
+ response_ids(result, expected: ids)
139
+ end
56
140
 
57
- log "[DEBUG] expression: #{dump[:expression]}\n data: #{data[:expression]}"
141
+ # 使用 trigger.update 完整替换一个触发器的依赖集合。
142
+ # 传入空集合可清除依赖,返回被更新的触发器 ID。
143
+ # @return [Integer]
144
+ def replace_dependencies(hostid:, triggerid:, depends_on:, allow_cross_host_dependencies: false)
145
+ validate_boolean!(allow_cross_host_dependencies, "allow_cross_host_dependencies")
146
+ id = normalized_ids([triggerid]).first
147
+ dependency_ids = normalized_ids(depends_on, allow_empty: true)
148
+ dependencies = dependency_ids.map { |value| { triggerid: value } }
149
+ raise Invalid, "trigger cannot depend on itself" if dependencies.any? { |item| item[:triggerid] == id }
58
150
 
59
- if hash_equals?(dump, data)
60
- log "[DEBUG] 键 #{dump} 和 #{data} 相等,跳过 safe_update"
61
- item_id
62
- else
63
- data[:expression] = old_expression
64
- # disable old trigger
65
- log "[DEBUG] disable :" + @client.api_request(method: "#{method_name}.update",
66
- params: [{
67
- triggerid: data[:triggerid], status: "1"
68
- }]).inspect
69
- # create new trigger
70
- data.delete(:triggerid)
71
- create(data)
151
+ verify_host_ownership!(hostid, [id])
152
+ verify_host_ownership!(hostid, dependency_ids) if dependency_ids.any? && !allow_cross_host_dependencies
153
+ @client.with_upsert_lock("trigger-dependencies:#{id}") { update_dependencies(id, dependencies) }
154
+ end
155
+
156
+ # 在保留现有依赖的前提下,为触发器追加一个或多个依赖。
157
+ # 读改写过程按触发器 ID 加锁,跨进程可通过 Client 的 upsert_lock 协调。
158
+ # @return [Integer]
159
+ def add_dependencies(hostid:, triggerid:, depends_on:, allow_cross_host_dependencies: false)
160
+ validate_boolean!(allow_cross_host_dependencies, "allow_cross_host_dependencies")
161
+ id = normalized_ids([triggerid]).first
162
+ additions = normalized_ids(depends_on)
163
+ raise Invalid, "trigger cannot depend on itself" if additions.include?(id)
164
+
165
+ verify_host_ownership!(hostid, [id])
166
+
167
+ @client.with_upsert_lock("trigger-dependencies:#{id}") do
168
+ current = find_by_id(id, select_dependencies: ["triggerid"])
169
+ raise ApiError, "Zabbix trigger #{id} was not found" unless current
170
+
171
+ existing = Array(current["dependencies"]).filter_map { |dependency| dependency["triggerid"] }
172
+ dependency_ids = (existing + additions).uniq
173
+ unless allow_cross_host_dependencies
174
+ verify_host_ownership!(hostid, dependency_ids)
175
+ end
176
+ dependencies = dependency_ids.map { |value| { triggerid: value } }
177
+ update_dependencies(id, dependencies)
72
178
  end
73
179
  end
74
180
 
75
- # 通过 Zabbix API 获取或创建触发器对象
76
- def get_or_create(data)
77
- log "[DEBUG] 调用 #{method_name}.get_or_create 方法,参数: #{data.inspect}"
181
+ # 执行已加锁的触发器幂等写入。
182
+ # @return [Integer]
183
+ # @api private
184
+ private def perform_upsert_for_host(data)
185
+ attributes = data.deep_symbolize_keys
186
+ hostid = attributes.delete(:hostid)
187
+ managed_key = attributes.delete(:managed_key)&.to_s
188
+ raise Invalid, "hostid is required" if hostid.blank?
189
+ raise Invalid, "description is required" if attributes[:description].blank?
190
+ raise Invalid, "expression is required" if attributes[:expression].blank?
191
+
192
+ if managed_key.present?
193
+ raise Invalid, "managed_key is too long" if managed_key.length > MAX_MANAGED_KEY_LENGTH
78
194
 
79
- if (id = get_id(description: data[:description], hostid: data[:hostid]))
80
- id
195
+ attributes[:tags] = Array(attributes[:tags]).reject do |tag|
196
+ tag[:tag].to_s == "zabbix_manager_id" || tag["tag"].to_s == "zabbix_manager_id"
197
+ end
198
+ attributes[:tags] << { tag: "zabbix_manager_id", value: managed_key }
199
+ current = find_managed_for_host(hostid: hostid, managed_key: managed_key)
81
200
  else
82
- create(data)
201
+ ownership_tags = Array(attributes[:tags]).select do |tag|
202
+ tag[:tag].to_s == "managed_by" || tag["tag"].to_s == "managed_by"
203
+ end
204
+ current = find_for_host(hostid: hostid, description: attributes[:description], tags: ownership_tags)
205
+ end
206
+ if current
207
+ triggerid = current["triggerid"]
208
+ response_identifier(triggerid)
209
+ attributes[:tags] = merge_tags(current["tags"], attributes[:tags]) if attributes[:tags]
210
+ result = @client.api_request(method: "trigger.update", params: attributes.merge(triggerid: triggerid))
211
+ response_id(result, expected: [triggerid])
212
+ else
213
+ create_managed_trigger(hostid, managed_key, attributes)
214
+ end
215
+ end
216
+
217
+ # 创建触发器;响应丢失时按管理键回读并核对期望状态。
218
+ # @return [Integer]
219
+ # @api private
220
+ private def create_managed_trigger(hostid, managed_key, attributes)
221
+ result = @client.api_request(method: "trigger.create", params: attributes)
222
+ response_id(result)
223
+ rescue TransportError => original_error
224
+ if managed_key.present?
225
+ current = recover_created_trigger(hostid: hostid, managed_key: managed_key, attributes: attributes)
226
+ end
227
+ return response_identifier(current["triggerid"]) if current
228
+
229
+ raise ResultUnknown,
230
+ "trigger create result is unknown; inspect managed key #{managed_key.inspect} before retrying: " \
231
+ "#{original_error.message}"
232
+ end
233
+
234
+ # 对结果不确定的创建执行短暂退避回读,不重放写请求。
235
+ # @return [Hash, nil]
236
+ # @api private
237
+ private def recover_created_trigger(hostid:, managed_key:, attributes:)
238
+ delays = @client.options.fetch(:uncertain_write_delays, DEFAULT_UNCERTAIN_WRITE_DELAYS)
239
+ delays.each do |delay|
240
+ sleep(delay) if delay.positive?
241
+ current = find_managed_for_host(hostid: hostid, managed_key: managed_key)
242
+ if current && recovered_trigger_matches?(current, attributes)
243
+ response_identifier(current["triggerid"])
244
+ return current
245
+ end
246
+ rescue ApiError, TransportError
247
+ next
83
248
  end
249
+ nil
84
250
  end
85
251
 
86
- # 使用 Zabbix API 创建或更新触发器对象
87
- def create_or_update(data)
88
- triggerid = get_id(description: data[:description], hostid: data[:hostid])
89
- triggerid ? update(data.merge(triggerid: triggerid)) : create(data)
252
+ private def recovered_trigger_matches?(current, attributes)
253
+ desired_tags = Array(attributes[:tags]).map(&:deep_symbolize_keys)
254
+ actual_tags = Array(current["tags"]).map(&:deep_symbolize_keys)
255
+ attributes_match?(current, attributes.except(:tags)) && (desired_tags - actual_tags).empty?
256
+ end
257
+
258
+ # 合并受管标签,同时保留调用方未覆盖的运维标签。
259
+ # @return [Array<Hash>]
260
+ # @api private
261
+ private def merge_tags(existing_tags, desired_tags)
262
+ desired = Array(desired_tags).map(&:deep_symbolize_keys)
263
+ desired_names = desired.map { |tag| tag.fetch(:tag).to_s }
264
+ retained = Array(existing_tags).map(&:deep_symbolize_keys).reject do |tag|
265
+ desired_names.include?(tag[:tag].to_s)
266
+ end
267
+ retained + desired
90
268
  end
91
269
 
92
- # 使用 Zabbix API 添加触发器依赖关系
93
- def add_trigger_dependency(triggerid, depend_on_trigger_id)
94
- log "[DEBUG] 调用 add_trigger_dependency 方法,参数: #{triggerid} #{depend_on_trigger_id}"
270
+ # 回查触发器归属,阻止共享高权限令牌跨主机修改。
271
+ private def verify_host_ownership!(hostid, ids)
272
+ raise Invalid, "hostid is required" if hostid.blank?
95
273
 
96
274
  result = @client.api_request(
97
- method: "trigger.adddependencies",
98
- params: {
99
- triggerid: triggerid,
100
- dependsOnTriggerid: depend_on_trigger_id
101
- }
275
+ method: "trigger.get", params: { hostids: hostid, triggerids: ids, output: ["triggerid"] }
276
+ )
277
+ owned_ids = result.map { |trigger| trigger.fetch("triggerid").to_s }
278
+ foreign_ids = ids - owned_ids
279
+ raise Conflict, "triggers do not belong to host #{hostid}: #{foreign_ids.join(", ")}" if foreign_ids.any?
280
+ end
281
+
282
+ # 使用官方 trigger.update 完整写入依赖集合。
283
+ private def update_dependencies(triggerid, dependencies)
284
+ result = @client.api_request(
285
+ method: "trigger.update", params: { triggerid: triggerid, dependencies: dependencies }
102
286
  )
103
- result.empty? ? nil : result["triggerids"]
287
+ response_id(result, expected: [triggerid])
104
288
  end
105
289
  end
106
290
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZabbixManager
4
+ # 用户组成员和主机组权限替换;空数组表示清空,省略字段不参与更新。
5
+ class UserGroups < Resource
6
+ # @return [String] 原生用户组 API 前缀
7
+ def method_name
8
+ "usergroup"
9
+ end
10
+
11
+ # @return [String] 用户组 ID 字段
12
+ def key
13
+ "usrgrpid"
14
+ end
15
+
16
+ # 完整替换主机组权限;空数组明确清空,Zabbix 6.2 起保留独立的模板组权限。
17
+ # @param user_group_id [Integer, String] 目标用户组 ID
18
+ # @param host_group_ids [Array<Integer, String>] 完整期望主机组集合
19
+ # @param permission [Integer, String] 0 拒绝、2 只读、3 读写
20
+ # @return [Integer] 回执确认的用户组 ID
21
+ def replace_host_group_permissions(user_group_id:, host_group_ids:, permission: 2)
22
+ group_id = normalized_ids([user_group_id]).first
23
+ host_ids = collection_ids(host_group_ids, "host_group_ids", allow_empty: true)
24
+ access = integer_attribute(permission, "permission")
25
+ raise Invalid, "permission must be 0, 2, or 3" unless [0, 2, 3].include?(access)
26
+
27
+ rights_field = api_version >= Gem::Version.new("6.2") ? :hostgroup_rights : :rights
28
+ result = @client.api_request(
29
+ method: "usergroup.update",
30
+ params: { usrgrpid: group_id, rights_field => host_ids.map { |id| { id: id, permission: access } } }
31
+ )
32
+ response_id(result, expected: [group_id])
33
+ end
34
+
35
+ # 完整替换各用户组成员;空 user_ids 明确移除现有成员。
36
+ # @param user_group_ids [Array<Integer, String>] 待更新用户组 ID
37
+ # @param user_ids [Array<Integer, String>] 每组的完整期望用户集合
38
+ # @return [Array<Integer>] 回执确认的全部用户组 ID
39
+ def replace_users(user_group_ids:, user_ids:)
40
+ group_ids = collection_ids(user_group_ids, "user_group_ids")
41
+ users = collection_ids(user_ids, "user_ids", allow_empty: true)
42
+ membership = api_version >= Gem::Version.new("6.0") ?
43
+ { users: users.map { |id| { userid: id } } } : { userids: users }
44
+ result = @client.api_request(
45
+ method: "usergroup.update",
46
+ params: group_ids.map { |id| membership.merge(usrgrpid: id) }
47
+ )
48
+ response_ids(result, expected: group_ids)
49
+ end
50
+
51
+ private
52
+
53
+ def api_version
54
+ Gem::Version.new(@client.api_version)
55
+ end
56
+
57
+ def collection_ids(values, name, allow_empty: false)
58
+ raise Invalid, "#{name} must be an Array" unless values.is_a?(Array)
59
+
60
+ normalized_ids(values, allow_empty: allow_empty)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZabbixManager
4
+ # 主机宏由 hostid + macro 唯一定位,全局宏由 macro 唯一定位。
5
+ class UserMacros < Resource
6
+ # @return [String] 宏业务标识
7
+ def identify
8
+ "macro"
9
+ end
10
+
11
+ # @return [String] API 模块名
12
+ def method_name
13
+ "usermacro"
14
+ end
15
+
16
+ # @return [String] 主机宏 ID 字段
17
+ def key
18
+ "hostmacroid"
19
+ end
20
+
21
+ # @return [Hash] 主机宏复合身份
22
+ def identity_filter(data)
23
+ attributes = data.deep_symbolize_keys
24
+ { macro: attributes.fetch(:macro), hostid: attributes.fetch(:hostid) }
25
+ end
26
+
27
+ # @return [Integer, nil] 唯一主机宏 ID
28
+ def get_id(data)
29
+ attributes = identity_filter(data)
30
+ unique_macro_id(get_full_data(attributes), "hostmacroid")
31
+ end
32
+
33
+ # @return [Integer, nil] 唯一全局宏 ID
34
+ def get_id_global(data)
35
+ attributes = data.deep_symbolize_keys
36
+ unique_macro_id(get_full_data_global(macro: attributes.fetch(:macro)), "globalmacroid")
37
+ end
38
+
39
+ # @return [Array<Hash>] 主机宏查询结果
40
+ def get_full_data(data)
41
+ attributes = data.deep_symbolize_keys
42
+ params = { filter: attributes.except(:hostid) }
43
+ params[:hostids] = attributes[:hostid] if attributes.key?(:hostid)
44
+ get_raw(params)
45
+ end
46
+
47
+ # @return [Array<Hash>] 全局宏查询结果
48
+ def get_full_data_global(data)
49
+ get_raw(globalmacro: true, filter: data.deep_symbolize_keys)
50
+ end
51
+
52
+ # @return [Integer, nil] 新建主机宏 ID
53
+ def create(data)
54
+ write_macro("create", data, "hostmacroids")
55
+ end
56
+
57
+ # @return [Integer, nil] 新建全局宏 ID
58
+ def create_global(data)
59
+ write_macro("createglobal", data, "globalmacroids")
60
+ end
61
+
62
+ # @return [Integer, nil] 删除主机宏 ID
63
+ def delete(ids)
64
+ requested = normalized_ids(ids)
65
+ write_macro("delete", requested, "hostmacroids", expected: requested)
66
+ end
67
+
68
+ # @return [Integer, nil] 删除全局宏 ID
69
+ def delete_global(ids)
70
+ requested = normalized_ids(ids)
71
+ write_macro("deleteglobal", requested, "globalmacroids", expected: requested)
72
+ end
73
+
74
+ # @return [Integer, nil] 更新主机宏 ID
75
+ def update(data)
76
+ write_macro("update", data, "hostmacroids", expected: [data.with_indifferent_access[:hostmacroid]])
77
+ end
78
+
79
+ # @return [Integer, nil] 更新全局宏 ID
80
+ def update_global(data)
81
+ write_macro("updateglobal", data, "globalmacroids", expected: [data.with_indifferent_access[:globalmacroid]])
82
+ end
83
+
84
+ # @return [Integer, nil] 已存在或新建全局宏 ID
85
+ def get_or_create_global(data)
86
+ get_id_global(data) || create_global(data)
87
+ end
88
+
89
+ # @return [Integer, nil] 新建或更新全局宏 ID
90
+ def create_or_update_global(data)
91
+ attributes = data.deep_symbolize_keys
92
+ id = get_id_global(attributes)
93
+ id ? update_global(attributes.merge(globalmacroid: id)) : create_global(attributes)
94
+ end
95
+
96
+ private
97
+
98
+ def unique_macro_id(result, id_field)
99
+ raise Conflict, "multiple macros match the requested identity" if result.length > 1
100
+
101
+ response_identifier(result.first[id_field], id_field) if result.first
102
+ end
103
+
104
+ def write_macro(operation, params, id_field, expected: nil)
105
+ normalized_ids(expected) if expected
106
+ result = @client.api_request(method: "usermacro.#{operation}", params: params)
107
+ expected ? response_ids(result, id_field, expected: expected).first : response_id(result, id_field)
108
+ end
109
+ end
110
+ end
@@ -1,39 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ZabbixManager
4
- class Users < Basic
5
- # 用于通过 Zabbix API 与 Users 交互的方法名称
4
+ # 用户资源与媒介配置替换;字段名称按服务器版本转换。
5
+ class Users < Resource
6
+ # @return [String] API 模块名
6
7
  def method_name
7
8
  "user"
8
9
  end
9
10
 
10
- # 用于通过 Zabbix API 识别特定 User 对象的 id 字段名称
11
+ # @return [String] 用户名字段
11
12
  def identify
12
- "alias"
13
+ Gem::Version.new(@client.api_version) >= Gem::Version.new("5.4") ? "username" : "alias"
13
14
  end
14
15
 
15
- # 用于辅助处理添加或更新用户媒体信息的方法
16
- def medias_helper(data, action)
16
+ # 替换指定用户的完整媒介配置。
17
+ # @param data [Hash] 包含 userids 和 media
18
+ # @return [Integer, nil] 首个更新用户的 ID
19
+ def update_medias(data)
20
+ attributes = data.deep_symbolize_keys
21
+ ids = normalized_ids(attributes.fetch(:userids))
22
+ media_field = Gem::Version.new(@client.api_version) >= Gem::Version.new("5.4") ? :medias : :user_medias
17
23
  result = @client.api_request(
18
- method: "user.#{action}",
19
- params: data[:userids].map do |i|
20
- {
21
- userid: i,
22
- user_medias: data[:media]
23
- }
24
+ method: "user.update",
25
+ params: attributes.fetch(:userids).map do |userid|
26
+ { userid: userid, media_field => attributes.fetch(:media) }
24
27
  end
25
28
  )
26
- result ? result["userids"][0].to_i : nil
27
- end
28
-
29
- # 通过 Zabbix API 为用户添加媒体信息
30
- def add_medias(data)
31
- medias_helper(data, "update")
32
- end
33
-
34
- # 通过 Zabbix API 更新用户媒体信息
35
- def update_medias(data)
36
- medias_helper(data, "update")
29
+ response_ids(result, expected: ids).first
37
30
  end
38
31
  end
39
32
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZabbixManager
4
+ # 原生值映射资源;所属范围与映射规则由调用方提供。
5
+ class ValueMaps < Resource
6
+ # 返回 Zabbix API 中值映射对象的方法名前缀。
7
+ #
8
+ # @return [String] 值映射对象的方法名前缀
9
+ def method_name
10
+ "valuemap"
11
+ end
12
+
13
+ # 返回用于唯一识别值映射的业务字段名。
14
+ #
15
+ # @return [String] 值映射对象的业务标识字段名
16
+ def identify
17
+ "name"
18
+ end
19
+
20
+ # 5.4 起值映射属于主机或模板;旧版仍按全局名称定位。
21
+ # @return [Hash] 带版本边界的精确身份条件
22
+ def identity_filter(data)
23
+ attributes = data.deep_symbolize_keys
24
+ identity = super(attributes)
25
+ if host_scoped?
26
+ raise Invalid, "hostid is required for a host-scoped value map" unless attributes.key?(:hostid)
27
+
28
+ identity[:hostid] = normalized_ids([attributes[:hostid]]).first
29
+ end
30
+ identity
31
+ end
32
+
33
+ # 名称查询始终经过完整身份检查,避免跨主机合法重名造成误选。
34
+ # @return [Integer, nil] 唯一值映射 ID
35
+ def get_id(data)
36
+ super(identity_filter(data))
37
+ end
38
+
39
+ # hostid 用于身份查询和创建,不能用于移动已有值映射。
40
+ # @return [Integer] 已确认的值映射 ID
41
+ def update(data)
42
+ attributes = data.deep_symbolize_keys
43
+ super(host_scoped? ? attributes.except(:hostid) : attributes)
44
+ end
45
+
46
+ private
47
+
48
+ def host_scoped?
49
+ Gem::Version.new(@client.api_version) >= Gem::Version.new("5.4")
50
+ end
51
+ end
52
+ end