zabbix_manager 5.1.6 → 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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +69 -46
  4. data/lib/zabbix_manager/classes/actions.rb +3 -11
  5. data/lib/zabbix_manager/classes/applications.rb +2 -8
  6. data/lib/zabbix_manager/classes/configurations.rb +4 -9
  7. data/lib/zabbix_manager/classes/{drules.rb → discovery_rules.rb} +2 -8
  8. data/lib/zabbix_manager/classes/errors.rb +3 -0
  9. data/lib/zabbix_manager/classes/events.rb +2 -1
  10. data/lib/zabbix_manager/classes/graphs.rb +19 -15
  11. data/lib/zabbix_manager/classes/{hostgroups.rb → host_groups.rb} +5 -11
  12. data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
  13. data/lib/zabbix_manager/classes/hosts.rb +135 -89
  14. data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
  15. data/lib/zabbix_manager/classes/items.rb +91 -87
  16. data/lib/zabbix_manager/classes/maintenance.rb +2 -1
  17. data/lib/zabbix_manager/classes/{mediatypes.rb → media_types.rb} +2 -7
  18. data/lib/zabbix_manager/classes/problems.rb +11 -40
  19. data/lib/zabbix_manager/classes/proxies.rb +5 -15
  20. data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
  21. data/lib/zabbix_manager/classes/roles.rb +13 -19
  22. data/lib/zabbix_manager/classes/screens.rb +11 -30
  23. data/lib/zabbix_manager/classes/scripts.rb +2 -8
  24. data/lib/zabbix_manager/classes/server.rb +1 -0
  25. data/lib/zabbix_manager/classes/templates.rb +2 -65
  26. data/lib/zabbix_manager/classes/triggers.rb +42 -45
  27. data/lib/zabbix_manager/classes/user_groups.rb +63 -0
  28. data/lib/zabbix_manager/classes/user_macros.rb +110 -0
  29. data/lib/zabbix_manager/classes/users.rb +15 -54
  30. data/lib/zabbix_manager/classes/value_maps.rb +52 -0
  31. data/lib/zabbix_manager/client.rb +148 -277
  32. data/lib/zabbix_manager/configuration.rb +107 -0
  33. data/lib/zabbix_manager/http_transport.rb +145 -97
  34. data/lib/zabbix_manager/log_sanitizer.rb +9 -19
  35. data/lib/zabbix_manager/monitoring/device.rb +393 -0
  36. data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
  37. data/lib/zabbix_manager/monitoring/line.rb +175 -0
  38. data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
  39. data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
  40. data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
  41. data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
  42. data/lib/zabbix_manager/monitoring/traffic_items.rb +132 -0
  43. data/lib/zabbix_manager/monitoring/validation.rb +92 -0
  44. data/lib/zabbix_manager/monitoring.rb +216 -642
  45. data/lib/zabbix_manager/resource.rb +235 -0
  46. data/lib/zabbix_manager/traffic.rb +238 -0
  47. data/lib/zabbix_manager/version.rb +1 -1
  48. data/lib/zabbix_manager.rb +66 -179
  49. data/zabbix_manager.gemspec +6 -22
  50. metadata +41 -190
  51. data/lib/zabbix_manager/basic/basic_alias.rb +0 -42
  52. data/lib/zabbix_manager/basic/basic_func.rb +0 -55
  53. data/lib/zabbix_manager/basic/basic_init.rb +0 -50
  54. data/lib/zabbix_manager/basic/basic_logic.rb +0 -239
  55. data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -189
  56. data/lib/zabbix_manager/classes/usergroups.rb +0 -75
  57. data/lib/zabbix_manager/classes/usermacros.rb +0 -222
  58. data/lib/zabbix_manager/classes/valuemaps.rb +0 -26
@@ -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
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ZabbixManager
4
- class Hosts < Basic
4
+ # 主机身份解析、状态对账与模板关联;技术名称和显示名称分别处理。
5
+ class Hosts < Resource
5
6
  # 返回主机对应的 Zabbix API 模块名。
6
7
  def method_name
7
8
  "host"
@@ -22,7 +23,6 @@ class ZabbixManager
22
23
 
23
24
  # 按主机 ID 查询完整主机及接口、群组和模板。
24
25
  def dump_by_id(data)
