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,23 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ZabbixManager
4
- class Graphs < Basic
5
- # 用于通过 Zabbix API 与 Graphs 交互的方法名称
4
+ # 图形以 hostid + name 查询;写入所有者由 gitems 中的监控项确定。
5
+ class Graphs < Resource
6
+ # 返回图形对象对应的 Zabbix API 方法前缀。
7
+ #
8
+ # @return [String]
6
9
  def method_name
7
10
  "graph"
8
11
  end
9
12
 
10
- # 用于通过 Zabbix API 标识特定 Graph 对象的 id 字段名称
11
- def identify
12
- "name"
13
- end
14
-
15
- # 从 Zabbix API 获取完整/扩展的 Graph 数据
13
+ # 按名称搜索并获取图形的完整数据。
14
+ #
15
+ # @param data [Hash] 包含图形识别字段及其值的查询条件
16
+ # @raise [ApiError] Zabbix API 返回业务错误时抛出
17
+ # @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
18
+ # @return [Array<Hash>] 匹配的图形完整数据
16
19
  def get_full_data(data)
17
- log "[DEBUG] 使用参数调用 get_full_data: #{data.inspect}"
18
-
19
- get_raw(
20
- {
20
+ @client.api_request(
21
+ method: "#{method_name}.get",
22
+ params: {
21
23
  search: {
22
24
  identify.to_sym => data[identify.to_sym]
23
25
  },
@@ -26,10 +28,16 @@ class ZabbixManager
26
28
  )
27
29
  end
28
30
 
29
- # 从 Zabbix API 获取主机的 Graph ids
31
+ # 获取指定主机的图形 ID,并可按名称片段进一步过滤。
32
+ #
33
+ # @param data [Hash] 包含 host,且可包含 filter 的查询条件
34
+ # @raise [ApiError] Zabbix API 返回业务错误时抛出
35
+ # @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
36
+ # @return [Array] 匹配的图形 ID 列表
30
37
  def get_ids_by_host(data)
31
- result = get_raw(
32
- {
38
+ result = @client.api_request(
39
+ method: "graph.get",
40
+ params: {
33
41
  filter: {
34
42
  host: data[:host]
35
43
  },
@@ -38,45 +46,48 @@ class ZabbixManager
38
46
  )
39
47
 
40
48
  result.filter_map do |graph|
41
- graphid = graph["graphid"]
42
- name = graph["name"]
43
- filter = data[:filter]
49
+ num = graph["graphid"]
50
+ name = graph["name"]
51
+ filter = data[:filter]
44
52
 
45
- graphid if filter.nil? || name.match?(/#{filter}/)
53
+ num if filter.nil? || name.include?(filter.to_s)
46
54
  end
47
55
  end
48
56
 
49
- # 通过 Zabbix API 获取 Graph Item 对象
57
+ # 获取指定图形包含的图形监控项。
58
+ #
59
+ # @param data [Hash, String, Integer] 图形 ID
60
+ # @raise [ApiError] Zabbix API 返回业务错误时抛出
61
+ # @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
62
+ # @return [Array<Hash>] 图形监控项数据
50
63
  def get_items(data)
51
64
  @client.api_request(
52
65
  method: "graphitem.get",
53
66
  params: {
54
67
  graphids: [data],
55
- output: "extend"
68
+ output: "extend"
56
69
  }
57
70
  )
58
71
  end
59
72
 
60
- # 通过 Zabbix API 获取或创建 Graph 对象
61
- def get_or_create(data)
62
- log "[DEBUG] 使用参数调用 get_or_create: #{data.inspect}"
63
-
64
- unless (id = get_id(name: data[:name], templateid: data[:templateid]))
65
- id = create(data)
66
- end
67
-
68
- id
73
+ # 生成由图形名称和所属主机或模板构成的稳定查询条件。
74
+ # graph.templateid 是继承源图形 ID,不是所属模板 ID。
75
+ # @param data [Hash] 包含 name 和 hostid 的图形属性
76
+ # @return [Hash] 图形唯一查询条件
77
+ def identity_filter(data)
78
+ attributes = data.deep_symbolize_keys
79
+ { name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
69
80
  end
70
81
 
71
- # 通过 Zabbix API 创建或更新 Graph 对象
72
- def create_or_update(data)
73
- graphid = get_id(name: data[:name], templateid: data[:templateid])
74
- graphid ? _update(data.merge(graphid: graphid)) : create(data)
82
+ # 图形所有者由 gitems 确定,hostid 仅用于调用方的身份查询。
83
+ # @return [Integer, nil]
84
+ def create(data)
85
+ super(data.deep_symbolize_keys.except(:hostid))
75
86
  end
76
87
 
77
- def _update(data)
78
- data.delete(:name)
79
- update(data)
88
+ # @return [Integer, nil] 更新的图形 ID
89
+ def update(data)
90
+ super(data.deep_symbolize_keys.except(:hostid))
80
91
  end
81
92
  end
82
93
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZabbixManager
4
+ # 主机群组名称解析与按需创建;批量创建按顺序执行,不是远端事务。
5
+ class HostGroups < Resource
6
+ # 返回主机群组对应的 Zabbix API 方法前缀。
7
+ #
8
+ # @return [String]
9
+ def method_name
10
+ "hostgroup"
11
+ end
12
+
13
+ # 返回主机群组的 Zabbix ID 字段名。
14
+ #
15
+ # @return [String]
16
+ def key
17
+ "groupid"
18
+ end
19
+
20
+ # 批量解析主机群组名称并返回对应的群组 ID。
21
+ # @param data [Array<String>, String] 待查询的群组名称
22
+ # @return [Array<Hash>, nil] 群组 ID 列表,未命中时返回 nil
23
+ def get_hostgroup_ids(data)
24
+ names = normalized_names(data)
25
+ return nil if names.empty?
26
+
27
+ groups = @client.api_request(
28
+ method: "hostgroup.get",
29
+ params: {
30
+ output: %w[groupid name],
31
+ filter: { name: names }
32
+ }
33
+ )
34
+
35
+ groups.empty? ? nil : groups.map { |group| { groupid: group.fetch("groupid") } }
36
+ end
37
+
38
+ # 批量查询并创建缺失的主机群组,避免逐名称重复查询。
39
+ # @param data [Array<String>, String] 待确保存在的群组名称
40
+ # @return [Array<Hash>] 群组 ID 列表
41
+ def get_or_create_host_groups(data)
42
+ names = normalized_names(data)
43
+ existing = @client.api_request(
44
+ method: "hostgroup.get",
45
+ params: { output: %w[groupid name], filter: { name: names } }
46
+ ).index_by { |group| group.fetch("name") }
47
+
48
+ names.map do |name|
49
+ group = existing[name]
50
+ next({ groupid: response_identifier(group["groupid"]).to_s }) if group
51
+
52
+ result = @client.api_request(method: "hostgroup.create", params: { name: name })
53
+ { groupid: response_id(result).to_s }
54
+ end
55
+ end
56
+
57
+ # 清理、去空并去重主机群组名称。
58
+ # @param data [Array<String>, String] 原始群组名称
59
+ # @return [Array<String>] 规范化后的群组名称
60
+ private def normalized_names(data)
61
+ Array(data).filter_map { |name| name.to_s.strip.presence }.uniq
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZabbixManager
4
+ # 封装高频使用的 Zabbix 主机接口查询与幂等更新。
5
+ class HostInterfaces < Resource
6
+ # 返回对应的 Zabbix API 模块名。
7
+ # @return [String]
8
+ def method_name
9
+ "hostinterface"
10
+ end
11
+
12
+ # 返回接口对象的主键字段。
13
+ # @return [String]
14
+ def key
15
+ "interfaceid"
16
+ end
17
+
18
+ # 使用接口 ID 作为通用查询标识。
19
+ # @return [String]
20
+ def identify
21
+ "interfaceid"
22
+ end
23
+
24
+ # 查询指定主机的全部接口,可选择同时返回关联监控项。
25
+ # @return [Array<Hash>]
26
+ def for_host(hostid, select_items: nil)
27
+ raise Invalid, "hostid is required" if hostid.blank?
28
+
29
+ params = { hostids: hostid, output: "extend" }
30
+ params[:selectItems] = select_items if select_items
31
+ @client.api_request(method: "hostinterface.get", params: params)
32
+ end
33
+
34
+ # 规范化并校验接口定义,不执行远端查询或写入。
35
+ # @return [Array<Hash>]
36
+ def validate(interfaces)
37
+ desired = Array.wrap(interfaces).map { |interface| normalize_definition(interface) }
38
+ validate_definitions!(desired)
39
+ desired
40
+ end
41
+
42
+ # 校验 host.create 中全部接口的创建必填合同。
43
+ # @return [Array<Hash>]
44
+ def validate_for_create(interfaces)
45
+ validate(interfaces).each { |attributes| validate_create_definition!(attributes) }
46
+ end
47
+
48
+ # 只读取并校验接口归属和创建必填字段,供多资源编排在首个写入前预检。
49
+ # @param hostid [Integer, String] 目标主机 ID
50
+ # @param interfaces [Array<Hash>] 期望接口定义
51
+ # @return [Array<Hash>] 已验证的接口属性;执行时仍需重新核实远端状态
52
+ def validate_for_host(hostid:, interfaces:)
53
+ plan_for_host(hostid: hostid, interfaces: interfaces).map(&:first)
54
+ end
55
+
56
+ # 幂等创建或更新一个主机的接口集合。
57
+ # @return [Array<Integer>]
58
+ def reconcile_for_host(hostid:, interfaces:)
59
+ @client.with_upsert_lock("host-interfaces:#{hostid}") do
60
+ plan = plan_for_host(hostid: hostid, interfaces: interfaces)
61
+ apply_plan(hostid: hostid, plan: plan)
62
+ end
63
+ end
64
+
65
+ # 删除明确指定的主机接口;关联监控项由 Zabbix 按官方规则处理。
66
+ # @return [Array<Integer>]
67
+ def delete_many(hostid:, interfaceids:)
68
+ ids = normalized_ids(interfaceids)
69
+
70
+ owned_ids = for_host(hostid).map { |interface| interface.fetch("interfaceid").to_s }
71
+ foreign_ids = ids - owned_ids
72
+ raise Conflict, "interfaces do not belong to host #{hostid}: #{foreign_ids.join(", ")}" if foreign_ids.any?
73
+
74
+ result = @client.api_request(method: "hostinterface.delete", params: ids)
75
+ response_ids(result, expected: ids)
76
+ end
77
+
78
+ private
79
+
80
+ # 预先校验并生成仅供当前对象执行的接口变更计划。
81
+ def plan_for_host(hostid:, interfaces:)
82
+ desired = validate(interfaces)
83
+ existing = for_host(hostid)
84
+ plans = desired.map do |attributes|
85
+ current = matching_interface(existing, attributes)
86
+ response_identifier(current["interfaceid"]) if current
87
+ validate_create_definition!(attributes) unless current
88
+ [attributes, current]
89
+ end
90
+ targets = plans.filter_map { |_attributes, current| current && response_identifier(current["interfaceid"]) }
91
+ raise Invalid, "duplicate resolved interface target" unless targets.uniq == targets
92
+
93
+ plans
94
+ end
95
+
96
+ # 执行内部生成的接口变更计划,避免调用方伪造跨主机接口 ID。
97
+ def apply_plan(hostid:, plan:)
98
+ plan.map do |attributes, current|
99
+ current ? update_interface(attributes, current) : create_interface(hostid, attributes)
100
+ end
101
+ end
102
+
103
+ # 把接口字段递归转换为统一符号键。
104
+ def normalize_definition(interface)
105
+ raise Invalid, "interface must be a hash" unless interface.is_a?(Hash)
106
+
107
+ attributes = interface.deep_symbolize_keys
108
+ if attributes.key?(:type)
109
+ type = integer_attribute(attributes[:type], "interface type")
110
+ raise Invalid, "interface type must be 1, 2, 3, or 4" unless [1, 2, 3, 4].include?(type)
111
+ end
112
+ %i[main useip].each do |field|
113
+ next unless attributes.key?(field)
114
+
115
+ unless [0, 1].include?(integer_attribute(attributes[field], "interface #{field}"))
116
+ raise Invalid, "interface #{field} must be 0 or 1"
117
+ end
118
+ end
119
+ %i[ip dns].each do |field|
120
+ if attributes.key?(field) && !attributes[field].is_a?(String)
121
+ raise Invalid, "interface #{field} must be a string"
122
+ end
123
+ end
124
+ validate_port!(attributes[:port]) if attributes.key?(:port)
125
+ details = attributes[:details]
126
+ if attributes.key?(:details) && !details.is_a?(Hash)
127
+ raise Invalid, "interface details must be a hash"
128
+ end
129
+
130
+ validate_supplied_snmp_details!(details) if details
131
+ attributes
132
+ end
133
+
134
+ def validate_supplied_snmp_details!(details)
135
+ validate_snmp_version!(details[:version]) if details.key?(:version)
136
+ %i[community securityname authpassphrase privpassphrase contextname].each do |field|
137
+ if details.key?(field) && !details[field].is_a?(String)
138
+ raise Invalid, "SNMP #{field} must be a string"
139
+ end
140
+ end
141
+ { bulk: [0, 1], securitylevel: [0, 1, 2] }.each do |field, allowed|
142
+ next unless details.key?(field)
143
+
144
+ unless allowed.include?(integer_attribute(details[field], "SNMP #{field}"))
145
+ raise Invalid, "SNMP #{field} is invalid"
146
+ end
147
+ end
148
+ end
149
+
150
+ def validate_port!(value)
151
+ text = value.to_s
152
+ numeric = (value.is_a?(String) || value.is_a?(Integer)) && text.match?(/\A[1-9]\d*\z/)
153
+ macro = value.is_a?(String) && text.match?(/\A\{\$[^{}\r\n]+\}\z/)
154
+ return if macro || (numeric && value.to_i <= 65_535)
155
+
156
+ raise Invalid, "interface port must be between 1 and 65535 or a user macro"
157
+ end
158
+
159
+ # 校验新接口的通用字段及 SNMP 条件字段。
160
+ def validate_create_definition!(attributes)
161
+ raise Invalid, "new interfaces cannot reference an existing interfaceid" if attributes.key?(:interfaceid)
162
+
163
+ attributes[:ip] = "" unless attributes.key?(:ip)
164
+ attributes[:dns] = "" unless attributes.key?(:dns)
165
+ endpoint_identity(attributes)
166
+ validate_snmp_details!(attributes) if attributes[:type].to_i == 2
167
+ end
168
+
169
+ # 校验 SNMP 版本和 v1/v2c community 必填合同。
170
+ def validate_snmp_details!(attributes)
171
+ details = attributes[:details]&.deep_symbolize_keys
172
+ raise Invalid, "SNMP interface details are required" unless details
173
+
174
+ version = validate_snmp_version!(details[:version])
175
+ if [1, 2].include?(version) && details[:community].blank?
176
+ raise Invalid, "SNMP v1/v2 interface community is required"
177
+ end
178
+
179
+ attributes[:details] = details
180
+ end
181
+
182
+ def validate_snmp_version!(value)
183
+ version = integer_attribute(value, "SNMP interface version")
184
+ raise Invalid, "SNMP interface version must be 1, 2, or 3" unless [1, 2, 3].include?(version)
185
+
186
+ version
187
+ end
188
+
189
+ # 校验期望接口是否包含重复身份。
190
+ # @return [void]
191
+ # @api private
192
+ def validate_definitions!(interfaces)
193
+ identities = interfaces.map { |interface| interface_identity(interface) }
194
+ duplicate = identities.tally.find { |_identity, count| count > 1 }&.first
195
+ raise Invalid, "duplicate desired interface identity #{duplicate}" if duplicate
196
+ end
197
+
198
+ # 按显式 ID 或稳定端点身份匹配已有接口。
199
+ # @return [Hash, nil]
200
+ # @api private
201
+ def matching_interface(existing, attributes)
202
+ if attributes[:interfaceid]
203
+ match = existing.find { |item| item["interfaceid"].to_s == attributes[:interfaceid].to_s }
204
+ raise Invalid, "interfaceid #{attributes[:interfaceid]} does not belong to this host" unless match
205
+
206
+ return match
207
+ end
208
+
209
+ candidates = existing.select { |item| endpoint_identity(item) == endpoint_identity(attributes) }
210
+ raise Conflict, "interface identity is ambiguous; provide interfaceid" if candidates.length > 1
211
+
212
+ candidates.first
213
+ end
214
+
215
+ # 生成不依赖数组顺序的接口身份。
216
+ # @return [String]
217
+ # @api private
218
+ def interface_identity(attributes)
219
+ values = attributes.with_indifferent_access
220
+ return "id:#{values[:interfaceid]}" if values[:interfaceid].present?
221
+
222
+ endpoint_identity(values)
223
+ end
224
+
225
+ # 使用类型、端点和端口匹配接口,不把可变的 main 或远端 ID 作为端点身份。
226
+ def endpoint_identity(attributes)
227
+ values = attributes.with_indifferent_access
228
+
229
+ %i[type main useip port].each do |field|
230
+ raise Invalid, "interface #{field} is required" if values[field].blank?
231
+ end
232
+ endpoint_field = values[:useip].to_i == 1 ? :ip : :dns
233
+ raise Invalid, "interface #{endpoint_field} is required" if values[endpoint_field].blank?
234
+
235
+ "endpoint:#{values[:type]}:#{values[:useip]}:#{values[endpoint_field]}:#{values[:port]}"
236
+ end
237
+
238
+ # 更新已属于目标主机的接口。
239
+ # @return [Integer]
240
+ # @api private
241
+ def update_interface(attributes, current)
242
+ interfaceid = current.fetch("interfaceid")
243
+ result = @client.api_request(
244
+ method: "hostinterface.update",
245
+ params: attributes.except(:hostid).merge(interfaceid: interfaceid)
246
+ )
247
+ response_id(result, expected: [interfaceid])
248
+ end
249
+
250
+ # 为目标主机创建缺失接口。
251
+ # @return [Integer]
252
+ # @api private
253
+ def create_interface(hostid, attributes)
254
+ result = @client.api_request(
255
+ method: "hostinterface.create",
256
+ params: attributes.except(:interfaceid).merge(hostid: hostid)
257
+ )
258
+ response_id(result)
259
+ end
260
+ end
261
+ end