zabbix_manager 5.1.4 → 5.1.6
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 +25 -9
- data/LICENSE +1 -1
- data/README.md +202 -8
- data/lib/zabbix_manager/basic/basic_alias.rb +25 -3
- data/lib/zabbix_manager/basic/basic_func.rb +32 -61
- data/lib/zabbix_manager/basic/basic_init.rb +28 -13
- data/lib/zabbix_manager/basic/basic_logic.rb +172 -128
- data/lib/zabbix_manager/classes/actions.rb +21 -11
- data/lib/zabbix_manager/classes/applications.rb +12 -17
- data/lib/zabbix_manager/classes/configurations.rb +20 -5
- data/lib/zabbix_manager/classes/drules.rb +11 -23
- data/lib/zabbix_manager/classes/errors.rb +16 -28
- data/lib/zabbix_manager/classes/events.rb +6 -2
- data/lib/zabbix_manager/classes/graphs.rb +41 -34
- data/lib/zabbix_manager/classes/hostgroups.rb +53 -3
- data/lib/zabbix_manager/classes/hostinterfaces.rb +172 -12
- data/lib/zabbix_manager/classes/hosts.rb +197 -68
- data/lib/zabbix_manager/classes/httptests.rb +18 -20
- data/lib/zabbix_manager/classes/items.rb +199 -72
- data/lib/zabbix_manager/classes/maintenance.rb +6 -2
- data/lib/zabbix_manager/classes/mediatypes.rb +7 -63
- data/lib/zabbix_manager/classes/problems.rb +75 -57
- data/lib/zabbix_manager/classes/proxies.rb +48 -7
- data/lib/zabbix_manager/classes/roles.rb +47 -36
- data/lib/zabbix_manager/classes/screens.rb +45 -14
- data/lib/zabbix_manager/classes/scripts.rb +17 -7
- data/lib/zabbix_manager/classes/server.rb +7 -1
- data/lib/zabbix_manager/classes/templates.rb +59 -21
- data/lib/zabbix_manager/classes/triggers.rb +253 -66
- data/lib/zabbix_manager/classes/usergroups.rb +28 -6
- data/lib/zabbix_manager/classes/usermacros.rb +119 -33
- data/lib/zabbix_manager/classes/users.rb +39 -7
- data/lib/zabbix_manager/classes/valuemaps.rb +13 -19
- data/lib/zabbix_manager/client.rb +311 -135
- data/lib/zabbix_manager/http_transport.rb +192 -0
- data/lib/zabbix_manager/log_sanitizer.rb +68 -0
- data/lib/zabbix_manager/monitoring.rb +691 -0
- data/lib/zabbix_manager/version.rb +1 -1
- data/lib/zabbix_manager.rb +118 -40
- data/zabbix_manager.gemspec +21 -8
- metadata +208 -22
- data/lib/zabbix_manager/classes/unusable.rb +0 -11
|
@@ -2,22 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
class ZabbixManager
|
|
4
4
|
class Graphs < Basic
|
|
5
|
-
#
|
|
5
|
+
# 返回图形对象对应的 Zabbix API 方法前缀。
|
|
6
|
+
#
|
|
7
|
+
# @return [String]
|
|
6
8
|
def method_name
|
|
7
9
|
"graph"
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
#
|
|
12
|
+
# 返回图形对象用于业务识别的字段名。
|
|
13
|
+
#
|
|
14
|
+
# @return [String]
|
|
11
15
|
def identify
|
|
12
16
|
"name"
|
|
13
17
|
end
|
|
14
18
|
|
|
15
|
-
#
|
|
19
|
+
# 按名称搜索并获取图形的完整数据。
|
|
20
|
+
#
|
|
21
|
+
# @param data [Hash] 包含图形识别字段及其值的查询条件
|
|
22
|
+
# @raise [ApiError] Zabbix API 返回业务错误时抛出
|
|
23
|
+
# @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
|
|
24
|
+
# @return [Hash] 匹配的图形完整数据
|
|
16
25
|
def get_full_data(data)
|
|
17
|
-
log "[DEBUG]
|
|
26
|
+
log "[DEBUG] Call get_full_data with parameters: #{data.inspect}"
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
{
|
|
28
|
+
@client.api_request(
|
|
29
|
+
method: "#{method_name}.get",
|
|
30
|
+
params: {
|
|
21
31
|
search: {
|
|
22
32
|
identify.to_sym => data[identify.to_sym]
|
|
23
33
|
},
|
|
@@ -26,10 +36,16 @@ class ZabbixManager
|
|
|
26
36
|
)
|
|
27
37
|
end
|
|
28
38
|
|
|
29
|
-
#
|
|
39
|
+
# 获取指定主机的图形 ID,并可按名称片段进一步过滤。
|
|
40
|
+
#
|
|
41
|
+
# @param data [Hash] 包含 host,且可包含 filter 的查询条件
|
|
42
|
+
# @raise [ApiError] Zabbix API 返回业务错误时抛出
|
|
43
|
+
# @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
|
|
44
|
+
# @return [Array] 匹配的图形 ID 列表
|
|
30
45
|
def get_ids_by_host(data)
|
|
31
|
-
result =
|
|
32
|
-
|
|
46
|
+
result = @client.api_request(
|
|
47
|
+
method: "graph.get",
|
|
48
|
+
params: {
|
|
33
49
|
filter: {
|
|
34
50
|
host: data[:host]
|
|
35
51
|
},
|
|
@@ -38,45 +54,36 @@ class ZabbixManager
|
|
|
38
54
|
)
|
|
39
55
|
|
|
40
56
|
result.filter_map do |graph|
|
|
41
|
-
num
|
|
42
|
-
name
|
|
57
|
+
num = graph["graphid"]
|
|
58
|
+
name = graph["name"]
|
|
43
59
|
filter = data[:filter]
|
|
44
60
|
|
|
45
|
-
num if filter.nil? ||
|
|
61
|
+
num if filter.nil? || name.include?(filter.to_s)
|
|
46
62
|
end
|
|
47
63
|
end
|
|
48
64
|
|
|
49
|
-
#
|
|
65
|
+
# 获取指定图形包含的图形监控项。
|
|
66
|
+
#
|
|
67
|
+
# @param data [Hash, String, Integer] 图形 ID
|
|
68
|
+
# @raise [ApiError] Zabbix API 返回业务错误时抛出
|
|
69
|
+
# @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
|
|
70
|
+
# @return [Hash] 图形监控项数据
|
|
50
71
|
def get_items(data)
|
|
51
72
|
@client.api_request(
|
|
52
73
|
method: "graphitem.get",
|
|
53
74
|
params: {
|
|
54
75
|
graphids: [data],
|
|
55
|
-
output:
|
|
76
|
+
output: "extend"
|
|
56
77
|
}
|
|
57
78
|
)
|
|
58
79
|
end
|
|
59
80
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
id
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# 通过 Zabbix API 创建或更新 Graph 对象
|
|
72
|
-
def create_or_update(data)
|
|
73
|
-
graphid = get_id(name: data[:name], templateid: data[:templateid])
|
|
74
|
-
graphid ? _update(data.merge(graphid: graphid)) : create(data)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def _update(data)
|
|
78
|
-
data.delete(:name)
|
|
79
|
-
update(data)
|
|
81
|
+
# 生成由图形名称和所属模板构成的稳定查询条件。
|
|
82
|
+
# @param data [Hash] 包含 name 和 templateid 的图形属性
|
|
83
|
+
# @return [Hash] 图形唯一查询条件
|
|
84
|
+
def identity_filter(data)
|
|
85
|
+
attributes = data.deep_symbolize_keys
|
|
86
|
+
{ name: attributes.fetch(:name), templateid: attributes.fetch(:templateid) }
|
|
80
87
|
end
|
|
81
88
|
end
|
|
82
89
|
end
|
|
@@ -2,19 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
class ZabbixManager
|
|
4
4
|
class HostGroups < Basic
|
|
5
|
-
#
|
|
5
|
+
# 返回主机群组对应的 Zabbix API 方法前缀。
|
|
6
|
+
#
|
|
7
|
+
# @return [String]
|
|
6
8
|
def method_name
|
|
7
9
|
"hostgroup"
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
#
|
|
12
|
+
# 返回主机群组用于业务识别的字段名。
|
|
13
|
+
#
|
|
14
|
+
# @return [String]
|
|
11
15
|
def identify
|
|
12
16
|
"name"
|
|
13
17
|
end
|
|
14
18
|
|
|
15
|
-
#
|
|
19
|
+
# 返回主机群组的 Zabbix ID 字段名。
|
|
20
|
+
#
|
|
21
|
+
# @return [String]
|
|
16
22
|
def key
|
|
17
23
|
"groupid"
|
|
18
24
|
end
|
|
25
|
+
|
|
26
|
+
# 批量解析主机群组名称并返回对应的群组 ID。
|
|
27
|
+
# @param data [Array<String>, String] 待查询的群组名称
|
|
28
|
+
# @return [Array<Hash>, nil] 群组 ID 列表,未命中时返回 nil
|
|
29
|
+
def get_hostgroup_ids(data)
|
|
30
|
+
names = normalized_names(data)
|
|
31
|
+
return nil if names.empty?
|
|
32
|
+
|
|
33
|
+
groups = @client.api_request(
|
|
34
|
+
method: "hostgroup.get",
|
|
35
|
+
params: {
|
|
36
|
+
output: %w[groupid name],
|
|
37
|
+
filter: { name: names }
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
groups.empty? ? nil : groups.map { |group| { groupid: group.fetch("groupid") } }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 批量查询并创建缺失的主机群组,避免逐名称重复查询。
|
|
45
|
+
# @param data [Array<String>, String] 待确保存在的群组名称
|
|
46
|
+
# @return [Array<Hash>] 群组 ID 列表
|
|
47
|
+
def get_or_create_hostgroups(data)
|
|
48
|
+
names = normalized_names(data)
|
|
49
|
+
existing = @client.api_request(
|
|
50
|
+
method: "hostgroup.get",
|
|
51
|
+
params: { output: %w[groupid name], filter: { name: names } }
|
|
52
|
+
).index_by { |group| group.fetch("name") }
|
|
53
|
+
|
|
54
|
+
names.map do |name|
|
|
55
|
+
group = existing[name]
|
|
56
|
+
next({ groupid: group.fetch("groupid") }) if group
|
|
57
|
+
|
|
58
|
+
result = @client.api_request(method: "hostgroup.create", params: { name: name })
|
|
59
|
+
{ groupid: result.fetch("groupids").first }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 清理、去空并去重主机群组名称。
|
|
64
|
+
# @param data [Array<String>, String] 原始群组名称
|
|
65
|
+
# @return [Array<String>] 规范化后的群组名称
|
|
66
|
+
private def normalized_names(data)
|
|
67
|
+
Array(data).filter_map { |name| name.to_s.strip.presence }.uniq
|
|
68
|
+
end
|
|
19
69
|
end
|
|
20
70
|
end
|
|
@@ -1,29 +1,189 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ZabbixManager
|
|
4
|
+
# 封装高频使用的 Zabbix 主机接口查询与幂等更新。
|
|
4
5
|
class HostInterfaces < Basic
|
|
5
|
-
#
|
|
6
|
+
# 返回对应的 Zabbix API 模块名。
|
|
7
|
+
# @return [String]
|
|
6
8
|
def method_name
|
|
7
9
|
"hostinterface"
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
# 返回接口对象的主键字段。
|
|
13
|
+
# @return [String]
|
|
14
|
+
def key
|
|
15
|
+
"interfaceid"
|
|
13
16
|
end
|
|
14
17
|
|
|
15
|
-
#
|
|
16
|
-
|
|
18
|
+
# 使用接口 ID 作为通用查询标识。
|
|
19
|
+
# @return [String]
|
|
20
|
+
def identify
|
|
17
21
|
"interfaceid"
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
#
|
|
21
|
-
# @
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
+
# 查询指定主机的全部接口,可选择同时返回关联监控项。
|
|
25
|
+
# @return [Array<Hash>]
|
|
26
|
+
def for_host(hostid, select_items: nil)
|
|
27
|
+
raise Invalid, "hostid is required" if hostid.blank?
|
|
28
|
+
|
|
29
|
+
params = { hostids: hostid, output: "extend" }
|
|
30
|
+
params[:selectItems] = select_items if select_items
|
|
31
|
+
@client.api_request(method: "hostinterface.get", params: params)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 规范化并校验接口定义,不执行远端查询或写入。
|
|
35
|
+
# @return [Array<Hash>]
|
|
36
|
+
def validate(interfaces)
|
|
37
|
+
desired = Array.wrap(interfaces).map { |interface| normalize_definition(interface) }
|
|
38
|
+
validate_definitions!(desired)
|
|
39
|
+
desired
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 校验 host.create 中全部接口的创建必填合同。
|
|
43
|
+
# @return [Array<Hash>]
|
|
44
|
+
def validate_for_create(interfaces)
|
|
45
|
+
validate(interfaces).each { |attributes| validate_create_definition!(attributes) }
|
|
46
|
+
end
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
|
|
48
|
+
# 幂等创建或更新一个主机的接口集合。
|
|
49
|
+
# @return [Array<Integer>]
|
|
50
|
+
def reconcile_for_host(hostid:, interfaces:)
|
|
51
|
+
@client.with_upsert_lock("host-interfaces:#{hostid}") do
|
|
52
|
+
plan = plan_for_host(hostid: hostid, interfaces: interfaces)
|
|
53
|
+
apply_plan(hostid: hostid, plan: plan)
|
|
54
|
+
end
|
|
27
55
|
end
|
|
56
|
+
|
|
57
|
+
# 删除明确指定的主机接口;关联监控项由 Zabbix 按官方规则处理。
|
|
58
|
+
# @return [Array<Integer>]
|
|
59
|
+
def delete_many(hostid:, interfaceids:)
|
|
60
|
+
ids = Array(interfaceids).filter_map { |value| value.to_s.strip.presence }.uniq
|
|
61
|
+
raise Invalid, "interfaceids are required" if ids.empty?
|
|
62
|
+
|
|
63
|
+
owned_ids = for_host(hostid).map { |interface| interface.fetch("interfaceid").to_s }
|
|
64
|
+
foreign_ids = ids - owned_ids
|
|
65
|
+
raise Conflict, "interfaces do not belong to host #{hostid}: #{foreign_ids.join(", ")}" if foreign_ids.any?
|
|
66
|
+
|
|
67
|
+
result = @client.api_request(method: "hostinterface.delete", params: ids)
|
|
68
|
+
Array(result.fetch("interfaceids")).map(&:to_i)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# 预先校验并生成仅供当前对象执行的接口变更计划。
|
|
74
|
+
def plan_for_host(hostid:, interfaces:)
|
|
75
|
+
desired = validate(interfaces)
|
|
76
|
+
existing = for_host(hostid)
|
|
77
|
+
desired.map { |attributes| [attributes, matching_interface(existing, attributes)] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 执行内部生成的接口变更计划,避免调用方伪造跨主机接口 ID。
|
|
81
|
+
def apply_plan(hostid:, plan:)
|
|
82
|
+
plan.map do |attributes, current|
|
|
83
|
+
current ? update_interface(attributes, current) : create_interface(hostid, attributes)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 把接口字段递归转换为统一符号键。
|
|
88
|
+
def normalize_definition(interface)
|
|
89
|
+
interface.deep_symbolize_keys
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# 校验新接口的通用字段及 SNMP 条件字段。
|
|
93
|
+
def validate_create_definition!(attributes)
|
|
94
|
+
attributes[:ip] = "" unless attributes.key?(:ip)
|
|
95
|
+
attributes[:dns] = "" unless attributes.key?(:dns)
|
|
96
|
+
interface_identity(attributes)
|
|
97
|
+
validate_snmp_details!(attributes) if attributes[:type].to_i == 2
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 校验 SNMP 版本和 v1/v2c community 必填合同。
|
|
101
|
+
def validate_snmp_details!(attributes)
|
|
102
|
+
details = attributes[:details]&.deep_symbolize_keys
|
|
103
|
+
raise Invalid, "SNMP interface details are required" unless details
|
|
104
|
+
|
|
105
|
+
version = Integer(details[:version])
|
|
106
|
+
raise Invalid, "SNMP interface version must be 1, 2, or 3" unless [1, 2, 3].include?(version)
|
|
107
|
+
if [1, 2].include?(version) && details[:community].blank?
|
|
108
|
+
raise Invalid, "SNMP v1/v2 interface community is required"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
attributes[:details] = details
|
|
112
|
+
rescue ArgumentError, TypeError
|
|
113
|
+
raise Invalid, "SNMP interface version must be 1, 2, or 3"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# 校验期望接口是否包含重复身份。
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @api private
|
|
119
|
+
def validate_definitions!(interfaces)
|
|
120
|
+
identities = interfaces.map { |interface| interface_identity(interface) }
|
|
121
|
+
duplicate = identities.tally.find { |_identity, count| count > 1 }&.first
|
|
122
|
+
raise Invalid, "duplicate desired interface identity #{duplicate}" if duplicate
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# 按显式 ID 或稳定端点身份匹配已有接口。
|
|
126
|
+
# @return [Hash, nil]
|
|
127
|
+
# @api private
|
|
128
|
+
def matching_interface(existing, attributes)
|
|
129
|
+
if attributes[:interfaceid]
|
|
130
|
+
match = existing.find { |item| item["interfaceid"].to_s == attributes[:interfaceid].to_s }
|
|
131
|
+
raise Invalid, "interfaceid #{attributes[:interfaceid]} does not belong to this host" unless match
|
|
132
|
+
|
|
133
|
+
return match
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
candidates = existing.select { |item| endpoint_identity(item) == endpoint_identity(attributes) }
|
|
137
|
+
raise Conflict, "interface identity is ambiguous; provide interfaceid" if candidates.length > 1
|
|
138
|
+
|
|
139
|
+
candidates.first
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# 生成不依赖数组顺序的接口身份。
|
|
143
|
+
# @return [String]
|
|
144
|
+
# @api private
|
|
145
|
+
def interface_identity(attributes)
|
|
146
|
+
values = attributes.with_indifferent_access
|
|
147
|
+
return "id:#{values[:interfaceid]}" if values[:interfaceid].present?
|
|
148
|
+
|
|
149
|
+
endpoint_identity(values)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# 使用类型、端点和端口匹配接口,不把可变的 main 或远端 ID 作为端点身份。
|
|
153
|
+
def endpoint_identity(attributes)
|
|
154
|
+
values = attributes.with_indifferent_access
|
|
155
|
+
|
|
156
|
+
%i[type main useip port].each do |field|
|
|
157
|
+
raise Invalid, "interface #{field} is required" if values[field].blank?
|
|
158
|
+
end
|
|
159
|
+
endpoint_field = values[:useip].to_i == 1 ? :ip : :dns
|
|
160
|
+
raise Invalid, "interface #{endpoint_field} is required" if values[endpoint_field].blank?
|
|
161
|
+
|
|
162
|
+
"endpoint:#{values[:type]}:#{values[:useip]}:#{values[endpoint_field]}:#{values[:port]}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# 更新已属于目标主机的接口。
|
|
166
|
+
# @return [Integer]
|
|
167
|
+
# @api private
|
|
168
|
+
def update_interface(attributes, current)
|
|
169
|
+
interfaceid = current.fetch("interfaceid")
|
|
170
|
+
@client.api_request(
|
|
171
|
+
method: "hostinterface.update",
|
|
172
|
+
params: attributes.except(:hostid).merge(interfaceid: interfaceid)
|
|
173
|
+
)
|
|
174
|
+
interfaceid.to_i
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# 为目标主机创建缺失接口。
|
|
178
|
+
# @return [Integer]
|
|
179
|
+
# @api private
|
|
180
|
+
def create_interface(hostid, attributes)
|
|
181
|
+
validate_create_definition!(attributes)
|
|
182
|
+
result = @client.api_request(
|
|
183
|
+
method: "hostinterface.create",
|
|
184
|
+
params: attributes.except(:interfaceid).merge(hostid: hostid)
|
|
185
|
+
)
|
|
186
|
+
result.fetch("interfaceids").first.to_i
|
|
187
|
+
end
|
|
28
188
|
end
|
|
29
189
|
end
|