zabbix_manager 5.1.5 → 5.1.6

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -9
  3. data/LICENSE +1 -1
  4. data/README.md +202 -8
  5. data/lib/zabbix_manager/basic/basic_alias.rb +25 -3
  6. data/lib/zabbix_manager/basic/basic_func.rb +32 -61
  7. data/lib/zabbix_manager/basic/basic_init.rb +28 -13
  8. data/lib/zabbix_manager/basic/basic_logic.rb +172 -128
  9. data/lib/zabbix_manager/classes/actions.rb +21 -11
  10. data/lib/zabbix_manager/classes/applications.rb +12 -17
  11. data/lib/zabbix_manager/classes/configurations.rb +20 -5
  12. data/lib/zabbix_manager/classes/drules.rb +11 -23
  13. data/lib/zabbix_manager/classes/errors.rb +16 -28
  14. data/lib/zabbix_manager/classes/events.rb +6 -2
  15. data/lib/zabbix_manager/classes/graphs.rb +42 -35
  16. data/lib/zabbix_manager/classes/hostgroups.rb +53 -3
  17. data/lib/zabbix_manager/classes/hostinterfaces.rb +172 -12
  18. data/lib/zabbix_manager/classes/hosts.rb +191 -81
  19. data/lib/zabbix_manager/classes/httptests.rb +18 -20
  20. data/lib/zabbix_manager/classes/items.rb +193 -63
  21. data/lib/zabbix_manager/classes/maintenance.rb +6 -2
  22. data/lib/zabbix_manager/classes/mediatypes.rb +7 -63
  23. data/lib/zabbix_manager/classes/problems.rb +75 -57
  24. data/lib/zabbix_manager/classes/proxies.rb +48 -7
  25. data/lib/zabbix_manager/classes/roles.rb +47 -36
  26. data/lib/zabbix_manager/classes/screens.rb +45 -14
  27. data/lib/zabbix_manager/classes/scripts.rb +17 -7
  28. data/lib/zabbix_manager/classes/server.rb +7 -1
  29. data/lib/zabbix_manager/classes/templates.rb +59 -21
  30. data/lib/zabbix_manager/classes/triggers.rb +253 -66
  31. data/lib/zabbix_manager/classes/usergroups.rb +28 -6
  32. data/lib/zabbix_manager/classes/usermacros.rb +119 -33
  33. data/lib/zabbix_manager/classes/users.rb +39 -7
  34. data/lib/zabbix_manager/classes/valuemaps.rb +13 -19
  35. data/lib/zabbix_manager/client.rb +308 -133
  36. data/lib/zabbix_manager/http_transport.rb +192 -0
  37. data/lib/zabbix_manager/log_sanitizer.rb +68 -0
  38. data/lib/zabbix_manager/monitoring.rb +691 -0
  39. data/lib/zabbix_manager/version.rb +1 -1
  40. data/lib/zabbix_manager.rb +118 -40
  41. data/zabbix_manager.gemspec +21 -8
  42. metadata +208 -22
  43. data/lib/zabbix_manager/classes/unusable.rb +0 -11
@@ -2,122 +2,232 @@
2
2
 
3
3
  class ZabbixManager
4
4
  class Hosts < Basic
5
- # 与 Zabbix API 交互的主机方法名称
5
+ # 返回主机对应的 Zabbix API 模块名。
6
6
  def method_name
7
7
  "host"
8
8
  end
9
9
 
10
- # 用于通过 Zabbix API 标识特定主机对象的 id 字段名称
10
+ # 使用技术主机名作为通用 CRUD 标识。
11
11
  def identify
12
12
  "host"
13
13
  end
14
14
 
15
- # 在通过 Zabbix API 创建主机对象时使用的默认选项
15
+ # 返回创建主机时的克制默认值。
16
16
  def default_options
17
17
  {
18
- host: nil,
19
- interfaces: {
20
- main: 1,
21
- useip: 1,
22
- type: 2,
23
- ip: nil,
24
- dns: "",
25
- port: 161,
26
- details: {
27
- version: 2,
28
- community: "public"
29
- }
30
- },
31
- status: 0,
32
- available: 1,
33
- groups: [],
34
- proxy_hostid: nil,
18
+ status: 0,
35
19
  inventory_mode: 1
36
20
  }
