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
|
@@ -1,691 +1,265 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "zabbix_manager/monitoring/validation"
|
|
4
|
+
require "zabbix_manager/monitoring/traffic_items"
|
|
5
|
+
require "zabbix_manager/monitoring/thresholds"
|
|
6
|
+
require "zabbix_manager/monitoring/line"
|
|
7
|
+
require "zabbix_manager/monitoring/line_items"
|
|
8
|
+
require "zabbix_manager/monitoring/line_plan"
|
|
9
|
+
require "zabbix_manager/monitoring/line_lifecycle"
|
|
10
|
+
require "zabbix_manager/monitoring/device"
|
|
11
|
+
|
|
3
12
|
class ZabbixManager
|
|
4
|
-
#
|
|
13
|
+
# 网络设备、接口和线路监控编排。先验证整批输入,再按稳定身份执行远端操作。
|
|
5
14
|
class Monitoring
|
|
6
|
-
|
|
7
|
-
DEFAULT_PRIORITY = 3
|
|
8
|
-
SUPPORTED_FUNCTIONS = %w[avg max min last].freeze
|
|
9
|
-
EXPRESSION_ITEM_KEY = /\A[A-Za-z0-9_.-]+(?:\[[A-Za-z0-9_.:,\/\-\s{}\#\$"']*\])?\z/.freeze
|
|
10
|
-
|
|
11
|
-
# 使用同一个 Manager 复用 API 会话和资源模块。
|
|
12
|
-
# @return [Monitoring] 监控业务对象
|
|
13
|
-
# @api public
|
|
15
|
+
# @param manager [ZabbixManager] 共享认证客户端和资源对象
|
|
14
16
|
def initialize(manager)
|
|
15
17
|
@manager = manager
|
|
18
|
+
@thresholds = Thresholds.new(manager.client)
|
|
19
|
+
@workflow_mutex = Mutex.new
|
|
16
20
|
end
|
|
17
21
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# @
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
# 按技术主机名装配设备监控;名称引用在写入前通过 API 解析。
|
|
23
|
+
# 同一实例内串行执行工作流,远端写入没有数据库事务或跨进程原子性。
|
|
24
|
+
# @param device [Hash] 原生主机属性及 groups/templates、snmp、proxy_group、managed 等业务选项
|
|
25
|
+
# @return [Hash] hostid、enabled 和下次更新可传回的 managed 集合归属回执
|
|
26
|
+
# @raise [Invalid, Conflict] 输入无效、引用不唯一或不支持当前服务端版本
|
|
27
|
+
# @raise [ApiError, TransportError] 远端操作失败;超时后应先核查远端状态
|
|
28
|
+
def reconcile_device(device)
|
|
29
|
+
definition = Device.new(device)
|
|
30
|
+
@workflow_mutex.synchronize { definition.reconcile(@manager) }
|
|
23
31
|
end
|
|
24
32
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# @
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{ status: :ok, device: safe_device_identity(device), hostid: hostid }
|
|
34
|
-
rescue ApiError, TransportError, ArgumentError => e
|
|
35
|
-
raise if fail_fast
|
|
36
|
-
|
|
37
|
-
failure_result(:device, safe_device_identity(device), e)
|
|
38
|
-
end
|
|
33
|
+
# 先校验整批本地定义,再逐台对账;单台失败不掩盖其他设备的结果。
|
|
34
|
+
# @param devices [Array<Hash>] 设备定义,技术主机名必须唯一
|
|
35
|
+
# @param fail_fast [Boolean] true 时首个远端错误向上抛出
|
|
36
|
+
# @return [Array<Hash>] 每台设备的身份、status 和 result,或脱敏 error
|
|
37
|
+
# @raise [Invalid] 本地定义无效,此时整批尚未写入
|
|
38
|
+
def reconcile_devices(devices, fail_fast: false)
|
|
39
|
+
Validation.boolean!(fail_fast, "fail_fast")
|
|
40
|
+
reconcile_device_definitions(validate_devices!(devices), fail_fast: fail_fast)
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# @
|
|
43
|
+
# 线路预检在设备写入之前完成;线路发现依赖设备或模板产生的监控项。
|
|
44
|
+
# 模板/LLD 异步产生监控项,设备成功不代表对应线路已就绪。
|
|
45
|
+
# @param devices [Array<Hash>] 设备定义
|
|
46
|
+
# @param lines [Array<Hash>] 单端点线路定义;双端使用独立 line_id
|
|
47
|
+
# @param fail_fast [Boolean] true 时首个远端错误向上抛出
|
|
48
|
+
# @return [Hash] devices、lines 逐项结果和成功/失败/未知数量 summary
|
|
44
49
|
def reconcile_network(devices:, lines:, fail_fast: false)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
results = {
|
|
50
|
+
Validation.boolean!(fail_fast, "fail_fast")
|
|
51
|
+
line_definitions = validate_lines!(lines)
|
|
52
|
+
device_definitions = validate_devices!(devices)
|
|
53
|
+
results = {
|
|
54
|
+
devices: reconcile_device_definitions(device_definitions, fail_fast: fail_fast),
|
|
55
|
+
lines: reconcile_line_definitions(line_definitions, fail_fast: fail_fast)
|
|
56
|
+
}
|
|
49
57
|
results.merge(summary: batch_summary(results))
|
|
50
58
|
end
|
|
51
59
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# @
|
|
55
|
-
# @
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
# 只读解析主机和监控项,预览实际触发器属性及按类型引用的依赖。
|
|
61
|
+
# ICMP 是 Zabbix server/proxy 对目标的检查,不能证明专线两端的转发路径。
|
|
62
|
+
# @param attributes [Hash] 单端点 line_id、host、interface_name、capacity_mbps 及可选监控配置
|
|
63
|
+
# @return [Hash] hostid、line_id、itemids、triggers、dependencies 和可选 icmp_item
|
|
64
|
+
# @raise [Invalid, Conflict] 阈值、量纲、项目选择或管理归属无法确定
|
|
65
|
+
# @raise [ApiError, TransportError] 远端发现失败,此方法从不写入
|
|
66
|
+
def plan_line(attributes)
|
|
67
|
+
prepare_line(Line.new(attributes), {})
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# @
|
|
63
|
-
# @
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
rescue ApiError, TransportError, ArgumentError => e
|
|
70
|
-
raise if fail_fast
|
|
71
|
-
|
|
72
|
-
failure_result(:line, safe_line_identity(line), e)
|
|
73
|
-
end
|
|
70
|
+
# 对账一个端点,确认所需触发器后停用该线路不再需要的受管类型。
|
|
71
|
+
# 同一实例串行执行;多步远端写入不具备事务回滚或跨进程原子性。
|
|
72
|
+
# @param attributes [Hash] 与 plan_line 相同的线路定义
|
|
73
|
+
# @return [Hash] 主机、接口、line_id、按指标分类的 itemids 和 triggerids
|
|
74
|
+
# @raise [Invalid, Conflict] 预检失败,此时没有线路写入
|
|
75
|
+
# @raise [ApiError, TransportError] 远端失败;已确认的步骤保留,传输失败可能已提交
|
|
76
|
+
def reconcile_line(attributes)
|
|
77
|
+
line = Line.new(attributes)
|
|
78
|
+
@workflow_mutex.synchronize { apply_line(prepare_line(line, {})) }
|
|
74
79
|
end
|
|
75
80
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# @return [Hash] 监控项和触发器 ID
|
|
85
|
-
# @api public
|
|
86
|
-
def reconcile_interface(data)
|
|
87
|
-
input = data.deep_symbolize_keys
|
|
88
|
-
host = resolve_explicit_host(input.fetch(:host))
|
|
89
|
-
interface = input.fetch(:interface)
|
|
90
|
-
validate_interface!(interface)
|
|
91
|
-
|
|
92
|
-
prepared_items, item_keys = prepare_items(host, interface, input.fetch(:items, {}))
|
|
93
|
-
thresholds = input.fetch(:thresholds, {})
|
|
94
|
-
validate_threshold_items!(thresholds, prepared_items)
|
|
95
|
-
prepared_triggers = prepare_thresholds(
|
|
96
|
-
host: host,
|
|
97
|
-
interface: interface,
|
|
98
|
-
item_keys: item_keys,
|
|
99
|
-
thresholds: thresholds
|
|
100
|
-
)
|
|
101
|
-
itemids = reconcile_items(prepared_items)
|
|
102
|
-
triggerids = reconcile_thresholds(prepared_triggers)
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
hostid: host[:hostid].to_i,
|
|
106
|
-
interface: interface[:name],
|
|
107
|
-
itemids: itemids,
|
|
108
|
-
triggerids: triggerids
|
|
109
|
-
}
|
|
81
|
+
# 先预检整批本地输入和远端发现,再逐端点执行,保留失败与结果未知的区别。
|
|
82
|
+
# @param lines [Array<Hash>] line_id 唯一的端点定义;双端线路使用两个不同 line_id
|
|
83
|
+
# @param fail_fast [Boolean] 遇到首个远端失败时向上抛出
|
|
84
|
+
# @return [Array<Hash>] 每个端点的 status、result 或脱敏 error
|
|
85
|
+
# @raise [Invalid] 本地定义或解析后的目标重复
|
|
86
|
+
def reconcile_lines(lines, fail_fast: false)
|
|
87
|
+
Validation.boolean!(fail_fast, "fail_fast")
|
|
88
|
+
reconcile_line_definitions(validate_lines!(lines), fail_fast: fail_fast)
|
|
110
89
|
end
|
|
111
90
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
traffic = find_line_traffic_items(host.fetch(:hostid), interface_name, cache: cache)
|
|
122
|
-
capacity_mbps = numeric!(input[:capacity_mbps] || input[:capacity], "line capacity_mbps")
|
|
123
|
-
raise Invalid, "line capacity_mbps must be positive" unless capacity_mbps.positive?
|
|
91
|
+
# 查询端点的受管触发器;人工触发器及其他管理方的同名对象不在结果中。
|
|
92
|
+
# @param hostid [Integer, String] 目标主机 ID
|
|
93
|
+
# @param line_id [String, Integer] 端点的稳定身份,或 plan_line 返回的自动身份
|
|
94
|
+
# @return [Array<Hash>] 原生 trigger.get 对象,包括 tags、items、状态及表达式
|
|
95
|
+
# @raise [Invalid, Conflict, ProtocolError] 输入无效或远端管理身份不唯一
|
|
96
|
+
def line_triggers(hostid:, line_id:)
|
|
97
|
+
LineLifecycle.new(@manager, hostid, line_id).owned
|
|
98
|
+
end
|
|
124
99
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
prepared = line_thresholds(input, host, interface, item_keys, capacity_mbps, high_water, recovery_water)
|
|
135
|
-
triggerid = reconcile_thresholds(prepared).fetch(:bandwidth)
|
|
100
|
+
# 停用一个端点的全部受管触发器;不删除触发器或共享 ICMP 监控项。
|
|
101
|
+
# @param hostid [Integer, String] 目标主机 ID
|
|
102
|
+
# @param line_id [String, Integer] 端点的稳定身份
|
|
103
|
+
# @return [Array<Integer>] 已确认停用的触发器 ID;没有受管对象时为空
|
|
104
|
+
# @raise [ApiError, TransportError] 写入失败或结果尚未确认
|
|
105
|
+
def disable_line(hostid:, line_id:)
|
|
106
|
+
lifecycle = LineLifecycle.new(@manager, hostid, line_id)
|
|
107
|
+
@workflow_mutex.synchronize { lifecycle.disable }
|
|
108
|
+
end
|
|
136
109
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
110
|
+
# 按该端点的触发器 ID 在服务端过滤问题,再应用数量上限。
|
|
111
|
+
# Zabbix problem.get 的 recent 范围受服务器问题保留配置影响,不代表完整事件归档。
|
|
112
|
+
# @param hostid [Integer, String] 目标主机 ID
|
|
113
|
+
# @param line_id [String, Integer] 端点的稳定身份
|
|
114
|
+
# @param time_from [Integer, Time] 查询起点(Unix 秒或 Time)
|
|
115
|
+
# @param time_till [Integer, Time] 查询终点,默认为当前时间
|
|
116
|
+
# @param limit [Integer] 最大记录数,1..1000;多读取一条以判定截断
|
|
117
|
+
# @return [Hash] problems、truncated、time_from、time_till 和 limit
|
|
118
|
+
# @raise [Invalid, ProtocolError] 范围无效或响应超出目标线路
|
|
119
|
+
def line_problems(hostid:, line_id:, time_from:, time_till: Time.now, limit: 100)
|
|
120
|
+
LineLifecycle.new(@manager, hostid, line_id).problems(time_from: time_from, time_till: time_till, limit: limit)
|
|
121
|
+
end
|
|
145
122
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
123
|
+
# 接口更新只增改,不自动删除远端对象;所有本地校验在首个写入之前完成。
|
|
124
|
+
# @param attributes [Hash] host、interface、按名称分组的原生 items 和 thresholds
|
|
125
|
+
# @return [Hash] hostid、interface、itemids 和 triggerids
|
|
126
|
+
# @raise [Invalid, Conflict] 输入、量纲或资源归属无效
|
|
127
|
+
# @raise [ApiError, TransportError] 远端失败;已经确认的早期步骤不会被回滚
|
|
128
|
+
def reconcile_interface(attributes)
|
|
129
|
+
input = Validation.hash!(attributes, "interface monitoring")
|
|
130
|
+
input.assert_valid_keys(:host, :interface, :items, :thresholds)
|
|
131
|
+
supplied_host = Validation.hash!(input[:host], "host")
|
|
132
|
+
Validation.host!(supplied_host)
|
|
133
|
+
interface = Validation.hash!(input[:interface], "interface")
|
|
134
|
+
interface.assert_valid_keys(:name, :interfaceid)
|
|
135
|
+
Validation.text!(interface[:name], "interface.name")
|
|
136
|
+
Validation.positive_id!(interface[:interfaceid], "interface.interfaceid") if interface.key?(:interfaceid)
|
|
137
|
+
items = @thresholds.prepare_items(supplied_host, interface, input.fetch(:items, {}))
|
|
138
|
+
triggers = @thresholds.prepare_triggers(
|
|
139
|
+
host: supplied_host, interface: interface, items: items, thresholds: input.fetch(:thresholds, {}),
|
|
140
|
+
validate_items: false
|
|
141
|
+
)
|
|
142
|
+
host = resolve_host(supplied_host, {})
|
|
143
|
+
ids = @manager.items.upsert_many(items.values) do |effective|
|
|
144
|
+
# 读取已有配置后再次校验最终量纲和数值类型,仍在首个 item 写入之前。
|
|
145
|
+
triggers = @thresholds.prepare_triggers(
|
|
146
|
+
host: supplied_host, interface: interface, items: items.keys.zip(effective).to_h,
|
|
147
|
+
thresholds: input.fetch(:thresholds, {})
|
|
164
148
|
)
|
|
165
|
-
prepared[:bandwidth][:legacy_identity_tags] = [
|
|
166
|
-
{ tag: "category", value: "line_bandwidth", operator: 1 }
|
|
167
|
-
]
|
|
168
|
-
prepared[:bandwidth][:comments] = line_comments(input, capacity_mbps, high_water, recovery_water)
|
|
169
|
-
prepared
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# 补齐监控项的主机、接口和名称,并检查 key 唯一性。
|
|
173
|
-
def prepare_items(host, interface, items)
|
|
174
|
-
prepared = items.each_with_object({}) do |(metric, raw_attributes), result|
|
|
175
|
-
attributes = raw_attributes.deep_symbolize_keys
|
|
176
|
-
attributes[:hostid] = host[:hostid]
|
|
177
|
-
attributes[:interfaceid] ||= interface[:interfaceid] if interface[:interfaceid]
|
|
178
|
-
attributes[:name] ||= "#{interface[:name]} #{metric.to_s.tr("_", " ")}"
|
|
179
|
-
raise Invalid, "item #{metric} key_ is required" if attributes[:key_].blank?
|
|
180
|
-
unless attributes[:key_].to_s.match?(EXPRESSION_ITEM_KEY)
|
|
181
|
-
raise Invalid, "item #{metric} key_ contains unsupported expression characters"
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
result[metric.to_sym] = attributes
|
|
185
|
-
end
|
|
186
|
-
keys = prepared.transform_values { |attributes| attributes.fetch(:key_) }
|
|
187
|
-
if keys.values.uniq.length != keys.length
|
|
188
|
-
raise Invalid, "item key_ values must be unique within an interface"
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
[prepared, keys]
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# 把业务阈值转换为 Zabbix 问题和恢复表达式。
|
|
195
|
-
def prepare_thresholds(host:, interface:, item_keys:, thresholds:)
|
|
196
|
-
thresholds.each_with_object({}) do |(metric, raw_config), result|
|
|
197
|
-
config = raw_config.deep_symbolize_keys
|
|
198
|
-
keys = threshold_item_keys(metric, config, item_keys)
|
|
199
|
-
high, recovery = threshold_values(metric, config)
|
|
200
|
-
validate_threshold!(metric, high, recovery)
|
|
201
|
-
window = validate_window!(config.fetch(:window, DEFAULT_WINDOW))
|
|
202
|
-
recovery_window = validate_window!(config.fetch(:recovery_window, window))
|
|
203
|
-
function = validate_function!(config.fetch(:function, "avg"))
|
|
204
|
-
description = config[:description] || "#{interface[:name]} #{metric.to_s.tr("_", " ")} high"
|
|
205
|
-
raise Invalid, "threshold #{metric} description is required" if description.blank?
|
|
206
|
-
|
|
207
|
-
priority = validate_priority!(config.fetch(:priority, DEFAULT_PRIORITY))
|
|
208
|
-
|
|
209
|
-
result[metric.to_sym] = {
|
|
210
|
-
hostid: host[:hostid],
|
|
211
|
-
description: description,
|
|
212
|
-
expression: combine_expressions(host[:host], keys, function, window, ">", high, "or"),
|
|
213
|
-
recovery_mode: 1,
|
|
214
|
-
recovery_expression: combine_expressions(
|
|
215
|
-
host[:host], keys, function, recovery_window, "<", recovery, "and"
|
|
216
|
-
),
|
|
217
|
-
priority: priority,
|
|
218
|
-
manual_close: 1,
|
|
219
|
-
tags: managed_tags(interface[:name], metric, config[:tags])
|
|
220
|
-
}
|
|
221
|
-
result[metric.to_sym][:managed_key] = managed_key(interface, metric)
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# 在远端写入前校验各指标的单位和速率语义。
|
|
226
|
-
def validate_threshold_items!(thresholds, items)
|
|
227
|
-
thresholds.each do |metric, raw_config|
|
|
228
|
-
config = raw_config.deep_symbolize_keys
|
|
229
|
-
names = Array(config[:metrics] || default_metrics(metric)).map(&:to_sym)
|
|
230
|
-
selected = names.map do |name|
|
|
231
|
-
items.fetch(name) { raise Invalid, "threshold #{metric} requires item metric #{name}" }
|
|
232
|
-
end
|
|
233
|
-
case metric.to_sym
|
|
234
|
-
when :bandwidth
|
|
235
|
-
selected.each { |item| validate_bps_item!(item) }
|
|
236
|
-
when :errors
|
|
237
|
-
selected.each { |item| validate_error_rate_item!(item) }
|
|
238
|
-
when :packet_loss
|
|
239
|
-
selected.each { |item| validate_packet_loss_item!(item) }
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# 确认误码指标表示每秒速率而不是累计计数器。
|
|
245
|
-
def validate_error_rate_item!(item)
|
|
246
|
-
values = item.with_indifferent_access
|
|
247
|
-
preprocessing = Array(values[:preprocessing])
|
|
248
|
-
rate_step = preprocessing.any? { |step| (step["type"] || step[:type]).to_i == 10 }
|
|
249
|
-
explicit_rate = [values[:key_], values[:name]].join(" ").match?(/rate|per[._ -]?second/i)
|
|
250
|
-
return if rate_step || explicit_rate
|
|
251
|
-
|
|
252
|
-
raise Invalid, "error metric #{values[:key_]} must represent a per-second rate"
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# 确认丢包指标使用百分比单位。
|
|
256
|
-
def validate_packet_loss_item!(item)
|
|
257
|
-
values = item.with_indifferent_access
|
|
258
|
-
return if values[:units].to_s == "%"
|
|
259
|
-
|
|
260
|
-
raise Invalid, "packet loss metric #{values[:key_]} must use % units"
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# 按稳定 key 写入已准备好的监控项。
|
|
264
|
-
def reconcile_items(prepared_items)
|
|
265
|
-
prepared_items.transform_values { |attributes| @manager.items.upsert_by_key(attributes) }
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# 按稳定管理标签写入已准备好的触发器。
|
|
269
|
-
def reconcile_thresholds(prepared_triggers)
|
|
270
|
-
prepared_triggers.transform_values { |attributes| @manager.triggers.upsert_for_host(attributes) }
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# 在批量写入前拒绝重复线路 ID。
|
|
274
|
-
def validate_line_batch!(lines)
|
|
275
|
-
identities = lines.filter_map { |line| line[:line_id].presence || line[:id].presence }.map(&:to_s)
|
|
276
|
-
duplicate = identities.tally.find { |_identity, count| count > 1 }&.first
|
|
277
|
-
raise Invalid, "duplicate line_id #{duplicate}" if duplicate
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# 规范化并完整预检线路集合,保证校验阶段不执行远端写入。
|
|
281
|
-
def validate_line_collection!(collection)
|
|
282
|
-
lines = Array(collection).map(&:deep_symbolize_keys)
|
|
283
|
-
validate_line_batch!(lines)
|
|
284
|
-
lines.each { |line| validate_line_definition!(line) }
|
|
285
|
-
lines
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
# 在任何远端写入前校验线路必填字段和阈值格式。
|
|
289
|
-
def validate_line_definition!(line)
|
|
290
|
-
validate_line_host_reference!(line)
|
|
291
|
-
interface_name = line[:interface_name] || line[:iface] || line[:iface1]
|
|
292
|
-
raise Invalid, "line interface_name is required" if interface_name.blank?
|
|
293
|
-
|
|
294
|
-
capacity = numeric!(line[:capacity_mbps] || line[:capacity], "line capacity_mbps")
|
|
295
|
-
raise Invalid, "line capacity_mbps must be positive" unless capacity.positive?
|
|
296
|
-
|
|
297
|
-
high_water = water_ratio!(line.fetch(:high_water, 0.90), "high_water")
|
|
298
|
-
recovery_water = water_ratio!(line.fetch(:recovery_water, 0.80), "recovery_water")
|
|
299
|
-
raise Invalid, "recovery_water must be lower than high_water" unless recovery_water < high_water
|
|
300
|
-
|
|
301
|
-
validate_window!(line.fetch(:problem_window, DEFAULT_WINDOW))
|
|
302
|
-
validate_window!(line.fetch(:recovery_window, "15m"))
|
|
303
|
-
validate_priority!(line.fetch(:severity, DEFAULT_PRIORITY))
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
# 校验线路提供显式主机对象或至少一个可解析的设备候选值。
|
|
307
|
-
def validate_line_host_reference!(line)
|
|
308
|
-
supplied = line[:host]
|
|
309
|
-
if supplied.respond_to?(:deep_symbolize_keys)
|
|
310
|
-
validate_host!(supplied.deep_symbolize_keys)
|
|
311
|
-
return
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
candidates = Array(line[:host_candidates])
|
|
315
|
-
candidates.concat([supplied, line[:device], line[:device1], line[:ip], line[:ipaddr1], line[:serial]])
|
|
316
|
-
candidates = candidates.filter_map { |value| value.to_s.strip.presence }
|
|
317
|
-
candidates.delete("IP地址")
|
|
318
|
-
raise Invalid, "line host candidate is required" if candidates.empty?
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
# 在任何远端写入前拒绝同一技术主机名的重复设备定义。
|
|
322
|
-
def validate_device_batch!(devices)
|
|
323
|
-
identities = devices.map { |device| device.fetch(:host).to_s }
|
|
324
|
-
duplicate = identities.tally.find { |_identity, count| count > 1 }&.first
|
|
325
|
-
raise Invalid, "duplicate device host #{duplicate}" if duplicate
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# 只返回可安全记录的设备身份字段。
|
|
329
|
-
def safe_device_identity(device)
|
|
330
|
-
{ host: device[:host], name: device[:name] }.compact
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
# 构造设备或线路的脱敏失败结果。
|
|
334
|
-
def failure_result(kind, identity, error)
|
|
335
|
-
{
|
|
336
|
-
status: :error,
|
|
337
|
-
kind => identity,
|
|
338
|
-
error: { class: error.class.name, message: LogSanitizer.sanitize(error.message) }
|
|
339
|
-
}
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
# 汇总批次两个阶段的成功与失败数量。
|
|
343
|
-
def batch_summary(results)
|
|
344
|
-
results.each_with_object({}) do |(kind, entries), summary|
|
|
345
|
-
summary[kind] = {
|
|
346
|
-
total: entries.length,
|
|
347
|
-
succeeded: entries.count { |entry| entry[:status] == :ok },
|
|
348
|
-
failed: entries.count { |entry| entry[:status] == :error }
|
|
349
|
-
}
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# 只返回可安全记录的线路身份字段。
|
|
354
|
-
def safe_line_identity(line)
|
|
355
|
-
{
|
|
356
|
-
line_id: line[:line_id].presence || line[:id].presence,
|
|
357
|
-
device: line[:device] || line[:device1],
|
|
358
|
-
interface: line[:interface_name] || line[:iface] || line[:iface1]
|
|
359
|
-
}.compact
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# 解析一个阈值引用的监控项 key 集合。
|
|
363
|
-
def threshold_item_keys(metric, config, item_keys)
|
|
364
|
-
metric_names = Array(config[:metrics] || default_metrics(metric))
|
|
365
|
-
keys = metric_names.map do |name|
|
|
366
|
-
item_keys.fetch(name.to_sym) do
|
|
367
|
-
raise Invalid, "threshold #{metric} requires item metric #{name}"
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
|
-
raise Invalid, "threshold #{metric} requires at least one item" if keys.empty?
|
|
371
|
-
|
|
372
|
-
keys
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
# 返回常用指标默认依赖的监控项名称。
|
|
376
|
-
def default_metrics(metric)
|
|
377
|
-
case metric.to_sym
|
|
378
|
-
when :bandwidth then %i[inbound_bps outbound_bps]
|
|
379
|
-
when :errors then %i[in_errors out_errors]
|
|
380
|
-
when :packet_loss then [:packet_loss]
|
|
381
|
-
else [metric.to_sym]
|
|
382
|
-
end
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
# 计算带宽百分比或普通指标的高低阈值。
|
|
386
|
-
def threshold_values(metric, config)
|
|
387
|
-
if metric.to_sym == :bandwidth
|
|
388
|
-
capacity = numeric!(config[:capacity_bps], "bandwidth capacity_bps")
|
|
389
|
-
high_percent = numeric!(config[:high_percent], "bandwidth high_percent")
|
|
390
|
-
recovery_percent = numeric!(config[:recovery_percent], "bandwidth recovery_percent")
|
|
391
|
-
unless capacity.positive? && high_percent.between?(0, 100) && recovery_percent.between?(0, 100)
|
|
392
|
-
raise Invalid, "bandwidth capacity must be positive and percentages must be between 0 and 100"
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
[capacity * high_percent / 100.0, capacity * recovery_percent / 100.0]
|
|
396
|
-
else
|
|
397
|
-
[numeric!(config[:high], "#{metric} high"), numeric!(config[:recovery], "#{metric} recovery")]
|
|
398
|
-
end
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
# 校验恢复阈值低于问题阈值,形成滞回区间。
|
|
402
|
-
def validate_threshold!(metric, high, recovery)
|
|
403
|
-
return if high.positive? && recovery >= 0 && recovery < high
|
|
404
|
-
|
|
405
|
-
raise Invalid, "#{metric} recovery threshold must be non-negative and lower than high threshold"
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
# 组合多个方向的触发器表达式。
|
|
409
|
-
def combine_expressions(host, keys, function, window, operator, threshold, joiner)
|
|
410
|
-
keys.map do |key|
|
|
411
|
-
function_call = expression_function(host, key, function, window)
|
|
412
|
-
"#{function_call}#{operator}#{format_number(threshold)}"
|
|
413
|
-
end.join(" #{joiner} ")
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
# 按 Zabbix 版本生成新式或旧式函数表达式。
|
|
417
|
-
def expression_function(host, key, function, window)
|
|
418
|
-
if modern_trigger_expression?
|
|
419
|
-
source = "/#{host}/#{key}"
|
|
420
|
-
function == "last" ? "last(#{source})" : "#{function}(#{source},#{window})"
|
|
421
|
-
else
|
|
422
|
-
parameter = function == "last" ? "" : window
|
|
423
|
-
"{#{host}:#{key}.#{function}(#{parameter})}"
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# 判断服务端是否支持 Zabbix 5.4 起的新表达式语法。
|
|
428
|
-
def modern_trigger_expression?
|
|
429
|
-
version = @manager.client.api_version.to_s.split(".").first(2).map(&:to_i)
|
|
430
|
-
(version <=> [5, 4]) >= 0
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
# 合并系统管理标签和调用方扩展标签。
|
|
434
|
-
def managed_tags(interface_name, metric, extra_tags)
|
|
435
|
-
tags = [
|
|
436
|
-
{ tag: "managed_by", value: "zabbix_manager" },
|
|
437
|
-
{ tag: "interface", value: interface_name.to_s },
|
|
438
|
-
{ tag: "metric", value: metric.to_s }
|
|
439
|
-
]
|
|
440
|
-
tags.concat(Array(extra_tags).map do |tag|
|
|
441
|
-
unless tag.respond_to?(:deep_symbolize_keys)
|
|
442
|
-
raise Invalid, "threshold tags must contain a non-empty tag name"
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
normalized = tag.deep_symbolize_keys
|
|
446
|
-
raise Invalid, "threshold tags must contain a non-empty tag name" if normalized[:tag].blank?
|
|
447
|
-
|
|
448
|
-
normalized
|
|
449
|
-
end)
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
# 生成不受描述和数组顺序影响的触发器管理键。
|
|
453
|
-
def managed_key(interface, metric)
|
|
454
|
-
identity = interface[:line_id].presence || interface[:interfaceid].presence ||
|
|
455
|
-
canonical_interface_identity(interface[:name])
|
|
456
|
-
"interface:#{identity}:#{metric}"
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
# 从显式主机或历史线路字段中解析唯一主机。
|
|
460
|
-
def resolve_line_host(input, cache: nil)
|
|
461
|
-
supplied = input[:host]
|
|
462
|
-
if supplied.respond_to?(:deep_symbolize_keys)
|
|
463
|
-
return resolve_explicit_host(supplied)
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
candidates = Array(input[:host_candidates])
|
|
467
|
-
candidates.concat([supplied, input[:device], input[:device1], input[:ip], input[:ipaddr1], input[:serial]])
|
|
468
|
-
candidates = candidates.filter_map { |value| value.to_s.strip.presence }
|
|
469
|
-
candidates.delete("IP地址")
|
|
470
|
-
raise Invalid, "line host candidate is required" if candidates.empty?
|
|
471
|
-
|
|
472
|
-
cache_key = candidates.join("\0")
|
|
473
|
-
found = if cache
|
|
474
|
-
cache.fetch(:hosts)[cache_key] ||= @manager.hosts.find_by_candidates(candidates)
|
|
475
|
-
else
|
|
476
|
-
@manager.hosts.find_by_candidates(candidates)
|
|
477
|
-
end
|
|
478
|
-
raise ApiError, "Zabbix host not found for #{candidates.join(", ")}" unless found
|
|
479
|
-
|
|
480
|
-
found.deep_symbolize_keys
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
# 查询并精确选择接口的入向和出向流量项。
|
|
484
|
-
def find_line_traffic_items(hostid, interface_name, cache: nil)
|
|
485
|
-
aliases = interface_aliases(interface_name)
|
|
486
|
-
source_items = if cache
|
|
487
|
-
cache.fetch(:items)[hostid.to_s] ||= @manager.items.monitored_traffic_candidates(hostid)
|
|
488
|
-
else
|
|
489
|
-
@manager.items.monitored_traffic_candidates(hostid)
|
|
490
|
-
end
|
|
491
|
-
candidates = source_items.select do |item|
|
|
492
|
-
traffic_item?(item) && interface_item?(item, aliases)
|
|
493
|
-
end
|
|
494
|
-
selected = {
|
|
495
|
-
inbound: unique_direction_item!(candidates, :inbound, interface_name),
|
|
496
|
-
outbound: unique_direction_item!(candidates, :outbound, interface_name)
|
|
497
|
-
}
|
|
498
|
-
selected.each_value { |item| validate_bps_item!(item) }
|
|
499
|
-
selected
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
# 把接口名称转换为统一身份集合。
|
|
503
|
-
def interface_aliases(interface_name)
|
|
504
|
-
[canonical_interface_identity(interface_name)]
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
# 统一全称、缩写和分隔符,生成稳定接口身份。
|
|
508
|
-
def canonical_interface_identity(value)
|
|
509
|
-
value.to_s.downcase.gsub(/\s+|[-_]/, "")
|
|
510
|
-
.sub(/^tengigabitethernet/, "te")
|
|
511
|
-
.sub(/^gigabitethernet/, "gi")
|
|
512
|
-
.sub(/^ethernet/, "eth")
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
# 判断监控项字段是否包含目标接口的精确 token。
|
|
516
|
-
def interface_item?(item, aliases)
|
|
517
|
-
fields = [item["name"], item["key_"], item["snmp_oid"]].compact.map(&:to_s)
|
|
518
|
-
discovered = fields.flat_map { |field| interface_tokens(field) }.map do |name|
|
|
519
|
-
canonical_interface_identity(name)
|
|
520
|
-
end
|
|
521
|
-
aliases.any? { |name| discovered.include?(name) }
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
# 从名称、key 和 OID 文本中抽取完整接口 token。
|
|
525
|
-
def interface_tokens(value)
|
|
526
|
-
prefix = "(?:Ten-?GigabitEthernet|GigabitEthernet|Ethernet|Te|Gi|Eth)"
|
|
527
|
-
pattern = /(?:#{prefix})?\d+(?:\/\d+)+(?:[.:]\d+)*(?![A-Za-z0-9\/._-]|:\d)/i
|
|
528
|
-
value.to_s.scan(pattern)
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
# 排除误码、丢包和状态项,只保留可能的流量项。
|
|
532
|
-
def traffic_item?(item)
|
|
533
|
-
oid = item["snmp_oid"].to_s
|
|
534
|
-
blob = [item["key_"], item["name"]].join(" ").downcase
|
|
535
|
-
return false if blob.match?(/error|discard|drop|packet|status|utilization|percentage|percent/)
|
|
536
|
-
|
|
537
|
-
oid.match?(/1\.3\.6\.1\.2\.1\.31\.1\.1\.1\.(6|10)(\.|$)/) ||
|
|
538
|
-
blob.match?(/net\.if\.(in|out)(?:\[[^,\]]+\]|\b)|ifhc(in|out)octets|inbound|outbound/)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
# 确保一个方向只匹配一个监控项。
|
|
542
|
-
def unique_direction_item!(candidates, direction, interface_name)
|
|
543
|
-
matches = candidates.select { |item| direction_item?(item, direction) }
|
|
544
|
-
if matches.length != 1
|
|
545
|
-
raise Conflict, "expected one #{direction} traffic item for #{interface_name}, found #{matches.length}"
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
matches.first
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
# 根据 OID、key 和名称判断监控项方向。
|
|
552
|
-
def direction_item?(item, direction)
|
|
553
|
-
oid = item["snmp_oid"].to_s
|
|
554
|
-
blob = [item["key_"], item["name"]].join(" ").downcase
|
|
555
|
-
if direction == :inbound
|
|
556
|
-
oid.match?(/1\.3\.6\.1\.2\.1\.31\.1\.1\.1\.6(\.|$)/) || blob.match?(/net\.if\.in|ifhcin|inbound/)
|
|
557
|
-
else
|
|
558
|
-
oid.match?(/1\.3\.6\.1\.2\.1\.31\.1\.1\.1\.10(\.|$)/) || blob.match?(/net\.if\.out|ifhcout|outbound/)
|
|
559
|
-
end
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
# 校验流量项单位及原始八位组预处理链。
|
|
563
|
-
def validate_bps_item!(item)
|
|
564
|
-
values = item.with_indifferent_access
|
|
565
|
-
unless values[:units].to_s.casecmp("bps").zero?
|
|
566
|
-
raise Invalid, "traffic item #{values[:itemid] || values[:key_]} must use bps units"
|
|
567
|
-
end
|
|
568
|
-
return unless raw_octet_counter?(values)
|
|
569
|
-
|
|
570
|
-
preprocessing = Array(values[:preprocessing])
|
|
571
|
-
change_per_second = preprocessing.any? { |step| (step["type"] || step[:type]).to_i == 10 }
|
|
572
|
-
bits_multiplier = preprocessing.any? do |step|
|
|
573
|
-
type = step["type"] || step[:type]
|
|
574
|
-
params = step["params"] || step[:params]
|
|
575
|
-
type.to_i == 1 && numeric!(params, "traffic preprocessing multiplier") == 8
|
|
576
|
-
end
|
|
577
|
-
return if change_per_second && bits_multiplier
|
|
578
|
-
|
|
579
|
-
identity = values[:itemid] || values[:key_]
|
|
580
|
-
raise Invalid, "raw octet item #{identity} requires change-per-second and multiplier 8 preprocessing"
|
|
581
|
-
end
|
|
582
|
-
|
|
583
|
-
# 判断监控项是否直接读取 HC Octets 累计计数器。
|
|
584
|
-
def raw_octet_counter?(item)
|
|
585
|
-
values = item.with_indifferent_access
|
|
586
|
-
oid = values[:snmp_oid].to_s
|
|
587
|
-
key = values[:key_].to_s.downcase
|
|
588
|
-
oid.match?(/1\.3\.6\.1\.2\.1\.31\.1\.1\.1\.(6|10)(\.|$)/) || key.match?(/ifhc(in|out)octets/)
|
|
589
149
|
end
|
|
150
|
+
itemids = items.keys.zip(ids).to_h
|
|
151
|
+
triggerids = triggers.transform_values { |trigger| @manager.triggers.upsert_for_host(trigger) }
|
|
152
|
+
{ hostid: host[:hostid].to_i, interface: interface[:name], itemids: itemids, triggerids: triggerids }
|
|
153
|
+
end
|
|
590
154
|
|
|
591
|
-
|
|
592
|
-
def water_ratio!(value, name)
|
|
593
|
-
ratio = numeric!(value, name)
|
|
594
|
-
ratio /= 100.0 if ratio > 1
|
|
595
|
-
raise Invalid, "#{name} must be greater than 0 and at most 1" unless ratio.positive? && ratio <= 1
|
|
155
|
+
private
|
|
596
156
|
|
|
597
|
-
|
|
598
|
-
|
|
157
|
+
def validate_devices!(devices)
|
|
158
|
+
definitions = Validation.array!(devices, "devices").map { |device| Device.new(device) }
|
|
159
|
+
validate_unique!(definitions.map(&:identity), "device host")
|
|
160
|
+
definitions
|
|
161
|
+
end
|
|
599
162
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
163
|
+
def validate_lines!(lines)
|
|
164
|
+
definitions = Validation.array!(lines, "lines").map { |attributes| Line.new(attributes) }
|
|
165
|
+
validate_unique!(definitions.map(&:identity), "line_id or host/interface")
|
|
166
|
+
definitions
|
|
167
|
+
end
|
|
605
168
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
isp: input[:isp],
|
|
611
|
-
description: input[:description],
|
|
612
|
-
device: input[:device] || input[:device1],
|
|
613
|
-
iface: interface_name
|
|
614
|
-
}.filter_map { |tag, value| { tag: tag.to_s, value: value.to_s } if value.present? }
|
|
615
|
-
end
|
|
169
|
+
def validate_unique!(identities, name)
|
|
170
|
+
duplicate = identities.tally.find { |_identity, count| count > 1 }
|
|
171
|
+
raise Invalid, "duplicate #{name}" if duplicate
|
|
172
|
+
end
|
|
616
173
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
174
|
+
def reconcile_device_definitions(devices, fail_fast:)
|
|
175
|
+
devices.map do |device|
|
|
176
|
+
identity = device.safe_identity
|
|
177
|
+
result = @workflow_mutex.synchronize { device.reconcile(@manager) }
|
|
178
|
+
{ status: :ok, device: identity, result: result }
|
|
179
|
+
rescue ApiError, TransportError, Invalid => error
|
|
180
|
+
raise if fail_fast
|
|
622
181
|
|
|
623
|
-
|
|
624
|
-
def validate_host!(host)
|
|
625
|
-
raise Invalid, "host.hostid is required" if host[:hostid].blank?
|
|
626
|
-
raise Invalid, "host.host is required" if host[:host].blank?
|
|
627
|
-
raise Invalid, "host.host cannot contain /" if host[:host].to_s.include?("/")
|
|
182
|
+
failure_result(:device, identity, error, writing: true)
|
|
628
183
|
end
|
|
184
|
+
end
|
|
629
185
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
resolved = remote.deep_symbolize_keys
|
|
638
|
-
if resolved[:host].to_s != supplied[:host].to_s
|
|
639
|
-
raise Conflict, "hostid and technical host name do not refer to the same Zabbix host"
|
|
640
|
-
end
|
|
186
|
+
# 先完成线路发现,防止两个不同候选主机名解析到同一触发器后互相覆盖。
|
|
187
|
+
def reconcile_line_definitions(lines, fail_fast:)
|
|
188
|
+
cache = {}
|
|
189
|
+
prepared = lines.map do |line|
|
|
190
|
+
{ line: line, plan: prepare_line(line, cache) }
|
|
191
|
+
rescue ApiError, TransportError, Invalid => error
|
|
192
|
+
raise if fail_fast
|
|
641
193
|
|
|
642
|
-
|
|
643
|
-
resolved
|
|
194
|
+
{ line: line, failure: failure_result(:line, line.safe_identity, error) }
|
|
644
195
|
end
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
raise Invalid, "interface.name is required" if interface[:name].blank?
|
|
196
|
+
identities = prepared.filter_map do |entry|
|
|
197
|
+
plan = entry[:plan]
|
|
198
|
+
[plan[:hostid], plan[:line_id]] if plan
|
|
649
199
|
end
|
|
200
|
+
validate_unique!(identities, "line target")
|
|
201
|
+
prepared.map do |entry|
|
|
202
|
+
next entry[:failure] if entry[:failure]
|
|
650
203
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
raise Invalid, "window must use a Zabbix duration such as 5m or 1h" unless value.match?(/\A\d+[smhdw]\z/)
|
|
204
|
+
{ status: :ok, result: @workflow_mutex.synchronize { apply_line(entry[:plan]) } }
|
|
205
|
+
rescue ApiError, TransportError, Invalid => error
|
|
206
|
+
raise if fail_fast
|
|
655
207
|
|
|
656
|
-
|
|
208
|
+
failure_result(:line, entry[:line].safe_identity, error, writing: true)
|
|
657
209
|
end
|
|
210
|
+
end
|
|
658
211
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
212
|
+
def prepare_line(line, cache)
|
|
213
|
+
host = resolve_host(line.host_reference, cache)
|
|
214
|
+
source_items = cache[[:items, host[:hostid].to_s]] ||=
|
|
215
|
+
@manager.items.monitored_traffic_candidates(host[:hostid])
|
|
216
|
+
resolver = LineItems.new(@manager.items)
|
|
217
|
+
selected = resolver.resolve(line, source_items, hostid: host[:hostid])
|
|
218
|
+
icmp_item = resolver.icmp_plan(host[:hostid], line.reachability_target) if line.reachability_target
|
|
219
|
+
plan = LinePlan.new(@manager.client, line, host, selected, icmp_item).to_h
|
|
220
|
+
lifecycle = LineLifecycle.new(@manager, plan[:hostid], plan[:line_id])
|
|
221
|
+
lifecycle.validate_ownership!(lifecycle.inventory, plan[:triggers].values.pluck(:managed_key))
|
|
222
|
+
plan
|
|
223
|
+
end
|
|
668
224
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
225
|
+
def apply_line(plan)
|
|
226
|
+
lifecycle = LineLifecycle.new(@manager, plan[:hostid], plan[:line_id])
|
|
227
|
+
existing = lifecycle.inventory
|
|
228
|
+
desired_keys = plan[:triggers].values.pluck(:managed_key)
|
|
229
|
+
lifecycle.validate_ownership!(existing, desired_keys)
|
|
230
|
+
itemids = plan[:itemids].dup
|
|
231
|
+
if plan[:icmp_item]
|
|
232
|
+
itemids[:reachability] = LineItems.new(@manager.items).ensure_icmp(plan[:icmp_item])
|
|
233
|
+
end
|
|
234
|
+
triggerids = {}
|
|
235
|
+
plan[:triggers].each do |kind, attributes|
|
|
236
|
+
dependencies = plan[:dependencies].fetch(kind).map { |dependency| { triggerid: triggerids.fetch(dependency) } }
|
|
237
|
+
triggerids[kind] = @manager.triggers.upsert_for_host(attributes.merge(dependencies: dependencies))
|
|
238
|
+
end
|
|
239
|
+
lifecycle.retire(existing, desired_keys)
|
|
240
|
+
plan.slice(:hostid, :host, :interface, :line_id).merge(itemids: itemids, triggerids: triggerids)
|
|
241
|
+
end
|
|
673
242
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
243
|
+
def resolve_host(reference, cache)
|
|
244
|
+
host = cache[[:host, reference]] ||= @manager.hosts.resolve(reference)
|
|
245
|
+
reference.is_a?(Hash) ? host.merge(hostid: reference[:hostid]) : host
|
|
246
|
+
end
|
|
678
247
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
248
|
+
def failure_result(kind, identity, error, writing: false)
|
|
249
|
+
unknown = error.is_a?(ResultUnknown) || (writing && error.is_a?(TransportError))
|
|
250
|
+
{
|
|
251
|
+
status: unknown ? :unknown : :error,
|
|
252
|
+
kind => identity,
|
|
253
|
+
error: { class: error.class.name, message: LogSanitizer.sanitize(error.message) }
|
|
254
|
+
}
|
|
255
|
+
end
|
|
685
256
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
257
|
+
def batch_summary(results)
|
|
258
|
+
results.transform_values do |entries|
|
|
259
|
+
statuses = entries.map { |entry| entry[:status] }.tally
|
|
260
|
+
{ total: entries.length, succeeded: statuses.fetch(:ok, 0),
|
|
261
|
+
failed: statuses.fetch(:error, 0), unknown: statuses.fetch(:unknown, 0) }
|
|
689
262
|
end
|
|
263
|
+
end
|
|
690
264
|
end
|
|
691
265
|
end
|