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,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