37
21
  end
38
22
 
39
- # 通过 Zabbix API 从指定的 key 值获取主机对象数据
23
+ # 按主机 ID 查询完整主机及接口、群组和模板。
40
24
  def dump_by_id(data)
41
- log "[DEBUG] 调用 dump_by_id 方法,参数: #{data.inspect}"
42
-
43
- get_raw({
44
- filter: {
45
- key.to_sym => data[key.to_sym]
46
- },
47
- output: "extend",
48
- selectHosts: "shorten"
49
- }
25
+ log "[DEBUG] Call dump_by_id with parameters: #{data.inspect}"
26
+ @client.api_request(
27
+ method: "host.get",
28
+ params: {
29
+ filter: { key.to_sym => data[key.to_sym] },
30
+ output: "extend",
31
+ selectGroups: "extend",
32
+ selectInterfaces: "extend",
33
+ selectParentTemplates: ["templateid", "host", "name"]
34
+ }
50
35
  )
51
36
  end
52
37
 
53
- # 通过 Zabbix API 根据名称获取 Zabbix 对象的 ID
54
- def get_ids_by_name(data)
55
- log "[DEBUG] 调用 #{method_name}.get_ids_by_name 方法,参数: #{data.inspect}"
56
-
57
- result = get_raw({
58
- filter: { name: [data].flatten },
59
- output: [key]
60
- }
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
+ }
61
46
  )
62
- result.empty? ? nil : result.map { |i| { key.to_sym => i[key] } }
47
+ !result.empty?
63
48
  end
64
49
 
65
- # 使用 Zabbix API 创建或更新 Zabbix 对象
66
- def create_or_update(data)
67
- # 输出调试日志,显示方法调用和参数信息
68
- log "[DEBUG] 调用 #{method_name}.create_or_update 方法,参数: #{data.inspect}"
50
+ # 使用显式群组和接口创建主机,不注入隐藏凭据。
51
+ def create(data)
52
+ attributes = default_options.merge(data.deep_symbolize_keys)
53
+ validate_create_attributes!(attributes)
54
+ result = @client.api_request(method: "host.create", params: attributes)
55
+ first_id(result, "hostids")
56
+ end
69
57
 
70
- # 获取 Zabbix 对象的 ID
71
- hostid = get_id(identify.to_sym => data[identify.to_sym])
72
- # 如果不存在对应的对象ID,则创建新对象
73
- return create(data) unless hostid
58
+ # 规范化并校验设备定义,不执行远端查询或写入。
59
+ # 已存在设备允许只更新部分字段,新设备的群组和接口由 create 再次严格校验。
60
+ # @return [Hash]
61
+ def validate(data)
62
+ attributes = data.deep_symbolize_keys
63
+ raise Invalid, "host is required" if attributes[:host].blank?
74
64
 
75
- # 获取对象的接口 ID
76
- interface_id = get_interfaceid(hostid)
77
- # 如果接口 ID 不存在,抛出异常
78
- raise ZbxError, "#{data.inspect} get_interfaceid 查无记录" unless interface_id
65
+ hostinterfaces.validate(attributes[:interfaces]) if attributes.key?(:interfaces)
66
+ attributes
67
+ end
79
68
 
80
- # 复制数据以避免修改原始数据
81
- dump_data = data.dup
69
+ # 幂等同步主机元数据和显式接口,调用方保持业务字段权威。
70
+ def reconcile(data)
71
+ attributes = validate(data)
72
+ host = attributes.fetch(:host)
73
+
74
+ hostid = get_id(host: host)
75
+ unless hostid
76
+ outcome, hostid = @client.with_upsert_lock("host-create:#{host}") do
77
+ current_id = get_id(host: host)
78
+ current_id ? [:existing, current_id] : [:created, create(attributes)]
79
+ end
80
+ return hostid.to_i if outcome == :created
81
+ end
82
+
83
+ update_existing(hostid, attributes)
84
+ end
82
85
 
