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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +69 -46
- data/lib/zabbix_manager/classes/actions.rb +3 -11
- data/lib/zabbix_manager/classes/applications.rb +2 -8
- data/lib/zabbix_manager/classes/configurations.rb +4 -9
- data/lib/zabbix_manager/classes/{drules.rb → discovery_rules.rb} +2 -8
- data/lib/zabbix_manager/classes/errors.rb +3 -0
- data/lib/zabbix_manager/classes/events.rb +2 -1
- data/lib/zabbix_manager/classes/graphs.rb +19 -15
- data/lib/zabbix_manager/classes/{hostgroups.rb → host_groups.rb} +5 -11
- data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
- data/lib/zabbix_manager/classes/hosts.rb +135 -89
- data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
- data/lib/zabbix_manager/classes/items.rb +91 -87
- data/lib/zabbix_manager/classes/maintenance.rb +2 -1
- data/lib/zabbix_manager/classes/{mediatypes.rb → media_types.rb} +2 -7
- data/lib/zabbix_manager/classes/problems.rb +11 -40
- data/lib/zabbix_manager/classes/proxies.rb +5 -15
- data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
- data/lib/zabbix_manager/classes/roles.rb +13 -19
- data/lib/zabbix_manager/classes/screens.rb +11 -30
- data/lib/zabbix_manager/classes/scripts.rb +2 -8
- data/lib/zabbix_manager/classes/server.rb +1 -0
- data/lib/zabbix_manager/classes/templates.rb +2 -65
- data/lib/zabbix_manager/classes/triggers.rb +42 -45
- data/lib/zabbix_manager/classes/user_groups.rb +63 -0
- data/lib/zabbix_manager/classes/user_macros.rb +110 -0
- data/lib/zabbix_manager/classes/users.rb +15 -54
- data/lib/zabbix_manager/classes/value_maps.rb +52 -0
- data/lib/zabbix_manager/client.rb +148 -277
- data/lib/zabbix_manager/configuration.rb +107 -0
- data/lib/zabbix_manager/http_transport.rb +145 -97
- data/lib/zabbix_manager/log_sanitizer.rb +9 -19
- data/lib/zabbix_manager/monitoring/device.rb +393 -0
- data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
- data/lib/zabbix_manager/monitoring/line.rb +175 -0
- data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
- data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
- data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
- data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
- data/lib/zabbix_manager/monitoring/traffic_items.rb +132 -0
- data/lib/zabbix_manager/monitoring/validation.rb +92 -0
- data/lib/zabbix_manager/monitoring.rb +216 -642
- data/lib/zabbix_manager/resource.rb +235 -0
- data/lib/zabbix_manager/traffic.rb +238 -0
- data/lib/zabbix_manager/version.rb +1 -1
- data/lib/zabbix_manager.rb +66 -179
- data/zabbix_manager.gemspec +6 -22
- metadata +41 -190
- data/lib/zabbix_manager/basic/basic_alias.rb +0 -42
- data/lib/zabbix_manager/basic/basic_func.rb +0 -55
- data/lib/zabbix_manager/basic/basic_init.rb +0 -50
- data/lib/zabbix_manager/basic/basic_logic.rb +0 -239
- data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -189
- data/lib/zabbix_manager/classes/usergroups.rb +0 -75
- data/lib/zabbix_manager/classes/usermacros.rb +0 -222
- data/lib/zabbix_manager/classes/valuemaps.rb +0 -26
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
class Monitoring
|
|
5
|
+
# 通过原生 API 编排设备;调用方保存 managed 回执,本对象不持久化任何状态。
|
|
6
|
+
class Device
|
|
7
|
+
ROUTING_FIELDS = %i[monitored_by proxyid proxy_hostid proxy_groupid].freeze
|
|
8
|
+
RECEIPT_FIELDS = %i[group_ids template_ids tag_names].freeze
|
|
9
|
+
|
|
10
|
+
# @param attributes [Hash] 原生主机字段及 enabled、proxy、proxy_group、snmp、managed
|
|
11
|
+
# @option attributes [Array<String, Hash>] groups 群组名称或包含 groupid 的 Hash
|
|
12
|
+
# @option attributes [Array<String, Hash>] templates 模板技术名或包含 templateid 的 Hash
|
|
13
|
+
# @option attributes [Hash] snmp v1/v2c 的 ip 或 dns、community 及可选 port、macro、interfaceid
|
|
14
|
+
# @option attributes [Hash] managed 上次回执中的 group_ids、template_ids、tag_names。
|
|
15
|
+
# 显式期望的成员进入本次管理范围;同名但值不同的未管理标签会拒绝覆盖。
|
|
16
|
+
# @raise [Invalid] 输入不完整、冲突或类型错误;此阶段不访问远端
|
|
17
|
+
def initialize(attributes)
|
|
18
|
+
@attributes = Validation.hash!(attributes, "device").deep_dup
|
|
19
|
+
Validation.text!(@attributes[:host], "device.host")
|
|
20
|
+
unless Validation::HOST_NAME.match?(@attributes[:host])
|
|
21
|
+
raise Invalid, "device.host contains unsupported characters"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Validation.text!(@attributes[:name], "device.name") if @attributes.key?(:name)
|
|
25
|
+
enum!(@attributes[:inventory_mode], [-1, 0, 1], "inventory_mode") if @attributes.key?(:inventory_mode)
|
|
26
|
+
validate_status!
|
|
27
|
+
@managed = normalize_managed(@attributes.key?(:managed) ? @attributes.delete(:managed) : {})
|
|
28
|
+
validate_collections!
|
|
29
|
+
validate_routing!
|
|
30
|
+
prepare_snmp!
|
|
31
|
+
validate_interfaces!
|
|
32
|
+
validate_macros!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String] 设备的稳定技术主机名
|
|
36
|
+
def identity
|
|
37
|
+
@attributes.fetch(:host).dup
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Hash] 可安全放入批量结果的设备身份,不含接口和宏凭据
|
|
41
|
+
def safe_identity
|
|
42
|
+
@attributes.slice(:host, :name).compact.deep_dup
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String] 不包含凭据的调试标识
|
|
46
|
+
def inspect
|
|
47
|
+
"#<#{self.class} #{safe_identity.inspect}>"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 先完成名称解析、版本与接口归属校验,再执行写入。多次 API 写入不是远端事务。
|
|
51
|
+
# 失败的写入不重试、不回滚;调用方应保留异常并重新读取远端状态。
|
|
52
|
+
# @param manager [ZabbixManager] 已认证的共享连接
|
|
53
|
+
# @return [Hash] hostid、enabled 及 managed(包含 group_ids、template_ids 和 tag_names)
|
|
54
|
+
# 停用且主机不存在时 hostid 为 nil,且不执行创建。
|
|
55
|
+
def reconcile(manager)
|
|
56
|
+
existing = find_existing(manager)
|
|
57
|
+
unless @enabled
|
|
58
|
+
manager.hosts.set_status([existing.fetch("hostid")], enabled: false) if existing
|
|
59
|
+
return receipt(existing&.fetch("hostid"), @managed)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
validate_version!(manager.client.api_version)
|
|
63
|
+
attributes, managed = resolve_attributes(manager, existing)
|
|
64
|
+
unless existing
|
|
65
|
+
validate_creation!(manager, attributes)
|
|
66
|
+
# 仅协调创建;不嵌套接口锁,也不承诺跨进程或远端事务隔离。
|
|
67
|
+
outcome, value = manager.client.with_upsert_lock("host-create:#{identity}") do
|
|
68
|
+
current = find_existing(manager)
|
|
69
|
+
current ? [:existing, current] : [:created, manager.hosts.create(attributes)]
|
|
70
|
+
end
|
|
71
|
+
return receipt(value, managed) if outcome == :created
|
|
72
|
+
|
|
73
|
+
existing = value
|
|
74
|
+
attributes, managed = resolve_attributes(manager, existing)
|
|
75
|
+
end
|
|
76
|
+
update_existing(manager, existing, attributes)
|
|
77
|
+
receipt(existing.fetch("hostid"), managed)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def validate_status!
|
|
83
|
+
supplied = @attributes.key?(:enabled)
|
|
84
|
+
enabled = @attributes.delete(:enabled)
|
|
85
|
+
if supplied && enabled != true && enabled != false
|
|
86
|
+
raise Invalid, "device.enabled must be true or false"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if @attributes.key?(:status)
|
|
90
|
+
status = enum!(@attributes[:status], [0, 1], "device.status")
|
|
91
|
+
unless enabled.nil? || enabled == status.zero?
|
|
92
|
+
raise Invalid, "device.enabled and status disagree"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
enabled = status.zero?
|
|
96
|
+
end
|
|
97
|
+
@enabled = enabled.nil? ? true : enabled
|
|
98
|
+
@attributes[:status] = @enabled ? 0 : 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def normalize_managed(value)
|
|
102
|
+
managed = Validation.hash!(value, "managed")
|
|
103
|
+
raise Invalid, "unknown managed field" if (managed.keys - RECEIPT_FIELDS).any?
|
|
104
|
+
|
|
105
|
+
RECEIPT_FIELDS.to_h do |field|
|
|
106
|
+
values = Validation.array!(managed.fetch(field, []), "managed.#{field}").map do |item|
|
|
107
|
+
field == :tag_names ? Validation.text!(item, "managed.tag_names") :
|
|
108
|
+
Validation.positive_id!(item, "managed.#{field}").to_s
|
|
109
|
+
end
|
|
110
|
+
[field, values.uniq]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validate_collections!
|
|
115
|
+
{ groups: :groupid, templates: :templateid }.each do |field, id|
|
|
116
|
+
next unless @attributes.key?(field)
|
|
117
|
+
|
|
118
|
+
@attributes[field] = Validation.array!(@attributes[field], field).map do |reference|
|
|
119
|
+
if reference.is_a?(String)
|
|
120
|
+
Validation.text!(reference, field)
|
|
121
|
+
else
|
|
122
|
+
value = Validation.hash!(reference, field)
|
|
123
|
+
raise Invalid, "#{field} references require only #{id}" unless value.keys == [id]
|
|
124
|
+
|
|
125
|
+
{ id => Validation.positive_id!(value[id], id).to_s }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
if @attributes.key?(:inventory)
|
|
130
|
+
inventory = Validation.hash!(@attributes[:inventory], "inventory")
|
|
131
|
+
raise Invalid, "inventory values must be strings" unless inventory.values.all? { |value| value.is_a?(String) }
|
|
132
|
+
end
|
|
133
|
+
return unless @attributes.key?(:tags)
|
|
134
|
+
|
|
135
|
+
@attributes[:tags] = Validation.array!(@attributes[:tags], "tags").map do |tag|
|
|
136
|
+
value = Validation.hash!(tag, "tag")
|
|
137
|
+
raise Invalid, "tags accept only tag and value" if (value.keys - %i[tag value]).any?
|
|
138
|
+
|
|
139
|
+
Validation.text!(value[:tag], "tag.tag")
|
|
140
|
+
raise Invalid, "tag.value must be a string" unless value.fetch(:value, "").is_a?(String)
|
|
141
|
+
|
|
142
|
+
{ tag: value[:tag], value: value.fetch(:value, "") }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def validate_routing!
|
|
147
|
+
convenience = @attributes.keys & %i[proxy proxy_group]
|
|
148
|
+
if convenience.length > 1 || (convenience.any? && (@attributes.keys & ROUTING_FIELDS).any?)
|
|
149
|
+
raise Invalid, "choose one proxy routing reference"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
convenience.each do |field|
|
|
153
|
+
value = @attributes[field]
|
|
154
|
+
next Validation.text!(value, field) if value.is_a?(String)
|
|
155
|
+
|
|
156
|
+
id = field == :proxy ? :proxyid : :proxy_groupid
|
|
157
|
+
reference = Validation.hash!(value, field)
|
|
158
|
+
raise Invalid, "#{field} requires only #{id}" unless reference.keys == [id]
|
|
159
|
+
|
|
160
|
+
Validation.positive_id!(reference[id], id)
|
|
161
|
+
@attributes[field] = reference
|
|
162
|
+
end
|
|
163
|
+
(%i[proxyid proxy_hostid proxy_groupid] & @attributes.keys).each do |id|
|
|
164
|
+
Validation.positive_id!(@attributes[id], id)
|
|
165
|
+
end
|
|
166
|
+
return unless @attributes.key?(:monitored_by)
|
|
167
|
+
|
|
168
|
+
mode = enum!(@attributes[:monitored_by], [0, 1, 2], "monitored_by")
|
|
169
|
+
required = { 1 => :proxyid, 2 => :proxy_groupid }[mode]
|
|
170
|
+
Validation.positive_id!(@attributes[required], required) if required
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def prepare_snmp!
|
|
174
|
+
return unless @attributes.key?(:snmp)
|
|
175
|
+
raise Invalid, "snmp and interfaces cannot be combined" if @attributes.key?(:interfaces)
|
|
176
|
+
|
|
177
|
+
snmp = Validation.hash!(@attributes.delete(:snmp), "snmp")
|
|
178
|
+
allowed = %i[ip dns community macro port version bulk interfaceid]
|
|
179
|
+
raise Invalid, "unknown snmp field" if (snmp.keys - allowed).any?
|
|
180
|
+
raise Invalid, "snmp requires exactly one of ip or dns" unless (snmp.keys & %i[ip dns]).one?
|
|
181
|
+
|
|
182
|
+
endpoint = snmp.key?(:ip) ? :ip : :dns
|
|
183
|
+
Validation.text!(snmp[endpoint], "snmp.#{endpoint}")
|
|
184
|
+
Validation.text!(snmp[:community], "snmp.community")
|
|
185
|
+
macro = snmp.fetch(:macro, "{$SNMP_COMMUNITY}")
|
|
186
|
+
@snmp = {
|
|
187
|
+
type: 2, main: 1, useip: endpoint == :ip ? 1 : 0,
|
|
188
|
+
ip: snmp.fetch(:ip, ""), dns: snmp.fetch(:dns, ""), port: snmp.fetch(:port, "161").to_s,
|
|
189
|
+
details: { version: enum!(snmp.fetch(:version, 2), [1, 2], "snmp.version"),
|
|
190
|
+
bulk: enum!(snmp.fetch(:bulk, 1), [0, 1], "snmp.bulk"), community: macro }
|
|
191
|
+
}
|
|
192
|
+
if snmp.key?(:interfaceid)
|
|
193
|
+
@snmp[:interfaceid] = Validation.positive_id!(snmp[:interfaceid], "snmp.interfaceid")
|
|
194
|
+
end
|
|
195
|
+
@attributes[:interfaces] = [@snmp]
|
|
196
|
+
macros = Validation.array!(@attributes.fetch(:macros, []), "macros")
|
|
197
|
+
@attributes[:macros] = macros + [{ macro: macro, value: snmp[:community], type: 1 }]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def validate_interfaces!
|
|
201
|
+
return unless @attributes.key?(:interfaces)
|
|
202
|
+
|
|
203
|
+
interfaces = Validation.array!(@attributes[:interfaces], "interfaces").map do |interface|
|
|
204
|
+
value = Validation.hash!(interface, "interface")
|
|
205
|
+
Validation.positive_id!(value[:interfaceid], "interfaceid") if value.key?(:interfaceid)
|
|
206
|
+
value
|
|
207
|
+
end
|
|
208
|
+
@attributes[:interfaces] = HostInterfaces.new(nil).validate(interfaces)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def validate_macros!
|
|
212
|
+
return unless @attributes.key?(:macros)
|
|
213
|
+
|
|
214
|
+
@attributes[:macros] = Validation.array!(@attributes[:macros], "macros").map do |macro|
|
|
215
|
+
value = Validation.hash!(macro, "macro")
|
|
216
|
+
raise Invalid, "macros accept macro, value, type and description" if
|
|
217
|
+
(value.keys - %i[macro value type description]).any?
|
|
218
|
+
unless value[:macro].is_a?(String) && value[:macro].match?(/\A\{\$[^{}\r\n]+\}\z/)
|
|
219
|
+
raise Invalid, "macro name must use {$NAME} syntax"
|
|
220
|
+
end
|
|
221
|
+
raise Invalid, "macro value must be a string" unless value[:value].is_a?(String)
|
|
222
|
+
|
|
223
|
+
enum!(value.fetch(:type, 0), [0, 1, 2], "macro.type")
|
|
224
|
+
value
|
|
225
|
+
end
|
|
226
|
+
names = @attributes[:macros].pluck(:macro)
|
|
227
|
+
raise Invalid, "duplicate macro name" unless names.uniq == names
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def enum!(value, allowed, name)
|
|
231
|
+
unless (value.is_a?(String) || value.is_a?(Integer)) && allowed.map(&:to_s).include?(value.to_s)
|
|
232
|
+
raise Invalid, "#{name} must be one of #{allowed.join(', ')}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
value.to_i
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def validate_version!(version)
|
|
239
|
+
version = Gem::Version.new(version)
|
|
240
|
+
modern = version >= Gem::Version.new("7.0")
|
|
241
|
+
unsupported = modern ? [:proxy_hostid] : %i[proxy_group proxy_groupid proxyid monitored_by]
|
|
242
|
+
raise Invalid, "proxy routing is unsupported by this Zabbix version" if (@attributes.keys & unsupported).any?
|
|
243
|
+
|
|
244
|
+
if modern && (@attributes.keys & %i[proxyid proxy_groupid]).any?
|
|
245
|
+
mode = @attributes[:monitored_by].to_s
|
|
246
|
+
expected = { "1" => :proxyid, "2" => :proxy_groupid }[mode]
|
|
247
|
+
actual = @attributes.keys & %i[proxyid proxy_groupid]
|
|
248
|
+
raise Invalid, "proxy ID must match monitored_by" unless actual == [expected]
|
|
249
|
+
end
|
|
250
|
+
Array(@attributes[:macros]).each do |macro|
|
|
251
|
+
minimum = { 1 => "5.0", 2 => "5.2" }[macro.fetch(:type, 0).to_i]
|
|
252
|
+
if minimum && version < Gem::Version.new(minimum)
|
|
253
|
+
raise Invalid, "macro type requires Zabbix #{minimum} or newer"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def find_existing(manager)
|
|
259
|
+
modern = Gem::Version.new(manager.client.api_version) >= Gem::Version.new("7.0")
|
|
260
|
+
group_field = modern ? :selectHostGroups : :selectGroups
|
|
261
|
+
hosts = manager.hosts.get_raw(
|
|
262
|
+
filter: { host: identity }, output: %w[hostid host], group_field => ["groupid"],
|
|
263
|
+
selectParentTemplates: ["templateid"], selectTags: "extend", selectInterfaces: "extend"
|
|
264
|
+
)
|
|
265
|
+
unless hosts.is_a?(Array) && hosts.all? { |host| host.is_a?(Hash) && host["host"] == identity }
|
|
266
|
+
raise ProtocolError, "invalid device lookup response"
|
|
267
|
+
end
|
|
268
|
+
raise Conflict, "multiple hosts match device identity" if hosts.length > 1
|
|
269
|
+
|
|
270
|
+
hosts.first&.tap do |host|
|
|
271
|
+
remote_id!(host["hostid"], "hostid")
|
|
272
|
+
host["groups"] = host.fetch("hostgroups") if modern && host.key?("hostgroups")
|
|
273
|
+
%w[groups parentTemplates tags interfaces].each do |field|
|
|
274
|
+
unless host[field].is_a?(Array) && host[field].all? { |entry| entry.is_a?(Hash) }
|
|
275
|
+
raise ProtocolError, "device response is missing #{field}"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def resolve_attributes(manager, existing)
|
|
282
|
+
attributes = @attributes.except(:proxy, :proxy_group).deep_dup
|
|
283
|
+
managed = @managed.deep_dup
|
|
284
|
+
{ groups: [:host_groups, :groupid, :name, :group_ids, "groups"],
|
|
285
|
+
templates: [:templates, :templateid, :host, :template_ids, "parentTemplates"] }.each do |field, mapping|
|
|
286
|
+
next unless attributes.key?(field)
|
|
287
|
+
|
|
288
|
+
resource, id_key, name_key, receipt_key, remote_key = mapping
|
|
289
|
+
ids = attributes[field].map do |reference|
|
|
290
|
+
reference.is_a?(Hash) ? reference.fetch(id_key).to_s :
|
|
291
|
+
resolve_name(manager.public_send(resource), name_key, reference).to_s
|
|
292
|
+
end.uniq
|
|
293
|
+
current = existing ? existing.fetch(remote_key).map { |item| remote_id!(item[id_key.to_s], id_key) } : []
|
|
294
|
+
attributes[field] = ((current - @managed[receipt_key]) | ids).map { |id| { id_key => id } }
|
|
295
|
+
managed[receipt_key] = ids
|
|
296
|
+
end
|
|
297
|
+
if attributes.key?(:tags)
|
|
298
|
+
tags = existing ? existing.fetch("tags").map { |tag| remote_tag!(tag) } : []
|
|
299
|
+
desired = attributes[:tags]
|
|
300
|
+
unowned = tags.reject { |tag| @managed[:tag_names].include?(tag[:tag]) }
|
|
301
|
+
names = desired.pluck(:tag)
|
|
302
|
+
if unowned.any? { |tag| names.include?(tag[:tag]) && !desired.include?(tag) }
|
|
303
|
+
raise Conflict, "desired tag name has an unmanaged value"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
attributes[:tags] = unowned + desired
|
|
307
|
+
attributes[:tags].uniq!
|
|
308
|
+
managed[:tag_names] = @attributes[:tags].pluck(:tag).uniq
|
|
309
|
+
end
|
|
310
|
+
resolve_routing(manager, attributes)
|
|
311
|
+
if @snmp && existing && !@snmp.key?(:interfaceid)
|
|
312
|
+
main = existing.fetch("interfaces").select { |item| item["type"].to_s == "2" && item["main"].to_s == "1" }
|
|
313
|
+
raise Conflict, "multiple main SNMP interfaces; provide interfaceid" if main.length > 1
|
|
314
|
+
|
|
315
|
+
attributes[:interfaces].first[:interfaceid] =
|
|
316
|
+
remote_id!(main.first["interfaceid"], "interfaceid") if main.one?
|
|
317
|
+
end
|
|
318
|
+
raise Invalid, "groups cannot be empty" if attributes.key?(:groups) && attributes[:groups].empty?
|
|
319
|
+
|
|
320
|
+
[attributes, managed]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def resolve_name(resource, field, name)
|
|
324
|
+
resource.get_id(field => name) || raise(ApiError, "required #{resource.method_name} was not found")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def resolve_routing(manager, attributes)
|
|
328
|
+
%i[proxy proxy_group].each do |field|
|
|
329
|
+
next unless @attributes.key?(field)
|
|
330
|
+
|
|
331
|
+
reference = @attributes[field]
|
|
332
|
+
resource = field == :proxy ? manager.proxies : manager.proxy_groups
|
|
333
|
+
id_key = field == :proxy ? :proxyid : :proxy_groupid
|
|
334
|
+
id = if reference.is_a?(Hash)
|
|
335
|
+
reference.fetch(id_key)
|
|
336
|
+
else
|
|
337
|
+
resolve_name(resource, resource.identify.to_sym, reference)
|
|
338
|
+
end
|
|
339
|
+
if Gem::Version.new(manager.client.api_version) < Gem::Version.new("7.0")
|
|
340
|
+
attributes[:proxy_hostid] = id
|
|
341
|
+
else
|
|
342
|
+
attributes[:monitored_by] = field == :proxy ? 1 : 2
|
|
343
|
+
attributes[id_key] = id
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def validate_creation!(manager, attributes)
|
|
349
|
+
raise Invalid, "groups are required when creating a device" if attributes.fetch(:groups, []).empty?
|
|
350
|
+
raise Invalid, "interfaces are required when creating a device" if attributes.fetch(:interfaces, []).empty?
|
|
351
|
+
if attributes[:interfaces].any? { |interface| interface.key?(:interfaceid) }
|
|
352
|
+
raise Invalid, "new devices cannot reference existing interface IDs"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
manager.host_interfaces.validate_for_create(attributes[:interfaces])
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def update_existing(manager, existing, attributes)
|
|
359
|
+
hostid = existing.fetch("hostid")
|
|
360
|
+
interfaces = attributes.delete(:interfaces)
|
|
361
|
+
macros = attributes.delete(:macros) || []
|
|
362
|
+
manager.host_interfaces.validate_for_host(hostid: hostid, interfaces: interfaces) if interfaces&.any?
|
|
363
|
+
macro_plan = macros.map do |macro|
|
|
364
|
+
[macro, manager.user_macros.get_id(hostid: hostid, macro: macro.fetch(:macro))]
|
|
365
|
+
end
|
|
366
|
+
macro_plan.each do |macro, id|
|
|
367
|
+
id ? manager.user_macros.update(macro.merge(hostmacroid: id)) :
|
|
368
|
+
manager.user_macros.create(macro.merge(hostid: hostid))
|
|
369
|
+
end
|
|
370
|
+
manager.host_interfaces.reconcile_for_host(hostid: hostid, interfaces: interfaces) if interfaces&.any?
|
|
371
|
+
manager.hosts.update(attributes.merge(hostid: hostid))
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def remote_id!(value, field)
|
|
375
|
+
Validation.positive_id!(value, field).to_s
|
|
376
|
+
rescue Invalid
|
|
377
|
+
raise ProtocolError, "invalid device response #{field}", cause: nil
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def remote_tag!(tag)
|
|
381
|
+
unless tag["tag"].is_a?(String) && tag["value"].is_a?(String)
|
|
382
|
+
raise ProtocolError, "invalid device tag response"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
{ tag: tag["tag"], value: tag["value"] }
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def receipt(hostid, managed)
|
|
389
|
+
{ hostid: hostid&.to_i, enabled: @enabled, managed: managed.deep_dup }
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
class Monitoring
|
|
5
|
+
# 根据 API 版本,将已校验的主机、监控项键和数值转换为触发器表达式。
|
|
6
|
+
# @api private
|
|
7
|
+
class Expressions
|
|
8
|
+
attr_reader :version
|
|
9
|
+
|
|
10
|
+
def initialize(client, host)
|
|
11
|
+
version = client.api_version.to_s
|
|
12
|
+
unless version.match?(/\A\d+\.\d+(?:\.\d+)?\z/)
|
|
13
|
+
raise Invalid, "Zabbix API version must be a numeric release version"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@host = host
|
|
17
|
+
@version = Gem::Version.new(version)
|
|
18
|
+
@modern = @version >= Gem::Version.new("5.4")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 只接受上层已校验的函数、运算符、窗口和主机;这里再次校验嵌入表达式的 item key。
|
|
22
|
+
# last 不带窗口;其他函数按 5.4 前后语法生成,数值不使用科学计数法。
|
|
23
|
+
# @return [String] 一个可组合的比较表达式
|
|
24
|
+
def compare(key, function, window, operator, value)
|
|
25
|
+
Validation.item_key!(key)
|
|
26
|
+
call = if @modern
|
|
27
|
+
source = "/#{@host}/#{key}"
|
|
28
|
+
function == "last" ? "last(#{source})" : "#{function}(#{source},#{window})"
|
|
29
|
+
else
|
|
30
|
+
"{#{@host}:#{key}.#{function}(#{function == 'last' ? '' : window})}"
|
|
31
|
+
end
|
|
32
|
+
"#{call}#{operator}#{Validation.format_number(value)}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
class ZabbixManager
|
|
6
|
+
class Monitoring
|
|
7
|
+
# 线路清单使用一套明确字段;外部文件列名应由导入方转换。
|
|
8
|
+
# @api private
|
|
9
|
+
class Line
|
|
10
|
+
FIELDS = %i[line_id host host_candidates interface_name capacity_mbps high_water recovery_water
|
|
11
|
+
problem_window recovery_window severity device isp description low_traffic status speed
|
|
12
|
+
reachability_target tags comments event_name opdata].freeze
|
|
13
|
+
METADATA_FIELDS = %i[comments event_name opdata].freeze
|
|
14
|
+
attr_reader :host_reference, :interface_name, :threshold, :low_traffic, :status, :speed,
|
|
15
|
+
:reachability_target, :metadata
|
|
16
|
+
|
|
17
|
+
# 仅解析本地定义,不访问服务器;status/speed 为开关或显式 itemid 选择器。
|
|
18
|
+
# @param attributes [Hash] 规范化后的单端点清单字段
|
|
19
|
+
# @raise [Invalid] 字段、阈值或目标格式不合法
|
|
20
|
+
def initialize(attributes)
|
|
21
|
+
@attributes = Validation.hash!(attributes, "line")
|
|
22
|
+
@attributes.assert_valid_keys(*FIELDS)
|
|
23
|
+
@interface_name = Validation.text!(@attributes[:interface_name], "line interface_name")
|
|
24
|
+
validate_identity!
|
|
25
|
+
@host_reference = build_host_reference
|
|
26
|
+
@threshold = build_threshold
|
|
27
|
+
Thresholds.validate_config!(:bandwidth, @threshold)
|
|
28
|
+
@low_traffic = build_low_traffic if @attributes.key?(:low_traffic)
|
|
29
|
+
@status = item_selection(:status)
|
|
30
|
+
@speed = item_selection(:speed)
|
|
31
|
+
@reachability_target = build_reachability_target if @attributes.key?(:reachability_target)
|
|
32
|
+
@metadata = @attributes.slice(*METADATA_FIELDS)
|
|
33
|
+
@metadata.each do |key, value|
|
|
34
|
+
raise Invalid, "line #{key} must be a string" unless value.is_a?(String)
|
|
35
|
+
end
|
|
36
|
+
rescue ArgumentError => error
|
|
37
|
+
raise Invalid, error.message
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def identity
|
|
41
|
+
@attributes[:line_id].presence ||
|
|
42
|
+
[host_identity, TrafficItems.interface_identity(interface_name)]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def interface(hostid:)
|
|
46
|
+
{ name: interface_name, line_id: resolved_id(hostid) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 显式 line_id 优先;缺省身份由已解析 hostid 和规范接口名组成。
|
|
50
|
+
# @return [String] 用于受管标签的端点身份
|
|
51
|
+
def resolved_id(hostid)
|
|
52
|
+
@attributes[:line_id].presence || "#{hostid}:#{TrafficItems.interface_identity(interface_name)}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def safe_identity
|
|
56
|
+
@attributes.slice(:line_id, :device).merge(interface: interface_name).compact
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def host_identity
|
|
62
|
+
host_reference.is_a?(Hash) ? host_reference[:hostid].to_s : host_reference.sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_identity!
|
|
66
|
+
line_id = @attributes[:line_id]
|
|
67
|
+
unless line_id.nil? || line_id.is_a?(String) || line_id.is_a?(Integer)
|
|
68
|
+
raise Invalid, "line_id must be a string or integer"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@attributes[:line_id] = line_id.to_s if line_id
|
|
72
|
+
%i[device isp description].each do |name|
|
|
73
|
+
next unless @attributes.key?(name)
|
|
74
|
+
|
|
75
|
+
Validation.text!(@attributes[name], "line #{name}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_host_reference
|
|
80
|
+
if @attributes.key?(:host) && @attributes.key?(:host_candidates)
|
|
81
|
+
raise Invalid, "line must supply either host or host_candidates"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
host = @attributes[:host]
|
|
85
|
+
if host.is_a?(Hash)
|
|
86
|
+
Validation.host!(host)
|
|
87
|
+
return host.slice(:hostid, :host)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
candidates = @attributes.key?(:host_candidates) ?
|
|
91
|
+
Validation.array!(@attributes[:host_candidates], "host_candidates") : [host]
|
|
92
|
+
candidates = candidates.map { |value| Validation.text!(value, "line host candidate").strip }.uniq
|
|
93
|
+
raise Invalid, "line host candidate is required" if candidates.empty?
|
|
94
|
+
|
|
95
|
+
candidates
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_threshold
|
|
99
|
+
capacity = Validation.number!(@attributes[:capacity_mbps], "line capacity_mbps")
|
|
100
|
+
raise Invalid, "line capacity_mbps must be positive" unless capacity.positive?
|
|
101
|
+
|
|
102
|
+
high = water_ratio(@attributes.fetch(:high_water, 0.9), "high_water")
|
|
103
|
+
recovery = water_ratio(@attributes.fetch(:recovery_water, 0.8), "recovery_water")
|
|
104
|
+
raise Invalid, "recovery_water must be lower than high_water" unless recovery < high
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
capacity_bps: capacity * 1_000_000,
|
|
108
|
+
high_percent: high * 100, recovery_percent: recovery * 100,
|
|
109
|
+
window: @attributes.fetch(:problem_window, Thresholds::DEFAULT_WINDOW),
|
|
110
|
+
recovery_window: @attributes.fetch(:recovery_window, "15m"),
|
|
111
|
+
priority: @attributes.fetch(:severity, Thresholds::DEFAULT_PRIORITY),
|
|
112
|
+
description: ["专线流量超阈值", @attributes[:isp], @attributes[:description],
|
|
113
|
+
@attributes[:device], interface_name].compact.join(" | "),
|
|
114
|
+
tags: line_tags
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def line_tags
|
|
119
|
+
extra = Validation.array!(@attributes.fetch(:tags, []), "line tags").map do |tag|
|
|
120
|
+
tag = Validation.hash!(tag, "line tag")
|
|
121
|
+
tag.assert_valid_keys(:tag, :value)
|
|
122
|
+
Validation.text!(tag[:tag], "line tag name")
|
|
123
|
+
if (Thresholds::MANAGED_TAGS + ["line_id"]).include?(tag[:tag])
|
|
124
|
+
raise Invalid, "line tag #{tag[:tag]} is reserved"
|
|
125
|
+
end
|
|
126
|
+
raise Invalid, "line tag value must be a string" unless tag.fetch(:value, "").is_a?(String)
|
|
127
|
+
|
|
128
|
+
{ tag: tag[:tag], value: tag.fetch(:value, "") }
|
|
129
|
+
end
|
|
130
|
+
[{ tag: "category", value: "line_bandwidth" }] +
|
|
131
|
+
@attributes.slice(:isp, :description, :device).map { |name, value| { tag: name.to_s, value: value } } + extra
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_low_traffic
|
|
135
|
+
config = Validation.hash!(@attributes[:low_traffic], "low_traffic")
|
|
136
|
+
config.assert_valid_keys(:below_bps, :recovery_bps, :window, :recovery_window)
|
|
137
|
+
below = Validation.number!(config.fetch(:below_bps, 50), "low_traffic below_bps")
|
|
138
|
+
recovery = Validation.number!(config.fetch(:recovery_bps, 1000), "low_traffic recovery_bps")
|
|
139
|
+
unless below.positive? && recovery > below
|
|
140
|
+
raise Invalid, "low_traffic recovery_bps must be greater than positive below_bps"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
window = Validation.window!(config.fetch(:window, "5m"))
|
|
144
|
+
{ below_bps: below, recovery_bps: recovery, window: window,
|
|
145
|
+
recovery_window: Validation.window!(config.fetch(:recovery_window, window)) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def item_selection(name)
|
|
149
|
+
value = @attributes.fetch(name, false)
|
|
150
|
+
return value if value == true || value == false
|
|
151
|
+
|
|
152
|
+
selection = Validation.hash!(value, name.to_s)
|
|
153
|
+
selection.assert_valid_keys(:itemid)
|
|
154
|
+
Validation.positive_id!(selection[:itemid], "#{name} itemid")
|
|
155
|
+
selection
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def build_reachability_target
|
|
159
|
+
value = Validation.text!(@attributes[:reachability_target], "reachability_target")
|
|
160
|
+
raise Invalid, "reachability_target must be an IP address without a prefix" if value.include?("/")
|
|
161
|
+
|
|
162
|
+
IPAddr.new(value).to_s
|
|
163
|
+
rescue IPAddr::InvalidAddressError
|
|
164
|
+
raise Invalid, "reachability_target must be an IPv4 or IPv6 address"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def water_ratio(value, name)
|
|
168
|
+
ratio = Validation.number!(value, name)
|
|
169
|
+
raise Invalid, "#{name} must be greater than 0 and at most 1" unless ratio.positive? && ratio <= 1
|
|
170
|
+
|
|
171
|
+
ratio
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|