zabbix_manager 5.1.5 → 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 +55 -9
- data/LICENSE +1 -1
- data/README.md +241 -24
- data/lib/zabbix_manager/classes/actions.rb +19 -17
- data/lib/zabbix_manager/classes/applications.rb +11 -22
- data/lib/zabbix_manager/classes/configurations.rb +20 -10
- data/lib/zabbix_manager/classes/discovery_rules.rb +23 -0
- data/lib/zabbix_manager/classes/errors.rb +18 -27
- data/lib/zabbix_manager/classes/events.rb +8 -3
- data/lib/zabbix_manager/classes/graphs.rb +48 -37
- data/lib/zabbix_manager/classes/host_groups.rb +64 -0
- data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
- data/lib/zabbix_manager/classes/hosts.rb +232 -76
- data/lib/zabbix_manager/classes/http_tests.rb +32 -0
- data/lib/zabbix_manager/classes/items.rb +206 -72
- data/lib/zabbix_manager/classes/maintenance.rb +8 -3
- data/lib/zabbix_manager/classes/media_types.rb +18 -0
- data/lib/zabbix_manager/classes/problems.rb +51 -62
- data/lib/zabbix_manager/classes/proxies.rb +45 -14
- data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
- data/lib/zabbix_manager/classes/roles.rb +46 -41
- data/lib/zabbix_manager/classes/screens.rb +36 -24
- data/lib/zabbix_manager/classes/scripts.rb +16 -12
- data/lib/zabbix_manager/classes/server.rb +8 -1
- data/lib/zabbix_manager/classes/templates.rb +26 -51
- data/lib/zabbix_manager/classes/triggers.rb +251 -67
- 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 +16 -23
- data/lib/zabbix_manager/classes/value_maps.rb +52 -0
- data/lib/zabbix_manager/client.rb +183 -137
- data/lib/zabbix_manager/configuration.rb +107 -0
- data/lib/zabbix_manager/http_transport.rb +240 -0
- data/lib/zabbix_manager/log_sanitizer.rb +58 -0
- 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 +265 -0
- 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 +110 -145
- data/zabbix_manager.gemspec +12 -15
- metadata +67 -30
- data/lib/zabbix_manager/basic/basic_alias.rb +0 -20
- data/lib/zabbix_manager/basic/basic_func.rb +0 -84
- data/lib/zabbix_manager/basic/basic_init.rb +0 -35
- data/lib/zabbix_manager/basic/basic_logic.rb +0 -195
- data/lib/zabbix_manager/classes/drules.rb +0 -41
- data/lib/zabbix_manager/classes/hostgroups.rb +0 -20
- data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -29
- data/lib/zabbix_manager/classes/httptests.rb +0 -40
- data/lib/zabbix_manager/classes/mediatypes.rb +0 -79
- data/lib/zabbix_manager/classes/unusable.rb +0 -11
- data/lib/zabbix_manager/classes/usergroups.rb +0 -53
- data/lib/zabbix_manager/classes/usermacros.rb +0 -136
- data/lib/zabbix_manager/classes/valuemaps.rb +0 -32
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ZabbixManager
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# 图形以 hostid + name 查询;写入所有者由 gitems 中的监控项确定。
|
|
5
|
+
class Graphs < Resource
|
|
6
|
+
# 返回图形对象对应的 Zabbix API 方法前缀。
|
|
7
|
+
#
|
|
8
|
+
# @return [String]
|
|
6
9
|
def method_name
|
|
7
10
|
"graph"
|
|
8
11
|
end
|
|
9
12
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
13
|
+
# 按名称搜索并获取图形的完整数据。
|
|
14
|
+
#
|
|
15
|
+
# @param data [Hash] 包含图形识别字段及其值的查询条件
|
|
16
|
+
# @raise [ApiError] Zabbix API 返回业务错误时抛出
|
|
17
|
+
# @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
|
|
18
|
+
# @return [Array<Hash>] 匹配的图形完整数据
|
|
16
19
|
def get_full_data(data)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
20
|
+
@client.api_request(
|
|
21
|
+
method: "#{method_name}.get",
|
|
22
|
+
params: {
|
|
21
23
|
search: {
|
|
22
24
|
identify.to_sym => data[identify.to_sym]
|
|
23
25
|
},
|
|
@@ -26,10 +28,16 @@ class ZabbixManager
|
|
|
26
28
|
)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
#
|
|
31
|
+
# 获取指定主机的图形 ID,并可按名称片段进一步过滤。
|
|
32
|
+
#
|
|
33
|
+
# @param data [Hash] 包含 host,且可包含 filter 的查询条件
|
|
34
|
+
# @raise [ApiError] Zabbix API 返回业务错误时抛出
|
|
35
|
+
# @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
|
|
36
|
+
# @return [Array] 匹配的图形 ID 列表
|
|
30
37
|
def get_ids_by_host(data)
|
|
31
|
-
result =
|
|
32
|
-
|
|
38
|
+
result = @client.api_request(
|
|
39
|
+
method: "graph.get",
|
|
40
|
+
params: {
|
|
33
41
|
filter: {
|
|
34
42
|
host: data[:host]
|
|
35
43
|
},
|
|
@@ -38,45 +46,48 @@ class ZabbixManager
|
|
|
38
46
|
)
|
|
39
47
|
|
|
40
48
|
result.filter_map do |graph|
|
|
41
|
-
|
|
42
|
-
name
|
|
43
|
-
filter
|
|
49
|
+
num = graph["graphid"]
|
|
50
|
+
name = graph["name"]
|
|
51
|
+
filter = data[:filter]
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
num if filter.nil? || name.include?(filter.to_s)
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
|
|
49
|
-
#
|
|
57
|
+
# 获取指定图形包含的图形监控项。
|
|
58
|
+
#
|
|
59
|
+
# @param data [Hash, String, Integer] 图形 ID
|
|
60
|
+
# @raise [ApiError] Zabbix API 返回业务错误时抛出
|
|
61
|
+
# @raise [TransportError] Zabbix 服务端返回非成功 HTTP 状态时抛出
|
|
62
|
+
# @return [Array<Hash>] 图形监控项数据
|
|
50
63
|
def get_items(data)
|
|
51
64
|
@client.api_request(
|
|
52
65
|
method: "graphitem.get",
|
|
53
66
|
params: {
|
|
54
67
|
graphids: [data],
|
|
55
|
-
output:
|
|
68
|
+
output: "extend"
|
|
56
69
|
}
|
|
57
70
|
)
|
|
58
71
|
end
|
|
59
72
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
id
|
|
73
|
+
# 生成由图形名称和所属主机或模板构成的稳定查询条件。
|
|
74
|
+
# graph.templateid 是继承源图形 ID,不是所属模板 ID。
|
|
75
|
+
# @param data [Hash] 包含 name 和 hostid 的图形属性
|
|
76
|
+
# @return [Hash] 图形唯一查询条件
|
|
77
|
+
def identity_filter(data)
|
|
78
|
+
attributes = data.deep_symbolize_keys
|
|
79
|
+
{ name: attributes.fetch(:name), hostid: attributes.fetch(:hostid) }
|
|
69
80
|
end
|
|
70
81
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
# 图形所有者由 gitems 确定,hostid 仅用于调用方的身份查询。
|
|
83
|
+
# @return [Integer, nil]
|
|
84
|
+
def create(data)
|
|
85
|
+
super(data.deep_symbolize_keys.except(:hostid))
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
# @return [Integer, nil] 更新的图形 ID
|
|
89
|
+
def update(data)
|
|
90
|
+
super(data.deep_symbolize_keys.except(:hostid))
|
|
80
91
|
end
|
|
81
92
|
end
|
|
82
93
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
# 主机群组名称解析与按需创建;批量创建按顺序执行,不是远端事务。
|
|
5
|
+
class HostGroups < Resource
|
|
6
|
+
# 返回主机群组对应的 Zabbix API 方法前缀。
|
|
7
|
+
#
|
|
8
|
+
# @return [String]
|
|
9
|
+
def method_name
|
|
10
|
+
"hostgroup"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# 返回主机群组的 Zabbix ID 字段名。
|
|
14
|
+
#
|
|
15
|
+
# @return [String]
|
|
16
|
+
def key
|
|
17
|
+
"groupid"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 批量解析主机群组名称并返回对应的群组 ID。
|
|
21
|
+
# @param data [Array<String>, String] 待查询的群组名称
|
|
22
|
+
# @return [Array<Hash>, nil] 群组 ID 列表,未命中时返回 nil
|
|
23
|
+
def get_hostgroup_ids(data)
|
|
24
|
+
names = normalized_names(data)
|
|
25
|
+
return nil if names.empty?
|
|
26
|
+
|
|
27
|
+
groups = @client.api_request(
|
|
28
|
+
method: "hostgroup.get",
|
|
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") } }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 批量查询并创建缺失的主机群组,避免逐名称重复查询。
|
|
39
|
+
# @param data [Array<String>, String] 待确保存在的群组名称
|
|
40
|
+
# @return [Array<Hash>] 群组 ID 列表
|
|
41
|
+
def get_or_create_host_groups(data)
|
|
42
|
+
names = normalized_names(data)
|
|
43
|
+
existing = @client.api_request(
|
|
44
|
+
method: "hostgroup.get",
|
|
45
|
+
params: { output: %w[groupid name], filter: { name: names } }
|
|
46
|
+
).index_by { |group| group.fetch("name") }
|
|
47
|
+
|
|
48
|
+
names.map do |name|
|
|
49
|
+
group = existing[name]
|
|
50
|
+
next({ groupid: response_identifier(group["groupid"]).to_s }) if group
|
|
51
|
+
|
|
52
|
+
result = @client.api_request(method: "hostgroup.create", params: { name: name })
|
|
53
|
+
{ groupid: response_id(result).to_s }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 清理、去空并去重主机群组名称。
|
|
58
|
+
# @param data [Array<String>, String] 原始群组名称
|
|
59
|
+
# @return [Array<String>] 规范化后的群组名称
|
|
60
|
+
private def normalized_names(data)
|
|
61
|
+
Array(data).filter_map { |name| name.to_s.strip.presence }.uniq
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
# 封装高频使用的 Zabbix 主机接口查询与幂等更新。
|
|
5
|
+
class HostInterfaces < Resource
|
|
6
|
+
# 返回对应的 Zabbix API 模块名。
|
|
7
|
+
# @return [String]
|
|
8
|
+
def method_name
|
|
9
|
+
"hostinterface"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# 返回接口对象的主键字段。
|
|
13
|
+
# @return [String]
|
|
14
|
+
def key
|
|
15
|
+
"interfaceid"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 使用接口 ID 作为通用查询标识。
|
|
19
|
+
# @return [String]
|
|
20
|
+
def identify
|
|
21
|
+
"interfaceid"
|
|
22
|
+
end
|
|
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
|
|
47
|
+
|
|
48
|
+
# 只读取并校验接口归属和创建必填字段,供多资源编排在首个写入前预检。
|
|
49
|
+
# @param hostid [Integer, String] 目标主机 ID
|
|
50
|
+
# @param interfaces [Array<Hash>] 期望接口定义
|
|
51
|
+
# @return [Array<Hash>] 已验证的接口属性;执行时仍需重新核实远端状态
|
|
52
|
+
def validate_for_host(hostid:, interfaces:)
|
|
53
|
+
plan_for_host(hostid: hostid, interfaces: interfaces).map(&:first)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 幂等创建或更新一个主机的接口集合。
|
|
57
|
+
# @return [Array<Integer>]
|
|
58
|
+
def reconcile_for_host(hostid:, interfaces:)
|
|
59
|
+
@client.with_upsert_lock("host-interfaces:#{hostid}") do
|
|
60
|
+
plan = plan_for_host(hostid: hostid, interfaces: interfaces)
|
|
61
|
+
apply_plan(hostid: hostid, plan: plan)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 删除明确指定的主机接口;关联监控项由 Zabbix 按官方规则处理。
|
|
66
|
+
# @return [Array<Integer>]
|
|
67
|
+
def delete_many(hostid:, interfaceids:)
|
|
68
|
+
ids = normalized_ids(interfaceids)
|
|
69
|
+
|
|
70
|
+
owned_ids = for_host(hostid).map { |interface| interface.fetch("interfaceid").to_s }
|
|
71
|
+
foreign_ids = ids - owned_ids
|
|
72
|
+
raise Conflict, "interfaces do not belong to host #{hostid}: #{foreign_ids.join(", ")}" if foreign_ids.any?
|
|
73
|
+
|
|
74
|
+
result = @client.api_request(method: "hostinterface.delete", params: ids)
|
|
75
|
+
response_ids(result, expected: ids)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# 预先校验并生成仅供当前对象执行的接口变更计划。
|
|
81
|
+
def plan_for_host(hostid:, interfaces:)
|
|
82
|
+
desired = validate(interfaces)
|
|
83
|
+
existing = for_host(hostid)
|
|
84
|
+
plans = desired.map do |attributes|
|
|
85
|
+
current = matching_interface(existing, attributes)
|
|
86
|
+
response_identifier(current["interfaceid"]) if current
|
|
87
|
+
validate_create_definition!(attributes) unless current
|
|
88
|
+
[attributes, current]
|
|
89
|
+
end
|
|
90
|
+
targets = plans.filter_map { |_attributes, current| current && response_identifier(current["interfaceid"]) }
|
|
91
|
+
raise Invalid, "duplicate resolved interface target" unless targets.uniq == targets
|
|
92
|
+
|
|
93
|
+
plans
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 执行内部生成的接口变更计划,避免调用方伪造跨主机接口 ID。
|
|
97
|
+
def apply_plan(hostid:, plan:)
|
|
98
|
+
plan.map do |attributes, current|
|
|
99
|
+
current ? update_interface(attributes, current) : create_interface(hostid, attributes)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 把接口字段递归转换为统一符号键。
|
|
104
|
+
def normalize_definition(interface)
|
|
105
|
+
raise Invalid, "interface must be a hash" unless interface.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
attributes = interface.deep_symbolize_keys
|
|
108
|
+
if attributes.key?(:type)
|
|
109
|
+
type = integer_attribute(attributes[:type], "interface type")
|
|
110
|
+
raise Invalid, "interface type must be 1, 2, 3, or 4" unless [1, 2, 3, 4].include?(type)
|
|
111
|
+
end
|
|
112
|
+
%i[main useip].each do |field|
|
|
113
|
+
next unless attributes.key?(field)
|
|
114
|
+
|
|
115
|
+
unless [0, 1].include?(integer_attribute(attributes[field], "interface #{field}"))
|
|
116
|
+
raise Invalid, "interface #{field} must be 0 or 1"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
%i[ip dns].each do |field|
|
|
120
|
+
if attributes.key?(field) && !attributes[field].is_a?(String)
|
|
121
|
+
raise Invalid, "interface #{field} must be a string"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
validate_port!(attributes[:port]) if attributes.key?(:port)
|
|
125
|
+
details = attributes[:details]
|
|
126
|
+
if attributes.key?(:details) && !details.is_a?(Hash)
|
|
127
|
+
raise Invalid, "interface details must be a hash"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
validate_supplied_snmp_details!(details) if details
|
|
131
|
+
attributes
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_supplied_snmp_details!(details)
|
|
135
|
+
validate_snmp_version!(details[:version]) if details.key?(:version)
|
|
136
|
+
%i[community securityname authpassphrase privpassphrase contextname].each do |field|
|
|
137
|
+
if details.key?(field) && !details[field].is_a?(String)
|
|
138
|
+
raise Invalid, "SNMP #{field} must be a string"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
{ bulk: [0, 1], securitylevel: [0, 1, 2] }.each do |field, allowed|
|
|
142
|
+
next unless details.key?(field)
|
|
143
|
+
|
|
144
|
+
unless allowed.include?(integer_attribute(details[field], "SNMP #{field}"))
|
|
145
|
+
raise Invalid, "SNMP #{field} is invalid"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_port!(value)
|
|
151
|
+
text = value.to_s
|
|
152
|
+
numeric = (value.is_a?(String) || value.is_a?(Integer)) && text.match?(/\A[1-9]\d*\z/)
|
|
153
|
+
macro = value.is_a?(String) && text.match?(/\A\{\$[^{}\r\n]+\}\z/)
|
|
154
|
+
return if macro || (numeric && value.to_i <= 65_535)
|
|
155
|
+
|
|
156
|
+
raise Invalid, "interface port must be between 1 and 65535 or a user macro"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 校验新接口的通用字段及 SNMP 条件字段。
|
|
160
|
+
def validate_create_definition!(attributes)
|
|
161
|
+
raise Invalid, "new interfaces cannot reference an existing interfaceid" if attributes.key?(:interfaceid)
|
|
162
|
+
|
|
163
|
+
attributes[:ip] = "" unless attributes.key?(:ip)
|
|
164
|
+
attributes[:dns] = "" unless attributes.key?(:dns)
|
|
165
|
+
endpoint_identity(attributes)
|
|
166
|
+
validate_snmp_details!(attributes) if attributes[:type].to_i == 2
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# 校验 SNMP 版本和 v1/v2c community 必填合同。
|
|
170
|
+
def validate_snmp_details!(attributes)
|
|
171
|
+
details = attributes[:details]&.deep_symbolize_keys
|
|
172
|
+
raise Invalid, "SNMP interface details are required" unless details
|
|
173
|
+
|
|
174
|
+
version = validate_snmp_version!(details[:version])
|
|
175
|
+
if [1, 2].include?(version) && details[:community].blank?
|
|
176
|
+
raise Invalid, "SNMP v1/v2 interface community is required"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
attributes[:details] = details
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate_snmp_version!(value)
|
|
183
|
+
version = integer_attribute(value, "SNMP interface version")
|
|
184
|
+
raise Invalid, "SNMP interface version must be 1, 2, or 3" unless [1, 2, 3].include?(version)
|
|
185
|
+
|
|
186
|
+
version
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# 校验期望接口是否包含重复身份。
|
|
190
|
+
# @return [void]
|
|
191
|
+
# @api private
|
|
192
|
+
def validate_definitions!(interfaces)
|
|
193
|
+
identities = interfaces.map { |interface| interface_identity(interface) }
|
|
194
|
+
duplicate = identities.tally.find { |_identity, count| count > 1 }&.first
|
|
195
|
+
raise Invalid, "duplicate desired interface identity #{duplicate}" if duplicate
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# 按显式 ID 或稳定端点身份匹配已有接口。
|
|
199
|
+
# @return [Hash, nil]
|
|
200
|
+
# @api private
|
|
201
|
+
def matching_interface(existing, attributes)
|
|
202
|
+
if attributes[:interfaceid]
|
|
203
|
+
match = existing.find { |item| item["interfaceid"].to_s == attributes[:interfaceid].to_s }
|
|
204
|
+
raise Invalid, "interfaceid #{attributes[:interfaceid]} does not belong to this host" unless match
|
|
205
|
+
|
|
206
|
+
return match
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
candidates = existing.select { |item| endpoint_identity(item) == endpoint_identity(attributes) }
|
|
210
|
+
raise Conflict, "interface identity is ambiguous; provide interfaceid" if candidates.length > 1
|
|
211
|
+
|
|
212
|
+
candidates.first
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# 生成不依赖数组顺序的接口身份。
|
|
216
|
+
# @return [String]
|
|
217
|
+
# @api private
|
|
218
|
+
def interface_identity(attributes)
|
|
219
|
+
values = attributes.with_indifferent_access
|
|
220
|
+
return "id:#{values[:interfaceid]}" if values[:interfaceid].present?
|
|
221
|
+
|
|
222
|
+
endpoint_identity(values)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# 使用类型、端点和端口匹配接口,不把可变的 main 或远端 ID 作为端点身份。
|
|
226
|
+
def endpoint_identity(attributes)
|
|
227
|
+
values = attributes.with_indifferent_access
|
|
228
|
+
|
|
229
|
+
%i[type main useip port].each do |field|
|
|
230
|
+
raise Invalid, "interface #{field} is required" if values[field].blank?
|
|
231
|
+
end
|
|
232
|
+
endpoint_field = values[:useip].to_i == 1 ? :ip : :dns
|
|
233
|
+
raise Invalid, "interface #{endpoint_field} is required" if values[endpoint_field].blank?
|
|
234
|
+
|
|
235
|
+
"endpoint:#{values[:type]}:#{values[:useip]}:#{values[endpoint_field]}:#{values[:port]}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# 更新已属于目标主机的接口。
|
|
239
|
+
# @return [Integer]
|
|
240
|
+
# @api private
|
|
241
|
+
def update_interface(attributes, current)
|
|
242
|
+
interfaceid = current.fetch("interfaceid")
|
|
243
|
+
result = @client.api_request(
|
|
244
|
+
method: "hostinterface.update",
|
|
245
|
+
params: attributes.except(:hostid).merge(interfaceid: interfaceid)
|
|
246
|
+
)
|
|
247
|
+
response_id(result, expected: [interfaceid])
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# 为目标主机创建缺失接口。
|
|
251
|
+
# @return [Integer]
|
|
252
|
+
# @api private
|
|
253
|
+
def create_interface(hostid, attributes)
|
|
254
|
+
result = @client.api_request(
|
|
255
|
+
method: "hostinterface.create",
|
|
256
|
+
params: attributes.except(:interfaceid).merge(hostid: hostid)
|
|
257
|
+
)
|
|
258
|
+
response_id(result)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|