zabbix_manager 6.0.1 → 6.0.3
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 +13 -0
- data/README.md +4 -4
- data/lib/zabbix_manager/classes/applications.rb +1 -1
- data/lib/zabbix_manager/classes/graphs.rb +1 -1
- data/lib/zabbix_manager/classes/host_groups.rb +34 -15
- data/lib/zabbix_manager/classes/host_interfaces.rb +7 -2
- data/lib/zabbix_manager/classes/hosts.rb +4 -2
- data/lib/zabbix_manager/classes/http_tests.rb +1 -1
- data/lib/zabbix_manager/classes/items.rb +8 -8
- data/lib/zabbix_manager/classes/roles.rb +1 -1
- data/lib/zabbix_manager/classes/triggers.rb +54 -11
- data/lib/zabbix_manager/classes/user_macros.rb +32 -8
- data/lib/zabbix_manager/classes/users.rb +4 -2
- data/lib/zabbix_manager/client.rb +11 -4
- data/lib/zabbix_manager/http_transport.rb +2 -2
- data/lib/zabbix_manager/monitoring/thresholds.rb +4 -0
- data/lib/zabbix_manager/monitoring.rb +16 -3
- data/lib/zabbix_manager/resource.rb +8 -1
- data/lib/zabbix_manager/version.rb +1 -1
- data/zabbix_manager.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7bdb7262133718703b6cdd1a245914b33a3efa6d989b6a22877576ad18c83302
|
|
4
|
+
data.tar.gz: c5786d77070a1d5da5d34afc488feb21c0b7d1a2f7d91cdb808bdcb1c78469ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abb93fb5caaabef8b9e5ddc44c9c8d7aeddfa872490c7aedfb952d985395e57ef985d1486c1ae351786a14844c7b4c01cc783fcd98b9ab889aea7ab3ff96a5b6
|
|
7
|
+
data.tar.gz: 600d3d5797909d4ce7c453536336700303a9b3bea45e7b4b8ca1538b5125c5a6df08db176de233c8dc457efb60357fc83d10c32af2a9c604aed74db758025ccf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 6.0.3 (2026-10-11)
|
|
4
|
+
|
|
5
|
+
* Enable HTTPS certificate verification by default; use `ca_file:` for private certificate authorities.
|
|
6
|
+
* Freeze the negotiated API version and parse its major component once so authentication cannot drift between requests.
|
|
7
|
+
* Canonicalize resource IDs before queries, writes, receipt checks and upsert locking; deduplicate equivalent user targets.
|
|
8
|
+
* Snapshot interface monitoring definitions and serialize their execution with other monitoring workflows.
|
|
9
|
+
* Verify managed trigger identities and ownership before updates, including interface preflight before item writes; reject contradictory line tags during local validation.
|
|
10
|
+
* Validate complete group lookup responses before creating missing groups and verify host/global macro lookup identities.
|
|
11
|
+
|
|
12
|
+
## 6.0.2 (2026-10-05)
|
|
13
|
+
|
|
14
|
+
* Use the repository account name for Gem author metadata.
|
|
15
|
+
|
|
3
16
|
## 6.0.1 (2026-10-05)
|
|
4
17
|
|
|
5
18
|
* Discover batch item keys once per host and validate returned ownership, identities and duplicate targets before writes.
|
data/README.md
CHANGED
|
@@ -93,7 +93,7 @@ Logging uses `ActiveSupport::Logger` and `ActiveSupport::TaggedLogging`; structu
|
|
|
93
93
|
|
|
94
94
|
Passwords, API tokens, SNMPv3 authentication/privacy passphrases, authorization values, cookies, and session IDs are filtered. Quoted diagnostic credentials are filtered as complete values, including escaped quotes and backslashes. Request parameters and response bodies are not logged. API exceptions expose the server error code and request ID, without remote messages or data that might echo arbitrary secrets. Malformed or mismatched JSON-RPC responses and invalid mutation ID receipts raise `ProtocolError < TransportError`: a write may already have happened, so it must not be blindly replayed.
|
|
95
95
|
|
|
96
|
-
HTTPS certificate verification is
|
|
96
|
+
HTTPS certificate verification is enabled by default. Use `ca_file:` for a private certificate authority. An explicit `verify_ssl: false` disables verification and emits a warning when logging is enabled.
|
|
97
97
|
|
|
98
98
|
Zabbix 7 API-token requests need the `Authorization` header, so they cannot share that header with HTTP Basic authentication. The client rejects that combination instead of silently overwriting either credential.
|
|
99
99
|
|
|
@@ -123,7 +123,7 @@ zabbix.monitoring.reconcile_line(
|
|
|
123
123
|
)
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
The lookup accepts full and abbreviated interface names such as `Ten-GigabitEthernet1/0/49` and `Te1/0/49`. It refuses zero or multiple direction matches instead of selecting an item by response order. Trigger ownership
|
|
126
|
+
The lookup accepts full and abbreviated interface names such as `Ten-GigabitEthernet1/0/49` and `Te1/0/49`. It refuses zero or multiple direction matches instead of selecting an item by response order. Trigger ownership requires the requested managed identity and a unique matching `managed_by` tag. Contradictory `line_id` tags are rejected. Interface workflows check ownership before item writes, and trigger upserts check it again before updates; matching descriptions alone never adopt unrelated triggers.
|
|
127
127
|
|
|
128
128
|
Use `reconcile_lines(lines)` for imports. It reuses host and item discovery results within the batch, avoiding a full `item.get` scan for every line.
|
|
129
129
|
|
|
@@ -232,7 +232,7 @@ Invalid caller input raises `ZabbixManager::Invalid`, ambiguous remote ownership
|
|
|
232
232
|
|
|
233
233
|
`traffic.series` reads explicit item IDs; `traffic.for_interface` discovers both interface directions. Both expose missing data and query truncation, preserve numeric precision, and keep raw history separate from hourly trends. They never substitute zero or a stale last value for missing samples.
|
|
234
234
|
|
|
235
|
-
Device and
|
|
235
|
+
Device, line and interface definitions are copied before validation and discovery. Changes to caller-owned strings or nested fields during reconciliation do not alter the validated definition.
|
|
236
236
|
|
|
237
237
|
Multiword resource accessors use Ruby snake_case: `host_groups`, `host_interfaces`, `http_tests`, `media_types`, `proxy_groups`, `user_groups`, `user_macros`, `value_maps`, and `discovery_rules`.
|
|
238
238
|
|
|
@@ -248,7 +248,7 @@ The focused modules expose explicit operations:
|
|
|
248
248
|
|
|
249
249
|
* `triggers.for_host`, `triggers.upsert_for_host`, `triggers.add_dependencies`, `triggers.replace_dependencies`, `triggers.set_status`, `triggers.delete_many`
|
|
250
250
|
|
|
251
|
-
`items.upsert_many` discovers existing keys once per host before any write, then preserves input write order. Item and interface batch definitions are copied before discovery, including nested strings. Single-host helpers require a positive integer `hostid` or its decimal string representation. Identity lookup and batch helpers reject malformed query containers or required IDs with `ProtocolError`; `get_raw` preserves native response pass-through.
|
|
251
|
+
`items.upsert_many` discovers existing keys once per host before any write, then preserves input write order. Item and interface batch definitions are copied before discovery, including nested strings. Single-host helpers require a positive integer `hostid` or its decimal string representation. Equivalent resource IDs are normalized before locking, queries, writes and receipt checks. Identity lookup and batch helpers reject malformed query containers or required IDs with `ProtocolError`; `get_raw` preserves native response pass-through.
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
## Supported Ruby Versions
|
|
@@ -15,7 +15,7 @@ class ZabbixManager
|
|
|
15
15
|
# @return [Hash] 应用集唯一查询条件
|
|
16
16
|
def identity_filter(data)
|
|
17
17
|
attributes = data.deep_symbolize_keys
|
|
18
|
-
{ name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
|
|
18
|
+
{ name: attributes.fetch(:name), hostid: normalized_id_attribute(attributes.fetch(:hostid), "hostid") }
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -76,7 +76,7 @@ class ZabbixManager
|
|
|
76
76
|
# @return [Hash] 图形唯一查询条件
|
|
77
77
|
def identity_filter(data)
|
|
78
78
|
attributes = data.deep_symbolize_keys
|
|
79
|
-
{ name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
|
|
79
|
+
{ name: attributes.fetch(:name), hostid: normalized_id_attribute(attributes.fetch(:hostid), "hostid") }
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
# 图形所有者由 gitems 确定,hostid 仅用于调用方的身份查询。
|
|
@@ -19,41 +19,60 @@ class ZabbixManager
|
|
|
19
19
|
|
|
20
20
|
# 批量解析主机群组名称并返回对应的群组 ID。
|
|
21
21
|
# @param data [Array<String>, String] 待查询的群组名称
|
|
22
|
+
# @raise [ProtocolError] 远端群组身份或 ID 无效
|
|
23
|
+
# @raise [Conflict] 远端群组名称不唯一
|
|
22
24
|
# @return [Array<Hash>, nil] 群组 ID 列表,未命中时返回 nil
|
|
23
25
|
def get_hostgroup_ids(data)
|
|
24
26
|
names = normalized_names(data)
|
|
25
27
|
return nil if names.empty?
|
|
26
28
|
|
|
27
|
-
groups =
|
|
28
|
-
|
|
29
|
-
params: {
|
|
30
|
-
output: %w[groupid name],
|
|
31
|
-
filter: { name: names }
|
|
32
|
-
}
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
groups.empty? ? nil : groups.map { |group| { groupid: group.fetch("groupid") } }
|
|
29
|
+
groups = groups_by_name(names)
|
|
30
|
+
groups.empty? ? nil : groups.values.map { |id| { groupid: id } }
|
|
36
31
|
end
|
|
37
32
|
|
|
38
33
|
# 批量查询并创建缺失的主机群组,避免逐名称重复查询。
|
|
39
34
|
# @param data [Array<String>, String] 待确保存在的群组名称
|
|
35
|
+
# @raise [ProtocolError] 远端群组身份或 ID 无效
|
|
36
|
+
# @raise [Conflict] 远端群组名称不唯一
|
|
40
37
|
# @return [Array<Hash>] 群组 ID 列表
|
|
41
38
|
def get_or_create_host_groups(data)
|
|
42
39
|
names = normalized_names(data)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
).index_by { |group| group.fetch("name") }
|
|
40
|
+
return [] if names.empty?
|
|
41
|
+
|
|
42
|
+
existing = groups_by_name(names)
|
|
47
43
|
|
|
48
44
|
names.map do |name|
|
|
49
|
-
|
|
50
|
-
next({ groupid:
|
|
45
|
+
id = existing[name]
|
|
46
|
+
next({ groupid: id }) if id
|
|
51
47
|
|
|
52
48
|
result = @client.api_request(method: "hostgroup.create", params: { name: name })
|
|
53
49
|
{ groupid: response_id(result).to_s }
|
|
54
50
|
end
|
|
55
51
|
end
|
|
56
52
|
|
|
53
|
+
# 全部查询结果先核对身份和 ID,损坏或重名响应不能触发部分创建。
|
|
54
|
+
private def groups_by_name(names)
|
|
55
|
+
rows = response_objects(@client.api_request(
|
|
56
|
+
method: "hostgroup.get",
|
|
57
|
+
params: { output: %w[groupid name], filter: { name: names } }
|
|
58
|
+
))
|
|
59
|
+
requested = names.to_h { |name| [name, true] }
|
|
60
|
+
ids = {}
|
|
61
|
+
rows.each_with_object({}) do |group, result|
|
|
62
|
+
name = group["name"]
|
|
63
|
+
unless name.is_a?(String) && requested.key?(name)
|
|
64
|
+
raise ProtocolError, "hostgroup.get returned a group outside the requested names"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
id = response_identifier(group["groupid"]).to_s
|
|
68
|
+
raise Conflict, "multiple host groups share the same name" if result.key?(name)
|
|
69
|
+
raise ProtocolError, "hostgroup.get returned duplicate group IDs" if ids.key?(id)
|
|
70
|
+
|
|
71
|
+
ids[id] = true
|
|
72
|
+
result[name] = id
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
57
76
|
# 清理、去空并去重主机群组名称。
|
|
58
77
|
# @param data [Array<String>, String] 原始群组名称
|
|
59
78
|
# @return [Array<String>] 规范化后的群组名称
|
|
@@ -24,7 +24,7 @@ class ZabbixManager
|
|
|
24
24
|
# 查询指定主机的全部接口,可选择同时返回关联监控项。
|
|
25
25
|
# @return [Array<Hash>]
|
|
26
26
|
def for_host(hostid, select_items: nil)
|
|
27
|
-
|
|
27
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
28
28
|
|
|
29
29
|
params = { hostids: hostid, output: "extend" }
|
|
30
30
|
params[:selectItems] = select_items if select_items
|
|
@@ -56,6 +56,7 @@ class ZabbixManager
|
|
|
56
56
|
# 幂等创建或更新一个主机的接口集合。
|
|
57
57
|
# @return [Array<Integer>]
|
|
58
58
|
def reconcile_for_host(hostid:, interfaces:)
|
|
59
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
59
60
|
@client.with_upsert_lock("host-interfaces:#{hostid}") do
|
|
60
61
|
plan = plan_for_host(hostid: hostid, interfaces: interfaces)
|
|
61
62
|
apply_plan(hostid: hostid, plan: plan)
|
|
@@ -105,6 +106,9 @@ class ZabbixManager
|
|
|
105
106
|
raise Invalid, "interface must be a hash" unless interface.is_a?(Hash)
|
|
106
107
|
|
|
107
108
|
attributes = interface.deep_symbolize_keys.deep_dup
|
|
109
|
+
if attributes.key?(:interfaceid)
|
|
110
|
+
attributes[:interfaceid] = normalized_id_attribute(attributes[:interfaceid], "interfaceid")
|
|
111
|
+
end
|
|
108
112
|
if attributes.key?(:type)
|
|
109
113
|
type = integer_attribute(attributes[:type], "interface type")
|
|
110
114
|
raise Invalid, "interface type must be 1, 2, 3, or 4" unless [1, 2, 3, 4].include?(type)
|
|
@@ -232,7 +236,8 @@ class ZabbixManager
|
|
|
232
236
|
endpoint_field = values[:useip].to_i == 1 ? :ip : :dns
|
|
233
237
|
raise Invalid, "interface #{endpoint_field} is required" if values[endpoint_field].blank?
|
|
234
238
|
|
|
235
|
-
"endpoint:#{values[:type]}
|
|
239
|
+
"endpoint:#{integer_attribute(values[:type], 'interface type')}:" \
|
|
240
|
+
"#{integer_attribute(values[:useip], 'interface useip')}:#{values[endpoint_field]}:#{values[:port]}"
|
|
236
241
|
end
|
|
237
242
|
|
|
238
243
|
# 更新已属于目标主机的接口。
|
|
@@ -23,10 +23,12 @@ class ZabbixManager
|
|
|
23
23
|
|
|
24
24
|
# 按主机 ID 查询完整主机及接口、群组和模板。
|
|
25
25
|
def dump_by_id(data)
|
|
26
|
+
attributes = data.deep_symbolize_keys
|
|
27
|
+
id = normalized_id_attribute(attributes[key.to_sym], key)
|
|
26
28
|
@client.api_request(
|
|
27
29
|
method: "host.get",
|
|
28
30
|
params: {
|
|
29
|
-
filter: { key.to_sym =>
|
|
31
|
+
filter: { key.to_sym => id },
|
|
30
32
|
output: "extend",
|
|
31
33
|
selectGroups: "extend",
|
|
32
34
|
selectInterfaces: "extend",
|
|
@@ -145,7 +147,7 @@ class ZabbixManager
|
|
|
145
147
|
# 按主机 ID 查询唯一主机,并返回表达式需要的技术名称。
|
|
146
148
|
# @return [Hash, nil]
|
|
147
149
|
def find_by_id(hostid)
|
|
148
|
-
|
|
150
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
149
151
|
find_host_by_filter(hostid: hostid)
|
|
150
152
|
end
|
|
151
153
|
|
|
@@ -26,7 +26,7 @@ class ZabbixManager
|
|
|
26
26
|
# @return [Hash] Web 场景唯一查询条件
|
|
27
27
|
def identity_filter(data)
|
|
28
28
|
attributes = data.deep_symbolize_keys
|
|
29
|
-
{ name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
|
|
29
|
+
{ name: attributes.fetch(:name), hostid: normalized_id_attribute(attributes.fetch(:hostid), "hostid") }
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -35,13 +35,13 @@ class ZabbixManager
|
|
|
35
35
|
# @return [Hash] 名称和所属主机组成的精确过滤条件
|
|
36
36
|
def identity_filter(data)
|
|
37
37
|
attributes = data.deep_symbolize_keys
|
|
38
|
-
{ name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
|
|
38
|
+
{ name: attributes.fetch(:name), hostid: normalized_id_attribute(attributes.fetch(:hostid), "hostid") }
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# 查询主机监控项,可按稳定 key 集合过滤并选择关联对象。
|
|
42
42
|
# @return [Array<Hash>]
|
|
43
43
|
def for_host(hostid, keys: nil, output: "extend", select_preprocessing: nil, select_tags: nil)
|
|
44
|
-
|
|
44
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
45
45
|
|
|
46
46
|
params = { hostids: hostid, output: output }
|
|
47
47
|
params[:filter] = { key_: keys } if keys.present?
|
|
@@ -68,13 +68,13 @@ class ZabbixManager
|
|
|
68
68
|
# @return [Array<Integer>]
|
|
69
69
|
def upsert_many(collection)
|
|
70
70
|
items = Array(collection).map { |data| validate_item!(data) }
|
|
71
|
-
identities = items.map { |item| [
|
|
71
|
+
identities = items.map { |item| [item[:hostid].to_s, item[:key_]] }
|
|
72
72
|
duplicate = identities.tally.find { |_identity, count| count > 1 }&.first
|
|
73
73
|
raise Invalid, "duplicate hostid + key_ identity #{duplicate.join(":")}" if duplicate
|
|
74
74
|
|
|
75
75
|
existing = batch_items(items)
|
|
76
76
|
plans = items.map do |item|
|
|
77
|
-
identity = [
|
|
77
|
+
identity = [item[:hostid].to_s, item[:key_]]
|
|
78
78
|
plan_item(item, existing[identity])
|
|
79
79
|
end
|
|
80
80
|
if block_given?
|
|
@@ -116,7 +116,7 @@ class ZabbixManager
|
|
|
116
116
|
# 方向和接口精确匹配由 Monitoring 业务层负责。
|
|
117
117
|
# @return [Array<Hash>]
|
|
118
118
|
def monitored_traffic_candidates(hostid)
|
|
119
|
-
|
|
119
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
120
120
|
@client.api_request(
|
|
121
121
|
method: "item.get",
|
|
122
122
|
params: {
|
|
@@ -155,7 +155,7 @@ class ZabbixManager
|
|
|
155
155
|
|
|
156
156
|
# 每个主机只读取一次期望 key 集合,全部发现和校验完成后才开始写入。
|
|
157
157
|
def batch_items(items)
|
|
158
|
-
groups = items.group_by { |item|
|
|
158
|
+
groups = items.group_by { |item| item[:hostid].to_s }
|
|
159
159
|
ids = {}
|
|
160
160
|
groups.each_with_object({}) do |(owner, definitions), result|
|
|
161
161
|
hostid = definitions.first[:hostid]
|
|
@@ -218,7 +218,7 @@ class ZabbixManager
|
|
|
218
218
|
raise Invalid, "item must be a hash" unless data.is_a?(Hash)
|
|
219
219
|
|
|
220
220
|
attributes = data.deep_symbolize_keys.deep_dup
|
|
221
|
-
|
|
221
|
+
attributes[:hostid] = normalized_id_attribute(attributes[:hostid], "hostid")
|
|
222
222
|
%i[key_ name].each do |field|
|
|
223
223
|
unless attributes[field].is_a?(String) && attributes[field].present?
|
|
224
224
|
raise Invalid, "#{field} must be a non-empty string"
|
|
@@ -259,7 +259,7 @@ class ZabbixManager
|
|
|
259
259
|
|
|
260
260
|
# 回查监控项归属,阻止共享高权限令牌跨主机修改。
|
|
261
261
|
def verify_host_ownership!(hostid, ids)
|
|
262
|
-
|
|
262
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
263
263
|
|
|
264
264
|
result = @client.api_request(
|
|
265
265
|
method: "item.get", params: { hostids: hostid, itemids: ids, output: ["itemid"] }
|
|
@@ -21,7 +21,7 @@ class ZabbixManager
|
|
|
21
21
|
raise Invalid, "roleid is required" if attributes[:roleid].blank?
|
|
22
22
|
raise Invalid, "rules is required" if attributes[:rules].blank?
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
attributes[:roleid] = normalized_id_attribute(attributes[:roleid], "roleid")
|
|
25
25
|
|
|
26
26
|
result = @client.api_request(
|
|
27
27
|
method: "role.update",
|
|
@@ -26,11 +26,13 @@ class ZabbixManager
|
|
|
26
26
|
# @raise [TransportError] HTTP 或网络请求失败,可能无法确认远端结果
|
|
27
27
|
# @return [Array<Hash>] 匹配的触发器及其关联对象
|
|
28
28
|
def dump_by_id(data)
|
|
29
|
+
attributes = data.deep_symbolize_keys
|
|
30
|
+
id = normalized_id_attribute(attributes[key.to_sym], key)
|
|
29
31
|
@client.api_request(
|
|
30
32
|
method: "trigger.get",
|
|
31
33
|
params: {
|
|
32
34
|
filter: {
|
|
33
|
-
key.to_sym =>
|
|
35
|
+
key.to_sym => id
|
|
34
36
|
},
|
|
35
37
|
output: "extend",
|
|
36
38
|
selectItems: "extend",
|
|
@@ -43,7 +45,7 @@ class ZabbixManager
|
|
|
43
45
|
# @return [Array<Hash>]
|
|
44
46
|
def for_host(hostid, output: "extend", select_items: nil, select_functions: nil,
|
|
45
47
|
select_dependencies: nil, select_tags: nil, filter: nil)
|
|
46
|
-
|
|
48
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
47
49
|
|
|
48
50
|
params = { hostids: hostid, output: output }
|
|
49
51
|
params[:selectItems] = select_items if select_items
|
|
@@ -57,7 +59,7 @@ class ZabbixManager
|
|
|
57
59
|
# 按主机、描述和可选标签查询唯一触发器。
|
|
58
60
|
# @return [Hash, nil]
|
|
59
61
|
def find_for_host(hostid:, description:, tags: nil)
|
|
60
|
-
|
|
62
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
61
63
|
params = {
|
|
62
64
|
hostids: hostid,
|
|
63
65
|
filter: { description: description },
|
|
@@ -80,9 +82,13 @@ class ZabbixManager
|
|
|
80
82
|
# 展开表达式并读取依赖,供不确定写入后的完整期望状态核对。
|
|
81
83
|
# @param hostid [Integer, String] 触发器所属主机
|
|
82
84
|
# @param managed_key [String] 稳定、非秘密的受管身份
|
|
85
|
+
# @param managed_by [String, nil] 指定时必须与远端唯一管理方一致
|
|
86
|
+
# @param line_id [String, nil] 指定时拒绝矛盾的远端线路标签;兼容未标记的旧线路
|
|
87
|
+
# @raise [ProtocolError] 返回标签格式无效或受管身份不匹配
|
|
88
|
+
# @raise [Conflict] 受管身份不唯一或归属另一管理方、线路
|
|
83
89
|
# @return [Hash, nil]
|
|
84
|
-
def find_managed_for_host(hostid:, managed_key:)
|
|
85
|
-
|
|
90
|
+
def find_managed_for_host(hostid:, managed_key:, managed_by: nil, line_id: nil)
|
|
91
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
86
92
|
result = @client.api_request(
|
|
87
93
|
method: "trigger.get",
|
|
88
94
|
params: {
|
|
@@ -95,7 +101,10 @@ class ZabbixManager
|
|
|
95
101
|
}
|
|
96
102
|
)
|
|
97
103
|
response_objects(result)
|
|
98
|
-
result.each
|
|
104
|
+
result.each do |trigger|
|
|
105
|
+
response_identifier(trigger["triggerid"])
|
|
106
|
+
validate_managed_identity!(trigger, managed_key: managed_key, managed_by: managed_by, line_id: line_id)
|
|
107
|
+
end
|
|
99
108
|
raise Conflict, "multiple triggers use managed key #{managed_key}" if result.length > 1
|
|
100
109
|
|
|
101
110
|
result.first
|
|
@@ -123,7 +132,7 @@ class ZabbixManager
|
|
|
123
132
|
# @return [Integer]
|
|
124
133
|
def upsert_for_host(data)
|
|
125
134
|
attributes = data.deep_symbolize_keys.deep_dup
|
|
126
|
-
|
|
135
|
+
attributes[:hostid] = normalized_id_attribute(attributes[:hostid], "hostid")
|
|
127
136
|
lock_key = "trigger:#{attributes[:hostid]}:#{attributes[:managed_key] || attributes[:description]}"
|
|
128
137
|
@client.with_upsert_lock(lock_key) { perform_upsert_for_host(attributes) }
|
|
129
138
|
end
|
|
@@ -197,7 +206,7 @@ class ZabbixManager
|
|
|
197
206
|
attributes = data.deep_symbolize_keys
|
|
198
207
|
hostid = attributes.delete(:hostid)
|
|
199
208
|
managed_key = attributes.delete(:managed_key)&.to_s
|
|
200
|
-
|
|
209
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
201
210
|
raise Invalid, "description is required" if attributes[:description].blank?
|
|
202
211
|
raise Invalid, "expression is required" if attributes[:expression].blank?
|
|
203
212
|
|
|
@@ -208,7 +217,10 @@ class ZabbixManager
|
|
|
208
217
|
tag[:tag].to_s == "zabbix_manager_id" || tag["tag"].to_s == "zabbix_manager_id"
|
|
209
218
|
end
|
|
210
219
|
attributes[:tags] << { tag: "zabbix_manager_id", value: managed_key }
|
|
211
|
-
current = find_managed_for_host(
|
|
220
|
+
current = find_managed_for_host(
|
|
221
|
+
hostid: hostid, managed_key: managed_key,
|
|
222
|
+
managed_by: desired_tag_value(attributes, "managed_by"), line_id: desired_tag_value(attributes, "line_id")
|
|
223
|
+
)
|
|
212
224
|
else
|
|
213
225
|
ownership_tags = Array(attributes[:tags]).select do |tag|
|
|
214
226
|
tag[:tag].to_s == "managed_by" || tag["tag"].to_s == "managed_by"
|
|
@@ -250,7 +262,10 @@ class ZabbixManager
|
|
|
250
262
|
delays = @client.options.fetch(:uncertain_write_delays, DEFAULT_UNCERTAIN_WRITE_DELAYS)
|
|
251
263
|
delays.each do |delay|
|
|
252
264
|
sleep(delay) if delay.positive?
|
|
253
|
-
current = find_managed_for_host(
|
|
265
|
+
current = find_managed_for_host(
|
|
266
|
+
hostid: hostid, managed_key: managed_key,
|
|
267
|
+
managed_by: desired_tag_value(attributes, "managed_by"), line_id: desired_tag_value(attributes, "line_id")
|
|
268
|
+
)
|
|
254
269
|
if current && recovered_trigger_matches?(current, attributes)
|
|
255
270
|
response_identifier(current["triggerid"])
|
|
256
271
|
return current
|
|
@@ -261,6 +276,34 @@ class ZabbixManager
|
|
|
261
276
|
nil
|
|
262
277
|
end
|
|
263
278
|
|
|
279
|
+
# 原生标签过滤器不能代替本地身份和管理权校验。
|
|
280
|
+
private def validate_managed_identity!(trigger, managed_key:, managed_by:, line_id:)
|
|
281
|
+
tags = trigger["tags"]
|
|
282
|
+
unless tags.is_a?(Array) && tags.all? do |tag|
|
|
283
|
+
tag.is_a?(Hash) && tag["tag"].is_a?(String) && tag["value"].is_a?(String)
|
|
284
|
+
end
|
|
285
|
+
raise ProtocolError, "invalid trigger response: tags must contain string tag and value fields"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
values = ->(name) { tags.filter_map { |tag| tag["value"] if tag["tag"] == name } }
|
|
289
|
+
unless values.call("zabbix_manager_id") == [managed_key]
|
|
290
|
+
raise ProtocolError, "trigger.get returned an unexpected managed key"
|
|
291
|
+
end
|
|
292
|
+
if managed_by && values.call("managed_by") != [managed_by]
|
|
293
|
+
raise Conflict, "trigger managed key belongs to another owner"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
lines = values.call("line_id")
|
|
297
|
+
raise Conflict, "trigger managed key belongs to another line" if line_id && lines.any? && lines != [line_id]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
private def desired_tag_value(attributes, name)
|
|
301
|
+
values = Array(attributes[:tags]).filter_map { |tag| tag[:value] if tag[:tag] == name }
|
|
302
|
+
raise Invalid, "#{name} must have one string value" if values.length > 1 || values.any? { |v| !v.is_a?(String) }
|
|
303
|
+
|
|
304
|
+
values.first
|
|
305
|
+
end
|
|
306
|
+
|
|
264
307
|
private def recovered_trigger_matches?(current, attributes)
|
|
265
308
|
desired_tags = Array(attributes[:tags]).map(&:deep_symbolize_keys)
|
|
266
309
|
actual_tags = Array(current["tags"]).map(&:deep_symbolize_keys)
|
|
@@ -281,7 +324,7 @@ class ZabbixManager
|
|
|
281
324
|
|
|
282
325
|
# 回查触发器归属,阻止共享高权限令牌跨主机修改。
|
|
283
326
|
private def verify_host_ownership!(hostid, ids)
|
|
284
|
-
|
|
327
|
+
hostid = normalized_id_attribute(hostid, "hostid")
|
|
285
328
|
|
|
286
329
|
result = @client.api_request(
|
|
287
330
|
method: "trigger.get", params: { hostids: hostid, triggerids: ids, output: ["triggerid"] }
|
|
@@ -21,26 +21,33 @@ class ZabbixManager
|
|
|
21
21
|
# @return [Hash] 主机宏复合身份
|
|
22
22
|
def identity_filter(data)
|
|
23
23
|
attributes = data.deep_symbolize_keys
|
|
24
|
-
{ macro: attributes.fetch(:macro), hostid: attributes.fetch(:hostid) }
|
|
24
|
+
{ macro: attributes.fetch(:macro), hostid: normalized_id_attribute(attributes.fetch(:hostid), "hostid") }
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# @raise [ProtocolError] 远端宏名称、归属或 ID 无效
|
|
28
|
+
# @raise [Conflict] 宏身份不唯一
|
|
27
29
|
# @return [Integer, nil] 唯一主机宏 ID
|
|
28
30
|
def get_id(data)
|
|
29
31
|
attributes = identity_filter(data)
|
|
30
|
-
unique_macro_id(get_full_data(attributes), "hostmacroid")
|
|
32
|
+
unique_macro_id(get_full_data(attributes), "hostmacroid", attributes)
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
# @raise [ProtocolError] 远端宏名称或 ID 无效
|
|
36
|
+
# @raise [Conflict] 宏身份不唯一
|
|
33
37
|
# @return [Integer, nil] 唯一全局宏 ID
|
|
34
38
|
def get_id_global(data)
|
|
35
39
|
attributes = data.deep_symbolize_keys
|
|
36
|
-
|
|
40
|
+
identity = { macro: attributes.fetch(:macro) }
|
|
41
|
+
unique_macro_id(get_full_data_global(identity), "globalmacroid", identity)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
# @return [Array<Hash>] 主机宏查询结果
|
|
40
45
|
def get_full_data(data)
|
|
41
46
|
attributes = data.deep_symbolize_keys
|
|
42
47
|
params = { filter: attributes.except(:hostid) }
|
|
43
|
-
|
|
48
|
+
if attributes.key?(:hostid)
|
|
49
|
+
params[:hostids] = normalized_id_attribute(attributes[:hostid], "hostid")
|
|
50
|
+
end
|
|
44
51
|
get_raw(params)
|
|
45
52
|
end
|
|
46
53
|
|
|
@@ -73,12 +80,12 @@ class ZabbixManager
|
|
|
73
80
|
|
|
74
81
|
# @return [Integer, nil] 更新主机宏 ID
|
|
75
82
|
def update(data)
|
|
76
|
-
|
|
83
|
+
update_macro("update", data, "hostmacroid")
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
# @return [Integer, nil] 更新全局宏 ID
|
|
80
87
|
def update_global(data)
|
|
81
|
-
|
|
88
|
+
update_macro("updateglobal", data, "globalmacroid")
|
|
82
89
|
end
|
|
83
90
|
|
|
84
91
|
# @return [Integer, nil] 已存在或新建全局宏 ID
|
|
@@ -95,8 +102,25 @@ class ZabbixManager
|
|
|
95
102
|
|
|
96
103
|
private
|
|
97
104
|
|
|
98
|
-
def
|
|
99
|
-
|
|
105
|
+
def update_macro(operation, data, id_field)
|
|
106
|
+
attributes = data.deep_symbolize_keys
|
|
107
|
+
id = attributes[id_field.to_sym] = normalized_id_attribute(attributes[id_field.to_sym], id_field)
|
|
108
|
+
write_macro(operation, attributes, "#{id_field}s", expected: [id])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 名称过滤器和 hostids 只是远端查询条件,不能代替本地身份核对。
|
|
112
|
+
def unique_macro_id(result, id_field, identity)
|
|
113
|
+
response_objects(result).each do |macro|
|
|
114
|
+
response_identifier(macro[id_field], id_field)
|
|
115
|
+
unless macro["macro"].is_a?(String) && macro["macro"] == identity[:macro]
|
|
116
|
+
raise ProtocolError, "usermacro.get returned an unexpected macro identity"
|
|
117
|
+
end
|
|
118
|
+
next unless identity.key?(:hostid)
|
|
119
|
+
|
|
120
|
+
unless response_identifier(macro["hostid"], "hostid") == identity[:hostid].to_i
|
|
121
|
+
raise ProtocolError, "usermacro.get returned a macro outside the requested host"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
100
124
|
raise Conflict, "multiple macros match the requested identity" if result.length > 1
|
|
101
125
|
|
|
102
126
|
response_identifier(result.first[id_field], id_field) if result.first
|
|
@@ -18,11 +18,13 @@ class ZabbixManager
|
|
|
18
18
|
# @return [Integer, nil] 首个更新用户的 ID
|
|
19
19
|
def update_medias(data)
|
|
20
20
|
attributes = data.deep_symbolize_keys
|
|
21
|
-
ids =
|
|
21
|
+
ids = Array.wrap(attributes.fetch(:userids)).map { |id| normalized_id_attribute(id, "userid") }
|
|
22
|
+
normalized_ids(ids)
|
|
23
|
+
ids.uniq!(&:to_s)
|
|
22
24
|
media_field = Gem::Version.new(@client.api_version) >= Gem::Version.new("5.4") ? :medias : :user_medias
|
|
23
25
|
result = @client.api_request(
|
|
24
26
|
method: "user.update",
|
|
25
|
-
params:
|
|
27
|
+
params: ids.map do |userid|
|
|
26
28
|
{ userid: userid, media_field => attributes.fetch(:media) }
|
|
27
29
|
end
|
|
28
30
|
)
|
|
@@ -16,7 +16,10 @@ class ZabbixManager
|
|
|
16
16
|
error: Logger::ERROR, fatal: Logger::FATAL, unknown: Logger::UNKNOWN }.freeze
|
|
17
17
|
private_constant :LOG_LEVELS
|
|
18
18
|
|
|
19
|
-
attr_reader :options, :logger
|
|
19
|
+
attr_reader :options, :logger
|
|
20
|
+
|
|
21
|
+
# @return [String] 不可变的已协商版本,认证策略在连接期间保持稳定
|
|
22
|
+
attr_reader :api_version
|
|
20
23
|
|
|
21
24
|
# 校验配置后立即查询服务器版本并完成认证。
|
|
22
25
|
# 初始化失败时关闭连接;成功后的客户端生命周期由调用方负责。
|
|
@@ -199,7 +202,11 @@ class ZabbixManager
|
|
|
199
202
|
def validate_api_version!
|
|
200
203
|
match = @api_version.is_a?(String) && @api_version.match(/\A(\d+)\.(\d+)\.(\d+)\z/)
|
|
201
204
|
raise ProtocolError, "Invalid API version response" unless match
|
|
202
|
-
|
|
205
|
+
|
|
206
|
+
# 版本只在初始化时解析,后续请求不重复拆分字符串或改变认证策略。
|
|
207
|
+
@api_version = @api_version.dup.freeze
|
|
208
|
+
@api_major_version = match[1].to_i
|
|
209
|
+
return if SUPPORTED_MAJOR_VERSIONS.cover?(@api_major_version)
|
|
203
210
|
raise ApiError, "Zabbix API version is not supported" unless @settings[:ignore_version]
|
|
204
211
|
|
|
205
212
|
log(:warn, "version.unsupported", version: @api_version)
|
|
@@ -222,11 +229,11 @@ class ZabbixManager
|
|
|
222
229
|
end
|
|
223
230
|
|
|
224
231
|
def insecure_https?
|
|
225
|
-
@transport.uri.scheme == "https" && @settings[:verify_ssl]
|
|
232
|
+
@transport.uri.scheme == "https" && @settings[:verify_ssl] == false
|
|
226
233
|
end
|
|
227
234
|
|
|
228
235
|
def api_major_version
|
|
229
|
-
@
|
|
236
|
+
@api_major_version
|
|
230
237
|
end
|
|
231
238
|
|
|
232
239
|
def bearer_token_for(method)
|
|
@@ -203,12 +203,12 @@ class ZabbixManager
|
|
|
203
203
|
]
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
-
# 配置 HTTPS
|
|
206
|
+
# 配置 HTTPS;证书校验默认开启,仅在显式传入 false 时关闭。
|
|
207
207
|
def configure_tls(http)
|
|
208
208
|
return unless @uri.scheme == "https"
|
|
209
209
|
|
|
210
210
|
http.use_ssl = true
|
|
211
|
-
http.verify_mode = @options[:verify_ssl] ==
|
|
211
|
+
http.verify_mode = @options[:verify_ssl] == false ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
|
212
212
|
http.ca_file = @options[:ca_file] if @options[:ca_file]
|
|
213
213
|
end
|
|
214
214
|
|
|
@@ -166,6 +166,10 @@ class ZabbixManager
|
|
|
166
166
|
|
|
167
167
|
{ tag: tag[:tag], value: tag.fetch(:value, "") }
|
|
168
168
|
end
|
|
169
|
+
# line_id 参与资源层的归属校验,矛盾值必须在首个远端写入之前拒绝。
|
|
170
|
+
line_ids = extra.filter_map { |tag| tag[:value] if tag[:tag] == "line_id" }.uniq
|
|
171
|
+
raise Invalid, "line_id must have one string value" if line_ids.length > 1
|
|
172
|
+
|
|
169
173
|
(tags + extra).uniq
|
|
170
174
|
end
|
|
171
175
|
|
|
@@ -121,12 +121,19 @@ class ZabbixManager
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
# 接口更新只增改,不自动删除远端对象;所有本地校验在首个写入之前完成。
|
|
124
|
+
# 快照调用方输入,再与设备和线路工作流共享实例互斥锁。
|
|
124
125
|
# @param attributes [Hash] host、interface、按名称分组的原生 items 和 thresholds
|
|
125
126
|
# @return [Hash] hostid、interface、itemids 和 triggerids
|
|
126
127
|
# @raise [Invalid, Conflict] 输入、量纲或资源归属无效
|
|
127
128
|
# @raise [ApiError, TransportError] 远端失败;已经确认的早期步骤不会被回滚
|
|
128
129
|
def reconcile_interface(attributes)
|
|
129
|
-
input = Validation.hash!(attributes, "interface monitoring")
|
|
130
|
+
input = Validation.hash!(attributes, "interface monitoring").deep_dup
|
|
131
|
+
@workflow_mutex.synchronize { apply_interface(input) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def apply_interface(input)
|
|
130
137
|
input.assert_valid_keys(:host, :interface, :items, :thresholds)
|
|
131
138
|
supplied_host = Validation.hash!(input[:host], "host")
|
|
132
139
|
Validation.host!(supplied_host)
|
|
@@ -140,6 +147,14 @@ class ZabbixManager
|
|
|
140
147
|
validate_items: false
|
|
141
148
|
)
|
|
142
149
|
host = resolve_host(supplied_host, {})
|
|
150
|
+
triggers.each_value do |trigger|
|
|
151
|
+
ownership = { managed_by: "zabbix_manager" }
|
|
152
|
+
line_tag = trigger[:tags].find { |tag| tag[:tag] == "line_id" }
|
|
153
|
+
ownership[:line_id] = line_tag[:value] if line_tag
|
|
154
|
+
@manager.triggers.find_managed_for_host(
|
|
155
|
+
hostid: host[:hostid], managed_key: trigger[:managed_key], **ownership
|
|
156
|
+
)
|
|
157
|
+
end
|
|
143
158
|
ids = @manager.items.upsert_many(items.values) do |effective|
|
|
144
159
|
# 读取已有配置后再次校验最终量纲和数值类型,仍在首个 item 写入之前。
|
|
145
160
|
triggers = @thresholds.prepare_triggers(
|
|
@@ -152,8 +167,6 @@ class ZabbixManager
|
|
|
152
167
|
{ hostid: host[:hostid].to_i, interface: interface[:name], itemids: itemids, triggerids: triggerids }
|
|
153
168
|
end
|
|
154
169
|
|
|
155
|
-
private
|
|
156
|
-
|
|
157
170
|
def validate_devices!(devices)
|
|
158
171
|
definitions = Validation.array!(devices, "devices").map { |device| Device.new(device) }
|
|
159
172
|
validate_unique!(definitions.map(&:identity), "device host")
|
|
@@ -71,7 +71,7 @@ class ZabbixManager
|
|
|
71
71
|
id = attributes[key.to_sym]
|
|
72
72
|
raise Invalid, "#{key} is required" if id.blank?
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
id = attributes[key.to_sym] = normalized_id_attribute(id, key)
|
|
75
75
|
|
|
76
76
|
current = response_objects(dump_by_id(key.to_sym => id)).find do |item|
|
|
77
77
|
response_identifier(item[key]) == id.to_i
|
|
@@ -96,6 +96,7 @@ class ZabbixManager
|
|
|
96
96
|
id = attributes[key.to_sym]
|
|
97
97
|
raise Invalid, "#{key} is required" if id.blank?
|
|
98
98
|
|
|
99
|
+
id = normalized_id_attribute(id, key)
|
|
99
100
|
response_objects(get_raw(filter: { key.to_sym => id }, output: "extend"))
|
|
100
101
|
end
|
|
101
102
|
|
|
@@ -190,6 +191,12 @@ class ZabbixManager
|
|
|
190
191
|
id
|
|
191
192
|
end
|
|
192
193
|
|
|
194
|
+
# 同一 ID 的正号和前导零只在输入边界出现,查询、写入和锁使用统一身份。
|
|
195
|
+
def normalized_id_attribute(value, name)
|
|
196
|
+
id = positive_id_attribute(value, name)
|
|
197
|
+
value.is_a?(Integer) ? id : id.to_s
|
|
198
|
+
end
|
|
199
|
+
|
|
193
200
|
def validate_boolean!(value, name)
|
|
194
201
|
raise Invalid, "#{name} must be true or false" unless value == true || value == false
|
|
195
202
|
end
|
data/zabbix_manager.gemspec
CHANGED
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
|
|
12
12
|
spec.name = "zabbix_manager"
|
|
13
13
|
spec.version = ZabbixManager::VERSION
|
|
14
|
-
spec.authors = ["
|
|
14
|
+
spec.authors = ["gatework"]
|
|
15
15
|
spec.email = ["careline@foxmail.com"]
|
|
16
16
|
|
|
17
17
|
spec.summary = "Ruby client and monitoring workflows for the Zabbix 4.0 through 7.x API"
|
metadata
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zabbix_manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0.
|
|
4
|
+
version: 6.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- gatework
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|