83
- # 从数据中提取并删除 interfaces,并合并接口 ID
84
- interfaces = dump_data.delete(:interfaces)&.merge(interfaceid: interface_id)
86
+ # 更新主机元数据,并把接口同步委托给带主机锁的接口模块。
87
+ # @return [Integer]
88
+ # @api private
89
+ private def update_existing(hostid, attributes)
90
+ attributes = attributes.dup
91
+
92
+ interfaces = Array.wrap(attributes.delete(:interfaces))
93
+ hostinterfaces.validate(interfaces)
94
+ 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
98
+ end
85
99
 
86
- # 构建参数,包括 interfaces 和 Zabbix 对象的键值对
87
- params = dump_data.merge(interfaces: interfaces).merge(key.to_sym => hostid.to_s)
100
+ # 查询指定主机的全部接口。
101
+ def interfaces_for(hostid)
102
+ hostinterfaces.for_host(hostid)
103
+ end
88
104
 
89
- # 更新对象
90
- update(params)
105
+ # 返回主机的第一个接口 ID;高频业务应优先使用 hostinterfaces。
106
+ def get_interface_id(hostid)
107
+ interfaces_for(hostid).first&.fetch("interfaceid", nil)
91
108
  end
92
109
 
93
- # 从 API 中基于提供的 hostid 获取 Zabbix interface 的 id
94
- def get_interfaceid(hostid)
95
- log "[DEBUG] 调用 #{method_name}.get_interfaceid 方法,参数: #{hostid.inspect}"
110
+ # 按技术主机名查询主机 ID。
111
+ def get_host_id(name)
112
+ find_host_id(host: name)
113
+ end
96
114
 
97
- result = @client.api_request(
98
- method: "hostinterface.get",
99
- params: { output: "interfaceid", hostids: hostid }
100
- )
115
+ # 按可见名称查询主机 ID。
116
+ def get_hostid_by_name(name)
117
+ find_host_id(name: name)
118
+ end
119
+
120
+ # 用技术名、可见名或接口 IP/DNS 解析唯一主机。
121
+ def find_by_candidates(candidates)
122
+ matches = Array(candidates).filter_map { |candidate| candidate.to_s.strip.presence }.uniq.flat_map do |candidate|
123
+ hosts = [find_host_by_filter(host: candidate), find_host_by_filter(name: candidate)].compact
124
+ interface = find_interface_by_endpoint(candidate)
125
+ hosts << find_host_by_filter(hostid: interface["hostid"]) if interface
126
+ hosts
127
+ end
128
+ matches.uniq! { |host| host.fetch("hostid") }
129
+ if matches.length > 1
130
+ raise Conflict, "host candidates resolve to multiple hosts; provide an explicit host object"
131
+ end
132
+
133
+ matches.first
134
+ end
101
135
 
102
- # 使用 dig 方法获取深层嵌套的值
103
- interface_id = result&.dig(0, "interfaceid")&.to_i
136
+ # 按主机 ID 查询唯一主机,并返回表达式需要的技术名称。
137
+ # @return [Hash, nil]
138
+ def find_by_id(hostid)
139
+ find_host_by_filter(hostid: hostid)
140
+ end
104
141
 
105
- # 如果获取接口 ID 失败,提供更具体的错误信息
106
- raise ZbxError, "无法获取 #{hostid} 的接口 ID" if interface_id.nil?
142
+ # 批量启用或停用主机。
143
+ # @return [Array<Integer>]
144
+ 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?
107
147
 
108
- interface_id
148
+ params = ids.map { |hostid| { hostid: hostid, status: enabled ? 0 : 1 } }
149
+ result = @client.api_request(method: "host.update", params: params)
150
+ Array(result.fetch("hostids")).map(&:to_i)
109
151
  end
110
152
 
111
- # 在 Hosts 中使用 Zabbix API 取消关联/移除模板
112
- def unlink_templates(data)
113
- result = @client.api_request(
114
- method: "host.massRemove",
115
- params: {
116
- hostids: data[:hosts_id],
117
- templates: data[:templates_id]
118
- }
119
- )
120
- result.empty? ? false : true
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
121
162
  end
