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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -9
  3. data/LICENSE +1 -1
  4. data/README.md +202 -8
  5. data/lib/zabbix_manager/basic/basic_alias.rb +25 -3
  6. data/lib/zabbix_manager/basic/basic_func.rb +32 -61
  7. data/lib/zabbix_manager/basic/basic_init.rb +28 -13
  8. data/lib/zabbix_manager/basic/basic_logic.rb +172 -128
  9. data/lib/zabbix_manager/classes/actions.rb +21 -11
  10. data/lib/zabbix_manager/classes/applications.rb +12 -17
  11. data/lib/zabbix_manager/classes/configurations.rb +20 -5
  12. data/lib/zabbix_manager/classes/drules.rb +11 -23
  13. data/lib/zabbix_manager/classes/errors.rb +16 -28
  14. data/lib/zabbix_manager/classes/events.rb +6 -2
  15. data/lib/zabbix_manager/classes/graphs.rb +41 -34
  16. data/lib/zabbix_manager/classes/hostgroups.rb +53 -3
  17. data/lib/zabbix_manager/classes/hostinterfaces.rb +172 -12
  18. data/lib/zabbix_manager/classes/hosts.rb +197 -68
  19. data/lib/zabbix_manager/classes/httptests.rb +18 -20
  20. data/lib/zabbix_manager/classes/items.rb +199 -72
  21. data/lib/zabbix_manager/classes/maintenance.rb +6 -2
  22. data/lib/zabbix_manager/classes/mediatypes.rb +7 -63
  23. data/lib/zabbix_manager/classes/problems.rb +75 -57
  24. data/lib/zabbix_manager/classes/proxies.rb +48 -7
  25. data/lib/zabbix_manager/classes/roles.rb +47 -36
  26. data/lib/zabbix_manager/classes/screens.rb +45 -14
  27. data/lib/zabbix_manager/classes/scripts.rb +17 -7
  28. data/lib/zabbix_manager/classes/server.rb +7 -1
  29. data/lib/zabbix_manager/classes/templates.rb +59 -21
  30. data/lib/zabbix_manager/classes/triggers.rb +253 -66
  31. data/lib/zabbix_manager/classes/usergroups.rb +28 -6
  32. data/lib/zabbix_manager/classes/usermacros.rb +119 -33
  33. data/lib/zabbix_manager/classes/users.rb +39 -7
  34. data/lib/zabbix_manager/classes/valuemaps.rb +13 -19
  35. data/lib/zabbix_manager/client.rb +311 -135
  36. data/lib/zabbix_manager/http_transport.rb +192 -0
  37. data/lib/zabbix_manager/log_sanitizer.rb +68 -0
  38. data/lib/zabbix_manager/monitoring.rb +691 -0
  39. data/lib/zabbix_manager/version.rb +1 -1
  40. data/lib/zabbix_manager.rb +118 -40
  41. data/zabbix_manager.gemspec +21 -8
  42. metadata +208 -22
  43. 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
- # 用于通过 Zabbix API 与 Graphs 交互的方法名称
5
+ # 返回图形对象对应的 Zabbix API 方法前缀。
6
+ #
7
+ # @return [String]
6
8
  def method_name
7
9
  "graph"
8
10
  end
9
11
 
10
- # 用于通过 Zabbix API 标识特定 Graph 对象的 id 字段名称
12
+ # 返回图形对象用于业务识别的字段名。
13
+ #
14
+ # @return [String]
11
15
  def identify
12
16
  "name"
13
17
  end
14
18
 
15
- # 从 Zabbix API 获取完整/扩展的 Graph 数据
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] 使用参数调用 get_full_data: #{data.inspect}"
26
+ log "[DEBUG] Call get_full_data with parameters: #{data.inspect}"
18
27
 
19
- get_raw(
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
- # 从 Zabbix API 获取主机的 Graph ids
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 = get_raw(
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 = graph["graphid"]
42
- name = graph["name"]
57
+ num = graph["graphid"]
58
+ name = graph["name"]
43
59
  filter = data[:filter]
44
60
 
45
- num if filter.nil? || /#{filter}/ =~ name
61
+ num if filter.nil? || name.include?(filter.to_s)
46
62
  end
47
63
  end
48
64
 
49
- # 通过 Zabbix API 获取 Graph Item 对象
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: "extend"
76
+ output: "extend"
56
77
  }
57
78
  )
58
79
  end
59
80
 
60
- # 通过 Zabbix API 获取或创建 Graph 对象
61
- def get_or_create(data)
62
- log "[DEBUG] 使用参数调用 get_or_create: #{data.inspect}"
63
-
64
- unless (id = get_id(name: data[:name], templateid: data[:templateid]))
65
- id = create(data)
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
- # 用于通过 Zabbix API 与 HostGroups 交互的方法名称
5
+ # 返回主机群组对应的 Zabbix API 方法前缀。
6
+ #
7
+ # @return [String]
6
8
  def method_name
7
9
  "hostgroup"
8
10
  end
9
11
 
10
- # 用于通过 Zabbix API 标识特定 HostGroup 对象的 id 字段名称
12
+ # 返回主机群组用于业务识别的字段名。
13
+ #
14
+ # @return [String]
11
15
  def identify
12
16
  "name"
13
17
  end
14
18
 
15
- # HostGroup 对象通过 Zabbix API 使用的 key 字段名称
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
- # 用于通过 Zabbix API 与 HostInterfaces 交互的方法名称
6
+ # 返回对应的 Zabbix API 模块名。
7
+ # @return [String]
6
8
  def method_name
7
9
  "hostinterface"
8
10
  end
9
11
 
10
- # 用于通过 Zabbix API 标识特定 HostInterface 对象的 id 字段名称
11
- def identify
12
- "hostids"
12
+ # 返回接口对象的主键字段。
13
+ # @return [String]
14
+ def key
15
+ "interfaceid"
13
16
  end
14
17
 
15
- # HostInterface 对象通过 Zabbix API 使用的 key 字段名称
16
- def key
18
+ # 使用接口 ID 作为通用查询标识。
19
+ # @return [String]
20
+ def identify
17
21
  "interfaceid"
18
22
  end
19
23
 
20
- # 通过 Zabbix API 基于提供的 hostid 获取 Zabbix interface 的 id
21
- # @note 基于 hostid 查询关联的 Interfaceid
22
- def get_interfaceid(hostid)
23
- result = get_raw({ output: key, identify.to_sym => hostid })
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
- # 如果查询结果不为空,且第一个元素不为空,则继续获取 {interfaceid} 并转换为整数
26
- result&.[](0)&.[](key)&.to_i
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