25
- log "[DEBUG] Call dump_by_id with parameters: #{data.inspect}"
26
26
  @client.api_request(
27
27
  method: "host.get",
28
28
  params: {
@@ -35,16 +35,25 @@ class ZabbixManager
35
35
  )
36
36
  end
37
37
 
38
- # 从指定主机批量解除模板关联。
39
- def unlink_templates(data)
40
- result = @client.api_request(
41
- method: "host.massRemove",
42
- params: {
43
- hostids: data[:hosts_id],
44
- templateids: data[:templates_id]
45
- }
46
- )
47
- !result.empty?
38
+ # 向指定主机追加模板关联,保留现有模板。
39
+ # @return [Array<Integer>] 已更新主机 ID
40
+ def link_templates(host_ids:, template_ids:)
41
+ write_templates("massadd", host_ids, template_ids)
42
+ end
43
+
44
+ # 完整替换指定主机的模板关联,空模板集合明确清除全部关联。
45
+ # @return [Array<Integer>] 已更新主机 ID
46
+ def replace_templates(host_ids:, template_ids:)
47
+ write_templates("massupdate", host_ids, template_ids, allow_empty: true)
48
+ end
49
+
50
+ # 从指定主机解除选定模板的关联,保留模板生成的实体。
51
+ # @return [Array<Integer>] 已更新主机 ID
52
+ def unlink_templates(host_ids:, template_ids:)
53
+ ids = normalized_ids(host_ids)
54
+ templates = normalized_ids(template_ids)
55
+ result = @client.api_request(method: "host.massremove", params: { hostids: ids, templateids: templates })
56
+ response_ids(result, expected: ids)
48
57
  end
49
58
 
50
59
  # 使用显式群组和接口创建主机,不注入隐藏凭据。
@@ -52,7 +61,7 @@ class ZabbixManager
52
61
  attributes = default_options.merge(data.deep_symbolize_keys)
53
62
  validate_create_attributes!(attributes)
54
63
  result = @client.api_request(method: "host.create", params: attributes)
55
- first_id(result, "hostids")
64
+ response_id(result)
56
65
  end
57
66
 
58
67
  # 规范化并校验设备定义,不执行远端查询或写入。
@@ -62,7 +71,7 @@ class ZabbixManager
62
71
  attributes = data.deep_symbolize_keys
63
72
  raise Invalid, "host is required" if attributes[:host].blank?
64
73
 
65
- hostinterfaces.validate(attributes[:interfaces]) if attributes.key?(:interfaces)
74
+ host_interfaces.validate(attributes[:interfaces]) if attributes.key?(:interfaces)
66
75
  attributes
67
76
  end
68
77
 
@@ -90,19 +99,19 @@ class ZabbixManager
90
99
  attributes = attributes.dup
91
100
 
92
101
  interfaces = Array.wrap(attributes.delete(:interfaces))
93
- hostinterfaces.validate(interfaces)
102
+ host_interfaces.validate(interfaces)
94
103
  update_attributes = attributes.merge(hostid: hostid)
95
- @client.api_request(method: "host.update", params: update_attributes)
96
- hostinterfaces.reconcile_for_host(hostid: hostid, interfaces: interfaces) if interfaces.any?
97
- hostid.to_i
104
+ host_interfaces.reconcile_for_host(hostid: hostid, interfaces: interfaces) if interfaces.any?
105
+ result = @client.api_request(method: "host.update", params: update_attributes)
106
+ response_id(result, expected: [hostid])
98
107
  end
99
108
 
100
109
  # 查询指定主机的全部接口。
101
110
  def interfaces_for(hostid)
102
- hostinterfaces.for_host(hostid)
111
+ host_interfaces.for_host(hostid)
103
112
  end
104
113
 
105
- # 返回主机的第一个接口 ID;高频业务应优先使用 hostinterfaces。
114
+ # 返回主机的第一个接口 ID;高频业务应优先使用 host_interfaces。
106
115
  def get_interface_id(hostid)
107
116
  interfaces_for(hostid).first&.fetch("interfaceid", nil)
108
117
  end
@@ -139,95 +148,132 @@ class ZabbixManager
139
148
  find_host_by_filter(hostid: hostid)
140
149
  end
141
150
 
151
+ # 解析供流量查询和监控表达式使用的唯一主机;只执行读取。
152
+ # 显式 ID 与技术名必须同时匹配,候选名称不能分别指向不同主机。
153
+ # @param reference [String, Array<String>, Hash] 名称/IP/DNS 候选,或同时包含 hostid 和 host 的 Hash
154
+ # @return [Hash] 符号键主机属性,包含 hostid 和技术名 host
155
+ # @raise [Invalid] 本地主机引用无效
156
+ # @raise [ApiError] 主机不存在或当前 API 用户不可见
157
+ # @raise [Conflict] 候选不唯一,或 ID 与技术名不匹配
158
+ # @raise [ProtocolError] API 返回无效的主机身份
159
+ def resolve(reference)
160
+ if reference.is_a?(Hash)
161
+ expected = Monitoring::Validation.hash!(reference, "host")
162
+ Monitoring::Validation.host!(expected)
163
+ found = find_by_id(expected[:hostid])
164
+ else
165
+ candidates = Array.wrap(reference).map do |candidate|
166
+ Monitoring::Validation.text!(candidate, "host candidate").strip
167
+ end
168
+ raise Invalid, "host candidates are required" if candidates.empty?
169
+
170
+ found = find_by_candidates(candidates.uniq)
171
+ end
172
+ raise ApiError, "Zabbix host was not found for the supplied reference" unless found
173
+
174
+ resolved = resolved_host(found)
175
+ if expected && (resolved[:hostid].to_s != expected[:hostid].to_s || resolved[:host] != expected[:host])
176
+ raise Conflict, "hostid and technical host name do not refer to the same Zabbix host"
177
+ end
178
+
179
+ resolved
180
+ end
181
+
142
182
  # 批量启用或停用主机。
143
183
  # @return [Array<Integer>]
144
184
  def set_status(hostids, enabled:)
145
- ids = Array(hostids).filter_map { |value| value.to_s.strip.presence }.uniq
146
- raise Invalid, "hostids are required" if ids.empty?
185
+ validate_boolean!(enabled, "enabled")
186
+ ids = normalized_ids(hostids)
147
187
 
148
188
  params = ids.map { |hostid| { hostid: hostid, status: enabled ? 0 : 1 } }
149
189
  result = @client.api_request(method: "host.update", params: params)
150
- Array(result.fetch("hostids")).map(&:to_i)
151
- end
152
-
153
- # 把可见名称对应的主机更新为稳定技术标识。
154
- def update_host_to_serial(data)
155
- attributes = data.deep_symbolize_keys
156
- hostid = get_hostid_by_name(attributes[:name])
157
- return nil unless hostid
158
-
159
- attributes.delete(:templates)
160
- @client.api_request(method: "host.update", params: attributes.merge(hostid: hostid))
161
- hostid.to_i
190
+ response_ids(result, expected: ids)
162
191
  end
163
192
 
164
193
  private
165
194
 
166
- # 校验创建主机所需的技术名、群组和接口。
167
- def validate_create_attributes!(attributes)
168
- raise Invalid, "host is required" if attributes[:host].blank?
169
- raise Invalid, "groups are required when creating a host" if Array.wrap(attributes[:groups]).empty?
170
- raise Invalid, "interfaces are required when creating a host" if Array.wrap(attributes[:interfaces]).empty?
195
+ # 远端身份损坏是协议错误,不能当作调用方输入错误或不存在处理。
196
+ def resolved_host(found)
197
+ resolved = Monitoring::Validation.hash!(found, "Zabbix host")
198
+ Monitoring::Validation.host!(resolved)
199
+ resolved
200
+ rescue Invalid
201
+ raise ProtocolError, "invalid host response: hostid and technical host are required", cause: nil
202
+ end
171
203
 
172
- attributes[:interfaces] = hostinterfaces.validate_for_create(attributes[:interfaces])
173
- end
204
+ def write_templates(operation, host_ids, template_ids, allow_empty: false)
205
+ ids = normalized_ids(host_ids)
206
+ templates = normalized_ids(template_ids, allow_empty: allow_empty)
207
+ result = @client.api_request(
208
+ method: "host.#{operation}",
209
+ params: {
210
+ hosts: ids.map { |id| { hostid: id } },
211
+ templates: templates.map { |id| { templateid: id } }
212
+ }
213
+ )
214
+ response_ids(result, expected: ids)
215
+ end
174
216
 
175
- # 使用精确过滤查询唯一主机。
176
- def find_host_by_filter(filter)
177
- result = @client.api_request(
178
- method: "host.get",
179
- params: {
180
- output: %w[hostid host name status],
181
- filter: filter,
182
- selectInterfaces: %w[interfaceid ip dns]
183
- }
184
- )
185
- raise Conflict, "host lookup is ambiguous for #{filter.inspect}" if result.length > 1
217
+ # 校验创建主机所需的技术名、群组和接口。
218
+ def validate_create_attributes!(attributes)
219
+ raise Invalid, "host is required" if attributes[:host].blank?
220
+ raise Invalid, "groups are required when creating a host" if Array.wrap(attributes[:groups]).empty?
221
+ raise Invalid, "interfaces are required when creating a host" if Array.wrap(attributes[:interfaces]).empty?
186
222
 
187
- result.first
188
- end
223
+ attributes[:interfaces] = host_interfaces.validate_for_create(attributes[:interfaces])
224
+ end
189
225
 
190
- # 按 IP 和 DNS 端点查询唯一主机接口。
191
- def find_interface_by_endpoint(candidate)
192
- matches = []
193
- %i[ip dns].each do |field|
194
- result = @client.api_request(
195
- method: "hostinterface.get",
196
- params: { filter: { field => candidate }, output: ["hostid"] }
197
- )
198
- matches.concat(result)
199
- end
200
- matches.uniq! { |interface| interface.fetch("hostid") }
201
- raise Conflict, "interface endpoint #{candidate} belongs to multiple hosts" if matches.length > 1
226
+ # 使用精确过滤查询唯一主机。
227
+ def find_host_by_filter(filter)
228
+ result = @client.api_request(
229
+ method: "host.get",
230
+ params: {
231
+ output: %w[hostid host name status],
232
+ filter: filter,
233
+ selectInterfaces: %w[interfaceid ip dns]
234
+ }
235
+ )
236
+ raise Conflict, "host lookup is ambiguous for #{filter.inspect}" if result.length > 1
202
237
 
203
- matches.first
204
- end
238
+ response_identifier(result.first["hostid"]) if result.first
239
+ result.first
240
+ end
205
241
 
206
- # 只查询唯一主机 ID。
207
- def find_host_id(filter)
242
+ # 按 IP 和 DNS 端点查询唯一主机接口。
243
+ def find_interface_by_endpoint(candidate)
244
+ matches = []
245
+ %i[ip dns].each do |field|
208
246
  result = @client.api_request(
209
- method: "host.get",
210
- params: {
211
- output: ["hostid"],
212
- filter: filter
213
- }
247
+ method: "hostinterface.get",
248
+ params: { filter: { field => candidate }, output: ["hostid"] }
214
249
  )
215
- raise Conflict, "host lookup is ambiguous for #{filter.inspect}" if result.length > 1
216
-
217
- result.first&.fetch("hostid", nil)
250
+ matches.concat(result)
218
251
  end
252
+ matches.uniq! { |interface| interface.fetch("hostid") }
253
+ raise Conflict, "interface endpoint #{candidate} belongs to multiple hosts" if matches.length > 1
219
254
 
220
- # 从 Zabbix 创建响应中提取首个整数 ID。
221
- def first_id(result, key_name)
222
- value = result.fetch(key_name).first
223
- value && value.to_i
224
- end
255
+ matches.first
256
+ end
225
257
 
226
- # 延迟构建接口模块并复用当前客户端。
227
- # @return [HostInterfaces]
228
- # @api private
229
- def hostinterfaces
230
- @hostinterfaces ||= HostInterfaces.new(@client)
231
- end
258
+ # 只查询唯一主机 ID。
259
+ def find_host_id(filter)
260
+ result = @client.api_request(
261
+ method: "host.get",
262
+ params: {
263
+ output: ["hostid"],
264
+ filter: filter
265
+ }
266
+ )
267
+ raise Conflict, "host lookup is ambiguous for #{filter.inspect}" if result.length > 1
268
+
269
+ response_identifier(result.first["hostid"]).to_s if result.first
270
+ end
271
+
272
+ # 延迟构建接口模块并复用当前客户端。
273
+ # @return [HostInterfaces]
274
+ # @api private
275
+ def host_interfaces
276
+ @host_interfaces ||= HostInterfaces.new(@client)
277
+ end
232
278
  end
233
279
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ZabbixManager
4
- class HttpTests < Basic
4
+ # Web 场景以 hostid + name 定位;步骤定义及执行凭据由调用方提供。
5
+ class HttpTests < Resource
5
6
  # 返回 Web 场景对象对应的 Zabbix API 方法前缀。
6
7
  #
7
8
  # @return [String]
@@ -9,13 +10,6 @@ class ZabbixManager
9
10
  "httptest"
10
11
  end
11
12
 
12
- # 返回 Web 场景用于业务识别的字段名。
13
- #
14
- # @return [String]
15
- def identify
16
- "name"
17
- end
18
-
19
13
  # 返回创建 Web 场景时使用的默认步骤列表。
20
14
  #
21
15
  # @return [Hash] Web 场景默认属性