163
+
164
+ private
165
+
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?
171
+
172
+ attributes[:interfaces] = hostinterfaces.validate_for_create(attributes[:interfaces])
173
+ end
174
+
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
186
+
187
+ result.first
188
+ end
189
+
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
202
+
203
+ matches.first
204
+ end
205
+
206
+ # 只查询唯一主机 ID。
207
+ def find_host_id(filter)
208
+ result = @client.api_request(
209
+ method: "host.get",
210
+ params: {
211
+ output: ["hostid"],
212
+ filter: filter
213
+ }
214
+ )
215
+ raise Conflict, "host lookup is ambiguous for #{filter.inspect}" if result.length > 1
216
+
217
+ result.first&.fetch("hostid", nil)
218
+ end
219
+
220
+ # 从 Zabbix 创建响应中提取首个整数 ID。
221
+ def first_id(result, key_name)
222
+ value = result.fetch(key_name).first
223
+ value && value.to_i
224
+ end
225
+
226
+ # 延迟构建接口模块并复用当前客户端。
227
+ # @return [HostInterfaces]
228
+ # @api private
229
+ def hostinterfaces
230
+ @hostinterfaces ||= HostInterfaces.new(@client)
231
+ end
122
232
  end
123
233
  end
@@ -2,39 +2,37 @@
2
2
 
3
3
  class ZabbixManager
4
4
  class HttpTests < Basic
5
- # 用于通过 Zabbix API 与 HttpTests 交互的方法名称
5
+ # 返回 Web 场景对象对应的 Zabbix API 方法前缀。
6
+ #
7
+ # @return [String]
6
8
  def method_name
7
9
  "httptest"
8
10
  end
9
11
 
10
- # 用于通过 Zabbix API 标识特定 HttpTest 对象的 id 字段名称
12
+ # 返回 Web 场景用于业务识别的字段名。
13
+ #
14
+ # @return [String]
11
15
  def identify
12
16
  "name"
13
17
  end
14
18
 
15
- # 通过 Zabbix API 创建 HttpTest 对象时使用的默认选项
19
+ # 返回创建 Web 场景时使用的默认步骤列表。
20
+ #
21
+ # @return [Hash] Web 场景默认属性
16
22
  def default_options
17
23
  {
18
- hostid: nil,
19
- name: nil,
20
- steps: []
24
+ steps: []
21
25
  }
22
26
  end
23
27
 
