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,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
class Monitoring
|
|
5
|
+
# 解析端点的数值监控项,并复用主机已有的共享 ICMP 检查。
|
|
6
|
+
# 不重命名、启用、删除或接管已有 ICMP 监控项。
|
|
7
|
+
# @api private
|
|
8
|
+
class LineItems
|
|
9
|
+
PATTERNS = {
|
|
10
|
+
speed: /net\.if\.speed|ifhighspeed|interface speed|接口速率|1\.3\.6\.1\.2\.1\.31\.1\.1\.1\.15\./i,
|
|
11
|
+
status: /net\.if\.(?:status|oper)|ifoperstatus|operational status|接口状态|1\.3\.6\.1\.2\.1\.2\.2\.1\.8\./i
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(items)
|
|
15
|
+
@items = items
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 两个流量方向必须唯一、不同且属于目标主机;附加指标只在选择器启用时解析。
|
|
19
|
+
# @return [Hash{Symbol => Hash}] 流量及可选状态/速率监控项
|
|
20
|
+
# @raise [Invalid, Conflict, ProtocolError] 量纲、身份、归属或响应结构无法确认
|
|
21
|
+
def resolve(line, candidates, hostid:)
|
|
22
|
+
unless candidates.is_a?(Array) && candidates.all? { |item| item.is_a?(Hash) }
|
|
23
|
+
raise ProtocolError, "item.get must return an array of item objects"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
selected = TrafficItems.new(candidates).for_interface(line.interface_name)
|
|
27
|
+
%i[status speed].each do |kind|
|
|
28
|
+
selector = line.public_send(kind)
|
|
29
|
+
next unless selector
|
|
30
|
+
|
|
31
|
+
matches = candidates.select do |item|
|
|
32
|
+
if selector.is_a?(Hash)
|
|
33
|
+
item["itemid"].to_s == selector[:itemid].to_s
|
|
34
|
+
else
|
|
35
|
+
TrafficItems.interface_matches?(item, line.interface_name) &&
|
|
36
|
+
[item["key_"], item["name"], item["snmp_oid"]].join(" ").match?(PATTERNS.fetch(kind))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
raise Conflict,
|
|
40
|
+
"expected one #{kind} item for #{line.interface_name}, found #{matches.length}" unless matches.one?
|
|
41
|
+
|
|
42
|
+
selected[kind] = matches.first.deep_symbolize_keys
|
|
43
|
+
end
|
|
44
|
+
selected.each do |kind, item|
|
|
45
|
+
validate_numeric!(item, kind)
|
|
46
|
+
raise ProtocolError, "#{kind} item belongs to another host" unless item[:hostid].to_s == hostid.to_s
|
|
47
|
+
end
|
|
48
|
+
unless selected.values.map { |item| item[:itemid].to_s }.uniq.length == selected.length
|
|
49
|
+
raise Conflict, "line metrics must refer to distinct items"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
selected
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 查询稳定 ICMP key 并生成候选属性;缺失对象在执行阶段才创建。
|
|
56
|
+
# @return [Hash] attributes 及已有 itemid(不存在时为 nil)
|
|
57
|
+
def icmp_plan(hostid, target)
|
|
58
|
+
attributes = {
|
|
59
|
+
hostid: hostid, name: "ICMP #{target}", key_: "icmpping[#{target}]", type: 3,
|
|
60
|
+
value_type: 3, delay: "1m", history: "30d", trends: "365d", status: 0
|
|
61
|
+
}
|
|
62
|
+
current = find_icmp(attributes)
|
|
63
|
+
{ attributes: attributes, itemid: current&.fetch(:itemid) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 在调用方工作流互斥锁内再次查询,不覆盖已有共享监控项。
|
|
67
|
+
def ensure_icmp(plan)
|
|
68
|
+
current = find_icmp(plan.fetch(:attributes))
|
|
69
|
+
return current.fetch(:itemid).to_i if current
|
|
70
|
+
|
|
71
|
+
@items.create(plan.fetch(:attributes))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def find_icmp(attributes)
|
|
77
|
+
matches = @items.for_host(attributes[:hostid], keys: attributes[:key_])
|
|
78
|
+
unless matches.is_a?(Array) && matches.all? { |item| item.is_a?(Hash) }
|
|
79
|
+
raise ProtocolError, "item.get must return an array of item objects"
|
|
80
|
+
end
|
|
81
|
+
raise Conflict, "multiple items use ICMP key #{attributes[:key_]}" if matches.length > 1
|
|
82
|
+
return if matches.empty?
|
|
83
|
+
|
|
84
|
+
item = matches.first.deep_symbolize_keys
|
|
85
|
+
unless item[:hostid].to_s == attributes[:hostid].to_s && item[:key_] == attributes[:key_]
|
|
86
|
+
raise ProtocolError, "item.get returned an ICMP item outside the requested host and key"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
validate_numeric!(item, :reachability)
|
|
90
|
+
unless item[:type].to_s == "3" && item[:value_type].to_s == "3"
|
|
91
|
+
raise Conflict, "existing ICMP item must be an unsigned simple check"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
item
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_numeric!(item, kind)
|
|
98
|
+
Validation.positive_id!(item[:itemid], "#{kind} itemid")
|
|
99
|
+
Validation.item_key!(item[:key_])
|
|
100
|
+
unless %w[0 3].include?(item[:value_type].to_s)
|
|
101
|
+
raise Invalid, "#{kind} item must have a numeric value_type"
|
|
102
|
+
end
|
|
103
|
+
raise Conflict, "#{kind} item must be enabled" unless item[:status].to_s == "0"
|
|
104
|
+
return unless kind == :status && item[:value_type].to_s != "3"
|
|
105
|
+
|
|
106
|
+
raise Invalid, "status item must use unsigned integer values with 1 meaning up"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
class Monitoring
|
|
5
|
+
# 通过原生 API 核对端点触发器归属、停用旧检查并读取问题。
|
|
6
|
+
# 不保存持久状态;以已确认的标签和 ID 界定管理范围。
|
|
7
|
+
# @api private
|
|
8
|
+
class LineLifecycle
|
|
9
|
+
KINDS = %i[interface_status bandwidth low_traffic reachability].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(manager, hostid, line_id)
|
|
12
|
+
Validation.positive_id!(hostid, "hostid")
|
|
13
|
+
unless line_id.is_a?(String) || line_id.is_a?(Integer)
|
|
14
|
+
raise Invalid, "line_id must be a string or integer"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@line_id = Validation.text!(line_id.to_s, "line_id")
|
|
18
|
+
@prefix = "interface:#{@line_id}:"
|
|
19
|
+
@keys = KINDS.map { |kind| "#{@prefix}#{kind}" }
|
|
20
|
+
if @keys.any? { |key| key.length > Triggers::MAX_MANAGED_KEY_LENGTH }
|
|
21
|
+
raise Invalid, "managed_key is too long"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@manager, @hostid = manager, hostid
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 服务端前缀过滤后,再以完整受管键筛选并拒绝重复身份。
|
|
28
|
+
# @return [Array<Hash>] 该端点已知类型的原生触发器;所有者由 owned 继续筛选
|
|
29
|
+
def inventory
|
|
30
|
+
records = @manager.client.api_request(
|
|
31
|
+
method: "trigger.get",
|
|
32
|
+
params: {
|
|
33
|
+
hostids: @hostid, tags: [{ tag: "zabbix_manager_id", value: @prefix, operator: 0 }],
|
|
34
|
+
output: "extend", selectTags: "extend", selectItems: ["itemid", "name"], expandExpression: true
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
unless records.is_a?(Array) && records.all? { |record| record.is_a?(Hash) }
|
|
38
|
+
raise ProtocolError, "trigger.get must return an array of trigger objects"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
records.each do |record|
|
|
42
|
+
tags = record["tags"]
|
|
43
|
+
valid_tags = tags.is_a?(Array) && tags.all? do |tag|
|
|
44
|
+
tag.is_a?(Hash) && tag["tag"].is_a?(String) && tag["value"].is_a?(String)
|
|
45
|
+
end
|
|
46
|
+
raise ProtocolError, "trigger.get must include valid tags for ownership verification" unless valid_tags
|
|
47
|
+
end
|
|
48
|
+
selected = records.select { |record| managed_key(record) }
|
|
49
|
+
selected.each { |record| validate_receipt!(record) }
|
|
50
|
+
ids = selected.map { |record| record["triggerid"].to_s }
|
|
51
|
+
raise ProtocolError, "trigger.get returned duplicate trigger IDs" unless ids.uniq == ids
|
|
52
|
+
|
|
53
|
+
identities = selected.map { |record| managed_key(record) }
|
|
54
|
+
raise Conflict, "multiple triggers use the same line managed key" unless identities.uniq == identities
|
|
55
|
+
|
|
56
|
+
selected
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 兼容缺少 line_id 的既有受管标签,但拒绝其他管理方或矛盾的端点标签。
|
|
60
|
+
# @return [Array<Hash>] 当前库拥有的触发器
|
|
61
|
+
def owned(records = inventory)
|
|
62
|
+
records.select do |record|
|
|
63
|
+
owner = tag_values(record, "managed_by")
|
|
64
|
+
ids = tag_values(record, "line_id")
|
|
65
|
+
owner == ["zabbix_manager"] && (ids.empty? || ids == [@line_id])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_ownership!(records, desired_keys)
|
|
70
|
+
foreign = records - owned(records)
|
|
71
|
+
if foreign.any? { |record| desired_keys.include?(managed_key(record)) }
|
|
72
|
+
raise Conflict, "line managed key belongs to another owner"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def disable(records = owned)
|
|
77
|
+
return [] if records.empty?
|
|
78
|
+
|
|
79
|
+
@manager.triggers.set_status(hostid: @hostid, triggerids: records.map { |record| record.fetch("triggerid") },
|
|
80
|
+
enabled: false)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 仅停用本轮期望之外的受管类型;调用方必须先确认所有期望触发器写入成功。
|
|
84
|
+
# @return [Array<Integer>] 已确认停用的 ID;不删除对象
|
|
85
|
+
def retire(records, desired_keys)
|
|
86
|
+
disable(owned(records).reject { |record| desired_keys.include?(managed_key(record)) })
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def problems(time_from:, time_till:, limit:)
|
|
90
|
+
from = timestamp(time_from, "time_from")
|
|
91
|
+
till = timestamp(time_till, "time_till")
|
|
92
|
+
raise Invalid, "time_from must not be after time_till" if from > till
|
|
93
|
+
unless limit.is_a?(Integer) && limit.between?(1, 1000)
|
|
94
|
+
raise Invalid, "limit must be an integer between 1 and 1000"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
ids = owned.map { |record| record.fetch("triggerid").to_s }
|
|
98
|
+
records = if ids.empty?
|
|
99
|
+
[]
|
|
100
|
+
else
|
|
101
|
+
@manager.client.api_request(
|
|
102
|
+
method: "problem.get",
|
|
103
|
+
params: {
|
|
104
|
+
hostids: @hostid, objectids: ids, source: 0, object: 0, recent: true,
|
|
105
|
+
time_from: from, time_till: till, output: "extend", selectTags: "extend",
|
|
106
|
+
sortfield: "eventid", sortorder: "DESC", limit: limit + 1
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
unless records.is_a?(Array) && records.all? { |record| record.is_a?(Hash) }
|
|
111
|
+
raise ProtocolError, "problem.get must return an array of problem objects"
|
|
112
|
+
end
|
|
113
|
+
unless records.all? { |record| ids.include?(record["objectid"].to_s) }
|
|
114
|
+
raise ProtocolError, "problem.get returned a trigger outside the requested line"
|
|
115
|
+
end
|
|
116
|
+
raise ProtocolError, "problem.get exceeded the requested limit" if records.length > limit + 1
|
|
117
|
+
|
|
118
|
+
validate_problems!(records, from, till)
|
|
119
|
+
|
|
120
|
+
{ problems: records.first(limit), truncated: records.length > limit, time_from: from, time_till: till,
|
|
121
|
+
limit: limit }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def tag_values(record, name)
|
|
127
|
+
Array(record["tags"]).filter_map do |tag|
|
|
128
|
+
tag["value"] if tag.is_a?(Hash) && tag["tag"] == name
|
|
129
|
+
end.uniq
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def managed_key(record)
|
|
133
|
+
values = tag_values(record, "zabbix_manager_id")
|
|
134
|
+
matches = values & @keys
|
|
135
|
+
raise Conflict, "trigger has multiple line managed keys" if matches.any? && values.length > 1
|
|
136
|
+
|
|
137
|
+
matches.first
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def validate_receipt!(record)
|
|
141
|
+
Validation.positive_id!(record["triggerid"], "triggerid")
|
|
142
|
+
rescue Invalid => error
|
|
143
|
+
raise ProtocolError, error.message
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def timestamp(value, name)
|
|
147
|
+
value = value.to_i if value.is_a?(Time)
|
|
148
|
+
raise Invalid, "#{name} must be a non-negative Integer or Time" unless value.is_a?(Integer) && value >= 0
|
|
149
|
+
|
|
150
|
+
value
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_problems!(records, from, till)
|
|
154
|
+
records.each do |record|
|
|
155
|
+
Validation.positive_id!(record["eventid"], "eventid")
|
|
156
|
+
clock = record["clock"]
|
|
157
|
+
unless (clock.is_a?(Integer) || clock.is_a?(String)) && clock.to_s.match?(/\A\d+\z/) &&
|
|
158
|
+
clock.to_i.between?(from, till)
|
|
159
|
+
raise ProtocolError, "problem.get returned an invalid or out-of-range clock"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
ids = records.map { |record| record["eventid"].to_s }
|
|
163
|
+
raise ProtocolError, "problem.get returned duplicate event IDs" unless ids.uniq == ids
|
|
164
|
+
rescue Invalid => error
|
|
165
|
+
raise ProtocolError, error.message
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
class Monitoring
|
|
5
|
+
# 统一生成触发器属性,供预览和实际执行复用。
|
|
6
|
+
# 状态触发器 ID 确认之前,依赖以指标类型名称表示。
|
|
7
|
+
# @api private
|
|
8
|
+
class LinePlan
|
|
9
|
+
def initialize(client, line, host, items, icmp_item)
|
|
10
|
+
@client, @line, @host, @items, @icmp_item = client, line, host, items, icmp_item
|
|
11
|
+
@expressions = Expressions.new(client, host[:host])
|
|
12
|
+
@line_id = line.resolved_id(host[:hostid])
|
|
13
|
+
validate_metadata_version!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 纯本地构造计划,不写远端;依赖保留为指标名称,执行时转换为已确认 triggerid。
|
|
17
|
+
# @return [Hash] 端点身份、监控项 ID、触发器属性及依赖计划
|
|
18
|
+
def to_h
|
|
19
|
+
triggers = {}
|
|
20
|
+
triggers[:interface_status] = status_trigger if @items[:status]
|
|
21
|
+
triggers[:bandwidth] = bandwidth_trigger
|
|
22
|
+
triggers[:low_traffic] = low_trigger if @line.low_traffic
|
|
23
|
+
triggers[:reachability] = reachability_trigger if @icmp_item
|
|
24
|
+
dependencies = triggers.keys.to_h do |kind|
|
|
25
|
+
[kind, @items[:status] && kind != :interface_status ? [:interface_status] : []]
|
|
26
|
+
end
|
|
27
|
+
itemids = @items.transform_values { |item| item.fetch(:itemid).to_i }
|
|
28
|
+
itemids[:reachability] = @icmp_item[:itemid].to_i if @icmp_item&.dig(:itemid)
|
|
29
|
+
{
|
|
30
|
+
hostid: @host[:hostid].to_i, host: @host[:host], interface: @line.interface_name, line_id: @line_id,
|
|
31
|
+
itemids: itemids, triggers: triggers, dependencies: dependencies, icmp_item: @icmp_item
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def validate_metadata_version!
|
|
38
|
+
{ opdata: "4.4", event_name: "5.2" }.each do |field, minimum|
|
|
39
|
+
next unless @line.metadata.key?(field) && @expressions.version < Gem::Version.new(minimum)
|
|
40
|
+
|
|
41
|
+
raise Invalid, "#{field} requires Zabbix #{minimum} or newer"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bandwidth_trigger
|
|
46
|
+
attributes = Thresholds.new(@client).prepare_triggers(
|
|
47
|
+
host: @host, interface: @line.interface(hostid: @host[:hostid]),
|
|
48
|
+
items: { inbound_bps: @items[:inbound], outbound_bps: @items[:outbound] },
|
|
49
|
+
thresholds: { bandwidth: @line.threshold }
|
|
50
|
+
).fetch(:bandwidth)
|
|
51
|
+
attributes[:expression] = guarded(attributes[:expression])
|
|
52
|
+
attributes[:recovery_expression] = guarded(attributes[:recovery_expression])
|
|
53
|
+
decorate(attributes)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def low_trigger
|
|
57
|
+
config = @line.low_traffic
|
|
58
|
+
problem = traffic_expression(config[:window], "<", config[:below_bps], "and")
|
|
59
|
+
recovery = traffic_expression(config[:recovery_window], ">=", config[:recovery_bps], "or")
|
|
60
|
+
trigger(:low_traffic, guarded(problem), guarded(recovery), "low traffic")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def status_trigger
|
|
64
|
+
key = @items.fetch(:status).fetch(:key_)
|
|
65
|
+
trigger(:interface_status, compare(key, "last", nil, "<>", 1),
|
|
66
|
+
compare(key, "last", nil, "=", 1), "interface down")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reachability_trigger
|
|
70
|
+
key = @icmp_item.fetch(:attributes).fetch(:key_)
|
|
71
|
+
trigger(:reachability, compare(key, "max", "5m", "=", 0),
|
|
72
|
+
compare(key, "min", "2m", "=", 1), "ICMP unreachable")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def trigger(kind, problem, recovery, label)
|
|
76
|
+
key = "interface:#{@line_id}:#{kind}"
|
|
77
|
+
raise Invalid, "managed_key is too long" if key.length > Triggers::MAX_MANAGED_KEY_LENGTH
|
|
78
|
+
|
|
79
|
+
decorate(
|
|
80
|
+
hostid: @host[:hostid], managed_key: key,
|
|
81
|
+
description: "#{@line.interface_name} #{label}", expression: problem,
|
|
82
|
+
recovery_mode: 1, recovery_expression: recovery, priority: @line.threshold[:priority], manual_close: 1,
|
|
83
|
+
tags: [
|
|
84
|
+
{ tag: "managed_by", value: "zabbix_manager" }, { tag: "interface", value: @line.interface_name },
|
|
85
|
+
{ tag: "metric", value: kind.to_s }
|
|
86
|
+
] + @line.threshold[:tags]
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def decorate(attributes)
|
|
91
|
+
attributes.merge(@line.metadata).merge(
|
|
92
|
+
status: 0, tags: (attributes[:tags] + [{ tag: "line_id", value: @line_id }]).uniq
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def guarded(expression)
|
|
97
|
+
return expression unless @items[:speed]
|
|
98
|
+
|
|
99
|
+
"(#{expression}) and #{compare(@items[:speed][:key_], 'last', nil, '>', 0)}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def traffic_expression(window, operator, value, joiner)
|
|
103
|
+
%i[inbound outbound].map do |direction|
|
|
104
|
+
compare(@items.fetch(direction).fetch(:key_), "avg", window, operator, value)
|
|
105
|
+
end.join(" #{joiner} ")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def compare(...)
|
|
109
|
+
@expressions.compare(...)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zabbix_manager/monitoring/expressions"
|
|
4
|
+
|
|
5
|
+
class ZabbixManager
|
|
6
|
+
class Monitoring
|
|
7
|
+
# 先完整验证监控项和阈值,再生成不含远端副作用的触发器定义。
|
|
8
|
+
# 不执行 item.get 或写请求;已有监控项的归属及创建必填字段由资源层继续核对。
|
|
9
|
+
# @api private
|
|
10
|
+
class Thresholds
|
|
11
|
+
DEFAULT_WINDOW = "5m"
|
|
12
|
+
DEFAULT_PRIORITY = 3
|
|
13
|
+
FUNCTIONS = %w[avg max min last].freeze
|
|
14
|
+
METRICS = { bandwidth: %i[inbound_bps outbound_bps], errors: %i[in_errors out_errors],
|
|
15
|
+
packet_loss: [:packet_loss] }.freeze
|
|
16
|
+
MANAGED_TAGS = %w[managed_by interface metric zabbix_manager_id].freeze
|
|
17
|
+
CONFIG_KEYS = %i[metrics capacity_bps high_percent recovery_percent high recovery window
|
|
18
|
+
recovery_window function description priority tags].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(client)
|
|
21
|
+
@client = client
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 复制原生 item 定义并绑定主机/接口;省略字段保留给资源层默认值与校验处理。
|
|
25
|
+
# @return [Hash{Symbol => Hash}] 按业务指标名称分组的监控项属性
|
|
26
|
+
def prepare_items(host, interface, definitions)
|
|
27
|
+
items = Validation.hash!(definitions, "items").to_h do |metric, attributes|
|
|
28
|
+
attributes = Validation.hash!(attributes, "item #{metric}")
|
|
29
|
+
Validation.item_key!(attributes[:key_])
|
|
30
|
+
attributes[:hostid] = host[:hostid]
|
|
31
|
+
attributes[:interfaceid] = interface[:interfaceid] if interface[:interfaceid]
|
|
32
|
+
attributes[:name] = attributes.fetch(:name, "#{interface[:name]} #{metric.to_s.tr('_', ' ')}")
|
|
33
|
+
Validation.text!(attributes[:name], "item #{metric} name")
|
|
34
|
+
[metric, attributes]
|
|
35
|
+
end
|
|
36
|
+
if items.values.map { |item| item[:key_] }.uniq.length != items.length
|
|
37
|
+
raise Invalid, "item key_ values must be unique within an interface"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
items
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 高阈值任一方向超限即可告警;所有方向低于或等于恢复值才恢复。
|
|
44
|
+
# 初次预检可只检查结构;合并远端配置后必须校验完整监控项。
|
|
45
|
+
# @return [Hash{Symbol => Hash}] 包含表达式、恢复表达式及受管身份的写入属性
|
|
46
|
+
def prepare_triggers(host:, interface:, items:, thresholds:, validate_items: true)
|
|
47
|
+
Validation.host!(host)
|
|
48
|
+
Validation.hash!(thresholds, "thresholds").to_h do |metric, attributes|
|
|
49
|
+
config = self.class.validate_config!(metric, attributes)
|
|
50
|
+
selected = select_items(metric, config, items, validate_items: validate_items)
|
|
51
|
+
high, recovery = self.class.values(metric, config)
|
|
52
|
+
function = config.fetch(:function, "avg")
|
|
53
|
+
window = config.fetch(:window, DEFAULT_WINDOW)
|
|
54
|
+
description = config.fetch(:description, "#{interface[:name]} #{metric.to_s.tr('_', ' ')} high")
|
|
55
|
+
Validation.text!(description, "threshold #{metric} description")
|
|
56
|
+
keys = selected.map { |item| Validation.item_key!(item[:key_]) }
|
|
57
|
+
[metric, {
|
|
58
|
+
hostid: host[:hostid], description: description,
|
|
59
|
+
expression: expression(host[:host], keys, function, window, ">", high, "or"),
|
|
60
|
+
recovery_mode: 1,
|
|
61
|
+
recovery_expression: expression(host[:host], keys, function,
|
|
62
|
+
config.fetch(:recovery_window, window), "<=", recovery, "and"),
|
|
63
|
+
priority: Validation.priority!(config.fetch(:priority, DEFAULT_PRIORITY)), manual_close: 1,
|
|
64
|
+
tags: managed_tags(interface[:name], metric, config.fetch(:tags, [])),
|
|
65
|
+
managed_key: managed_key(interface, metric)
|
|
66
|
+
}]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.validate_config!(metric, attributes)
|
|
71
|
+
raise Invalid, "unsupported threshold #{metric}" unless METRICS.key?(metric)
|
|
72
|
+
|
|
73
|
+
config = Validation.hash!(attributes, "threshold #{metric}")
|
|
74
|
+
config.assert_valid_keys(*CONFIG_KEYS)
|
|
75
|
+
Validation.window!(config.fetch(:window, DEFAULT_WINDOW))
|
|
76
|
+
Validation.window!(config.fetch(:recovery_window, config.fetch(:window, DEFAULT_WINDOW)))
|
|
77
|
+
Validation.priority!(config.fetch(:priority, DEFAULT_PRIORITY))
|
|
78
|
+
unless FUNCTIONS.include?(config.fetch(:function, "avg"))
|
|
79
|
+
raise Invalid, "function must be one of #{FUNCTIONS.join(', ')}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
high, recovery = values(metric, config)
|
|
83
|
+
unless high.finite? && recovery.finite? && high.positive? && recovery >= 0 && recovery < high
|
|
84
|
+
raise Invalid, "#{metric} thresholds must be finite with recovery non-negative and lower than high"
|
|
85
|
+
end
|
|
86
|
+
if metric == :packet_loss && high > 100
|
|
87
|
+
raise Invalid, "packet_loss high must be at most 100 percent"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
config
|
|
91
|
+
rescue ArgumentError => error
|
|
92
|
+
raise Invalid, error.message
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.values(metric, config)
|
|
96
|
+
if metric == :bandwidth
|
|
97
|
+
capacity = Validation.number!(config[:capacity_bps], "bandwidth capacity_bps")
|
|
98
|
+
high = Validation.number!(config[:high_percent], "bandwidth high_percent")
|
|
99
|
+
recovery = Validation.number!(config[:recovery_percent], "bandwidth recovery_percent")
|
|
100
|
+
unless capacity.positive? && high.between?(0, 100) && recovery.between?(0, 100)
|
|
101
|
+
raise Invalid, "bandwidth capacity must be positive and percentages must be between 0 and 100"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
[capacity * (high / 100), capacity * (recovery / 100)]
|
|
105
|
+
else
|
|
106
|
+
[Validation.number!(config[:high], "#{metric} high"),
|
|
107
|
+
Validation.number!(config[:recovery], "#{metric} recovery")]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def select_items(metric, config, items, validate_items:)
|
|
114
|
+
names = Validation.array!(config.fetch(:metrics, METRICS.fetch(metric)), "threshold metrics")
|
|
115
|
+
raise Invalid, "threshold #{metric} requires at least one item" if names.empty?
|
|
116
|
+
|
|
117
|
+
names.map do |name|
|
|
118
|
+
unless name.is_a?(String) || name.is_a?(Symbol)
|
|
119
|
+
raise Invalid, "threshold metric names must be strings or symbols"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
item = items.fetch(name.to_sym) { raise Invalid, "threshold #{metric} requires item metric #{name}" }
|
|
123
|
+
validate_units!(metric, item) if validate_items
|
|
124
|
+
item
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_units!(metric, item)
|
|
129
|
+
if item.key?(:value_type) && !%w[0 3].include?(item[:value_type].to_s)
|
|
130
|
+
raise Invalid, "threshold #{metric} requires a numeric value_type"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
case metric
|
|
134
|
+
when :bandwidth
|
|
135
|
+
TrafficItems.validate_bps!(item)
|
|
136
|
+
when :errors
|
|
137
|
+
steps = Validation.array!(item.fetch(:preprocessing, []), "error preprocessing")
|
|
138
|
+
.map { |step| Validation.hash!(step, "error preprocessing step") }
|
|
139
|
+
rate_step = steps.any? { |step| step[:type].to_s == "10" }
|
|
140
|
+
explicit_rate = [item[:key_], item[:name]].join(" ").match?(/(?:\brate\b|[._]rate\b|per[._ -]?second)/i)
|
|
141
|
+
unless rate_step || explicit_rate
|
|
142
|
+
raise Invalid, "error metric #{item[:key_]} must represent a per-second rate"
|
|
143
|
+
end
|
|
144
|
+
when :packet_loss
|
|
145
|
+
raise Invalid, "packet loss metric #{item[:key_]} must use % units" unless item[:units] == "%"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def expression(host, keys, function, window, operator, threshold, joiner)
|
|
150
|
+
expressions = Expressions.new(@client, host)
|
|
151
|
+
keys.map { |key| expressions.compare(key, function, window, operator, threshold) }.join(" #{joiner} ")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def managed_tags(interface_name, metric, extra_tags)
|
|
155
|
+
tags = [
|
|
156
|
+
{ tag: "managed_by", value: "zabbix_manager" },
|
|
157
|
+
{ tag: "interface", value: interface_name },
|
|
158
|
+
{ tag: "metric", value: metric.to_s }
|
|
159
|
+
]
|
|
160
|
+
extra = Validation.array!(extra_tags, "threshold tags").map do |tag|
|
|
161
|
+
tag = Validation.hash!(tag, "threshold tag")
|
|
162
|
+
tag.assert_valid_keys(:tag, :value)
|
|
163
|
+
Validation.text!(tag[:tag], "threshold tag name")
|
|
164
|
+
raise Invalid, "threshold tag #{tag[:tag]} is reserved" if MANAGED_TAGS.include?(tag[:tag])
|
|
165
|
+
raise Invalid, "threshold tag value must be a string" unless tag.fetch(:value, "").is_a?(String)
|
|
166
|
+
|
|
167
|
+
{ tag: tag[:tag], value: tag.fetch(:value, "") }
|
|
168
|
+
end
|
|
169
|
+
(tags + extra).uniq
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def managed_key(interface, metric)
|
|
173
|
+
identity = interface[:line_id].presence || interface[:interfaceid].presence ||
|
|
174
|
+
TrafficItems.interface_identity(interface[:name])
|
|
175
|
+
key = "interface:#{identity}:#{metric}"
|
|
176
|
+
raise Invalid, "managed_key is too long" if key.length > Triggers::MAX_MANAGED_KEY_LENGTH
|
|
177
|
+
|
|
178
|
+
key
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|