24
- # 通过 Zabbix API 获取或创建 HttpTest 对象
25
- def get_or_create(data)
26
- log "[DEBUG] 使用参数调用 get_or_create: #{data.inspect}"
27
-
28
- unless (id = get_id(name: data[:name], hostid: data[:hostid]))
29
- id = create(data)
30
- end
31
- id
32
- end
33
-
34
- # 通过 Zabbix API 创建或更新 HttpTest 对象
35
- def create_or_update(data)
36
- httptestid = get_id(name: data[:name], hostid: data[:hostid])
37
- httptestid ? update(data.merge(httptestid: httptestid)) : create(data)
28
+ # 生成由 Web 场景名称和所属主机构成的稳定查询条件。
29
+ #
30
+ # @param data [Hash] 包含 name 和 hostid 的 Web 场景属性
31
+ # @raise [KeyError] 缺少 name 或 hostid 时抛出
32
+ # @return [Hash] Web 场景唯一查询条件
33
+ def identity_filter(data)
34
+ attributes = data.deep_symbolize_keys
35
+ { name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
38
36
  end
39
37
  end
40
38
  end
@@ -2,94 +2,224 @@
2
2
 
3
3
  class ZabbixManager
4
4
  class Items < Basic
5
- # 用于通过 Zabbix API 与 Items 交互的方法名称
5
+ DEFAULT_OPTIONS = {
6
+ delay: "1m",
7
+ history: "1h",
8
+ status: 0,
9
+ value_type: 3
10
+ }.freeze
11
+ ITEM_TYPES = [0, 2, 3, 5, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22].freeze
12
+ REQUIRED_INTERFACE_TYPES = [0, 12, 16, 17, 20].freeze
13
+ REQUIRED_PARAMS_TYPES = [11, 13, 14, 15, 21, 22].freeze
14
+
15
+ # 返回监控项对应的 Zabbix API 模块名。
16
+ #
17
+ # @return [String]
6
18
  def method_name
7
19
  "item"
8
20
  end
9
21
 
10
- # 用于通过 Zabbix API 标识特定 Item 对象的 id 字段名称
22
+ # 使用名称作为通用 CRUD 的业务标识。
23
+ #
24
+ # @return [String]
11
25
  def identify
12
26
  "name"
13
27
  end
14
28
 
15
- # 通过 Zabbix API 创建 Item 对象时使用的默认选项
29
+ # 返回创建监控项时的克制默认值。
30
+ #
31
+ # @return [Hash]
16
32
  def default_options
17
- {
18
- name: nil,
19
- key_: nil,
20
- hostid: nil,
21
- delay: 60,
22
- history: 3600,
23
- status: 0,
24
- type: 7,
25
- snmp_community: "",
26
- snmp_oid: "",
27
- value_type: 3,
28
- data_type: 0,
29
- trapper_hosts: "localhost",
30
- snmp_port: 161,
31
- units: "",
32
- multiplier: 0,
33
- delta: 0,
34
- snmpv3_securityname: "",
35
- snmpv3_securitylevel: 0,
36
- snmpv3_authpassphrase: "",
37
- snmpv3_privpassphrase: "",
38
- formula: 0,
39
- trends: 86_400,
40
- logtimefmt: "",
41
- valuemapid: 0,
42
- delay_flex: "",
43
- authtype: 0,
44
- username: "",
45
- password: "",
46
- publickey: "",
47
- privatekey: "",
48
- params: "",
49
- ipmi_sensor: ""
50
- }
33
+ DEFAULT_OPTIONS.dup
34
+ end
35
+
36
+ # 使用主机和名称组成通用 CRUD 的查询边界。
37
+ #
38
+ # @param data [Hash] Needs to include name and hostid to properly identify Items via Zabbix API
39
+ # @raise [ApiError] Error returned when there is a problem with the Zabbix API call.
40
+ # @raise [TransportError] Error raised when HTTP status from Zabbix Server response is not a 200 OK.
41
+ # @return [Integer] Zabbix object id
42
+ def identity_filter(data)
43
+ attributes = data.deep_symbolize_keys
44
+ { name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
45
+ end
46
+
47
+ # 查询主机监控项,可按稳定 key 集合过滤并选择关联对象。
48
+ # @return [Array<Hash>]
49
+ def for_host(hostid, keys: nil, output: "extend", select_preprocessing: nil, select_tags: nil)
50
+ raise Invalid, "hostid is required" if hostid.blank?
51
+
52
+ params = { hostids: hostid, output: output }
53
+ params[:filter] = { key_: keys } if keys.present?
54
+ params[:selectPreprocessing] = select_preprocessing if select_preprocessing
55
+ params[:selectTags] = select_tags if select_tags
56
+ @client.api_request(method: "item.get", params: params)
57
+ end
58
+
59
+ # 按主机和稳定 key 查询唯一监控项。
60
+ # @return [Hash, nil]
61
+ def find_by_key(hostid:, key:)
62
+ result = for_host(hostid, keys: key, output: ["itemid", "key_", "name"])
63
+ raise Conflict, "multiple items use key #{key} on host #{hostid}" if result.length > 1
64
+
65
+ result.first
51
66
  end
52
67
 
53
- # 通过 Zabbix API 获取或创建 Item 对象
54
- def get_or_create(data)
55
- log "[DEBUG] 调用 #{method_name}.get_or_create 方法,参数: #{data.inspect}"
68
+ # 按 hostid + key_ 幂等创建或更新监控项。
69
+ # @return [Integer]
70
+ def upsert_by_key(data)
71
+ attributes = validate_item!(data)
56
72
 
57
- if (id = get_id(name: data[:name], hostid: data[:hostid]))
58
- id
73
+ current = find_by_key(hostid: attributes[:hostid], key: attributes[:key_])
74
+ if current
75
+ itemid = current.fetch("itemid")
76
+ @client.api_request(
77
+ method: "item.update",
78
+ params: attributes.except(:hostid).merge(itemid: itemid)
79
+ )
80
+ itemid.to_i
59
81
  else
60
- create(data)
82
+ create_attributes = default_options.merge(attributes)
83
+ validate_create_item!(create_attributes)
84
+ result = @client.api_request(method: "item.create", params: create_attributes)
85
+ result.fetch("itemids").first.to_i
61
86
  end
62
87
  end
63
88
 
64
- # 通过 Zabbix API 创建或更新 Item 对象
65
- def create_or_update(data)
66
- itemid = get_id(name: data[:name], hostid: data[:hostid])
67
- itemid ? update(data.merge(itemid: itemid)) : create(data)
89
+ # 先校验整批数据,再按稳定 key 逐项幂等写入。
90
+ # @return [Array<Integer>]
91
+ def upsert_many(collection)
92
+ items = Array(collection).map { |data| validate_item!(data) }
93
+ identities = items.map { |item| [item[:hostid].to_s, item[:key_].to_s] }
94
+ duplicate = identities.tally.find { |_identity, count| count > 1 }&.first
95
+ raise Invalid, "duplicate hostid + key_ identity #{duplicate.join(":")}" if duplicate
96
+
97
+ items.map { |item| upsert_by_key(item) }
68
98
  end
69
99
 
70
- # 通过 Zabbix API 获取特定主机和接口下的监控项
71
- def get_interface_items(hostid, name)
72
- # 记录调试信息,标识方法调用以及传递的参数
73
- log "[DEBUG] 调用 #{method_name}.get_interface_items,参数:hostid #{hostid.inspect},name #{name.inspect}"
100
+ # 批量启用或停用监控项。
101
+ # @return [Array<Integer>]
102
+ def set_status(hostid:, itemids:, enabled:)
103
+ ids = normalized_ids(itemids, "itemids")
104
+ verify_host_ownership!(hostid, ids)
105
+ result = @client.api_request(
106
+ method: "item.update",
107
+ params: ids.map { |itemid| { itemid: itemid, status: enabled ? 0 : 1 } }
108
+ )
109
+ Array(result.fetch("itemids")).map(&:to_i)
110
+ end
74
111
 
75
- # 自动剔除收尾空白字符,规范化接口名
76
- _name = name&.gsub(%r{[^/0-9]}, "")&.strip
77
- iface = "#{_name}("
112
+ # 批量删除明确指定的监控项。
113
+ # @return [Array<Integer>]
114
+ def delete_many(hostid:, itemids:)
115
+ ids = normalized_ids(itemids, "itemids")
116
+ verify_host_ownership!(hostid, ids)
117
+ result = @client.api_request(method: "item.delete", params: ids)
118
+ Array(result.fetch("itemids")).map(&:to_i)
119
+ end
78
120
 
79
- # 使用 Zabbix API 进行模糊查询,获取特定主机和接口下的监控项
80
- result = get_raw(
81
- {
82
- output: "extend",
121
+ # 返回主机中可能表示接口流量的已启用监控项。
122
+ # 方向和接口精确匹配由 Monitoring 业务层负责。
123
+ # @return [Array<Hash>]
124
+ def monitored_traffic_candidates(hostid)
125
+ @client.api_request(
126
+ method: "item.get",
127
+ params: {
83
128
  hostids: [hostid],
84
- search: { name: iface }
129
+ monitored: true,
130
+ output: %w[itemid hostid name key_ snmp_oid value_type status units],
131
+ selectPreprocessing: "extend",
132
+ sortfield: "name",
133
+ sortorder: "ASC"
85
134
  }
86
135
  )
136
+ end
87
137
 
88
- # 过滤结果,只保留符合正则表达式的项
89
- filtered_result = result.select { |item| item["snmp_oid"].match?(/(1.3.6.1.2.1.31.1.1.1.(6|10|15)|1.3.6.1.2.1.2.2.1.8)./) }
138
+ # 按主机接口幂等创建或更新 DNS 解析监控项,并返回监控项 ID。
139
+ # DNS 名称会进入 item key,因此拒绝可能改变 key 参数结构的分隔符。
140
+ # @return [Integer]
141
+ def upsert_dns_item(hostid:, interfaceid:, dns_name:)
142
+ name = dns_name.to_s.strip
143
+ raise Invalid, "dns_name is required" if name.blank?
144
+ raise Invalid, "dns_name contains unsupported item key delimiters" if name.match?(/[\[\],]/)
90
145
 
91
- # 如果结果为空,则返回 nil;否则,返回排序后的结果
92
- filtered_result.empty? ? nil : filtered_result.sort_by { |item| item["key_"] }
146
+ itemid = upsert_by_key(
147
+ hostid: hostid,
148
+ interfaceid: interfaceid,
149
+ name: "【DNS域名解析监控】#{name}",
150
+ key_: "net.dns.record[,#{name},A,2,2]",
151
+ type: 0,
152
+ value_type: 1,
153
+ delay: "1m",
154
+ history: "90d",
155
+ timeout: "3s"
156
+ )
157
+ log "Ensured DNS monitoring item for #{name}"
158
+ itemid
159
+ rescue StandardError => e
160
+ log "Failed to ensure DNS monitoring item: #{e.class}"
161
+ raise
93
162
  end
163
+
164
+ private
165
+
166
+ # 规范化并校验幂等写入所需字段。
167
+ # @return [Hash]
168
+ # @api private
169
+ def validate_item!(data)
170
+ attributes = data.deep_symbolize_keys
171
+ raise Invalid, "hostid is required" if attributes[:hostid].blank?
172
+ raise Invalid, "key_ is required" if attributes[:key_].blank?
173
+ raise Invalid, "name is required" if attributes[:name].blank?
174
+
175
+ attributes
176
+ end
177
+
178
+ # 校验 item.create 的通用必填字段和 SNMP 条件字段。
179
+ def validate_create_item!(attributes)
180
+ raise Invalid, "type is required when creating an item" if attributes[:type].blank?
181
+ raise Invalid, "value_type is required when creating an item" if attributes[:value_type].blank?
182
+
183
+ type = Integer(attributes[:type])
184
+ raise Invalid, "unsupported item type #{type}" unless ITEM_TYPES.include?(type)
185
+
186
+ if REQUIRED_INTERFACE_TYPES.include?(type) && attributes[:interfaceid].blank?
187
+ raise Invalid, "interfaceid is required for item type #{type}"
188
+ end
189
+ if REQUIRED_PARAMS_TYPES.include?(type) && attributes[:params].blank?
190
+ raise Invalid, "params is required for item type #{type}"
191
+ end
192
+
193
+ raise Invalid,
194
+ "master_itemid is required for a dependent item" if type == 18 && attributes[:master_itemid].blank?
195
+ raise Invalid, "url is required for an HTTP item" if type == 19 && attributes[:url].blank?
196
+ return unless type == 20
197
+
198
+ raise Invalid, "snmp_oid is required for an SNMP item" if attributes[:snmp_oid].blank?
199
+ rescue ArgumentError, TypeError
200
+ raise Invalid, "type must be a supported integer"
201
+ end
202
+
203
+ # 回查监控项归属,阻止共享高权限令牌跨主机修改。
204
+ def verify_host_ownership!(hostid, ids)
205
+ raise Invalid, "hostid is required" if hostid.blank?
206
+
207
+ result = @client.api_request(
208
+ method: "item.get", params: { hostids: hostid, itemids: ids, output: ["itemid"] }
209
+ )
210
+ owned_ids = result.map { |item| item.fetch("itemid").to_s }
211
+ foreign_ids = ids - owned_ids
212
+ raise Conflict, "items do not belong to host #{hostid}: #{foreign_ids.join(", ")}" if foreign_ids.any?
213
+ end
214
+
215
+ # 统一批量 ID 并拒绝空集合。
216
+ # @return [Array<String>]
217
+ # @api private
218
+ def normalized_ids(values, name)
219
+ ids = Array(values).filter_map { |value| value.to_s.strip.presence }.uniq
220
+ raise Invalid, "#{name} are required" if ids.empty?
221
+
222
+ ids
223
+ end
94
224
  end
95
225
  end