net-connector 0.16.1 → 0.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8afbc2a37dcb0b9b96be6fd1af171201cb5315803452632f4af2be0db4d5a297
4
- data.tar.gz: f11815c48829959e83e89b2bfd37956c4ee9c8eded44c92c200445292fe458c0
3
+ metadata.gz: 233cb61903727cc01241fcae91e4e540a8bae1462b01210e73aa246fd1a906c9
4
+ data.tar.gz: 923cdca12fc7c38841743618a0d22527536bee646de7687c26480afbc3265a4a
5
5
  SHA512:
6
- metadata.gz: 4de68d2427f07acaca0820aac064faeabad7d73a4be81e3286566affb3300a84511955a1f5b721751a488b38651cafe0226a2daecd1bed54d8521f7967b0bd48
7
- data.tar.gz: c5d55197568da8f3a7945dd44697ad79d840ca7416408c57b9d605f1e2afb772d0a634eff72102e364e81fee6fdbb526a020300fa8085ac2018b58866cf96b96
6
+ metadata.gz: d3f2a794edb16faf5429967c4a25ee53a949fef194883cb5aefdc0f9a1a9968ccdf8a539e2f059a4a7d3f28d1e299ab1481c5d66823d555324bb11e2ae10ee0c
7
+ data.tar.gz: a3ca7a0f4bee7e40861a700063c448f5ac918bf09d7d5b5c87510a664eb024c3fd98cee3ae9a27516d63b59ee8c7aa6af7000231d74acaf24405c2852a7b423f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.17.0 - 2026-10-22
4
+
5
+ - Centralize inventory selection and CLI filters in Net::Connector::Inventory; add vendor/model inclusion, exclusion and matching modes.
6
+
3
7
  ## 未发布
4
8
 
5
9
  ## 0.16.1 - 2026-10-21
@@ -4,7 +4,7 @@ require "ipaddr"
4
4
 
5
5
  module Net
6
6
  module Connector
7
- module Netdisco
7
+ module Inventory
8
8
  # 精确地址用索引,网段和范围只保存端点;绝不展开地址或发起扫描。
9
9
  class AddressSet
10
10
  # 规范化精确地址并合并同族网段区间,不展开大型地址范围。
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "selection"
5
+
6
+ module Net
7
+ module Connector
8
+ module Inventory
9
+ # 脚本共享筛选词汇和参数;配置来源及优先级仍由宿主决定。
10
+ module Filters
11
+ LISTS = {
12
+ "hosts" => :include_hosts, "exclude_hosts" => :exclude_hosts,
13
+ "names" => :include_names, "exclude_names" => :exclude_names,
14
+ "vendors" => :include_vendors, "exclude_vendors" => :exclude_vendors,
15
+ "models" => :include_models, "exclude_models" => :exclude_models
16
+ }.freeze
17
+ MODES = %w[name_match model_match].freeze
18
+ FIELDS = (LISTS.keys + MODES).freeze
19
+ FLAGS = {
20
+ "--hosts LIST" => "hosts", "--exclude-hosts LIST" => "exclude_hosts",
21
+ "--name LIST" => "names", "--exclude-name LIST" => "exclude_names",
22
+ "--vendor LIST" => "vendors", "--exclude-vendor LIST" => "exclude_vendors",
23
+ "--model LIST" => "models", "--exclude-model LIST" => "exclude_models"
24
+ }.freeze
25
+
26
+ def self.selection(values)
27
+ arguments = LISTS.to_h { |key, argument| [argument, values.fetch(key, [])] }
28
+ MODES.each { |key| arguments[key.to_sym] = values.fetch(key, :contains) }
29
+ Selection.new(**arguments)
30
+ end
31
+
32
+ # 每个回调只交付本次参数;追加或覆盖由配置适配器显式处理。
33
+ def self.add_options(parser, single_host: true, &accept)
34
+ flags = single_host ? {"--host IP" => "hosts"}.merge(FLAGS) : FLAGS
35
+ flags.each do |flag, key|
36
+ parser.on(flag, "筛选已有清单,同类条件取并集,排除优先") do |value|
37
+ items = value.split(",", -1).map(&:strip)
38
+ raise OptionParser::InvalidArgument, "filter list contains empty items" if items.empty? || items.any?(&:empty?)
39
+
40
+ accept.call(key, items)
41
+ end
42
+ end
43
+ MODES.each do |key|
44
+ parser.on("--#{key.tr("_", "-")} MODE", %w[contains prefix glob], "匹配方式 contains / prefix / glob") { |value| accept.call(key, value) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "address_set"
4
+
5
+ module Net
6
+ module Connector
7
+ module Inventory
8
+ # 只筛选已有设备,不识别厂商、不发现地址、不建立连接。
9
+ class Selection
10
+ Decision = Data.define(:reasons) do
11
+ def initialize(reasons:)
12
+ super(reasons: reasons.dup.freeze)
13
+ end
14
+
15
+ def selected? = reasons.empty?
16
+ end
17
+
18
+ def initialize(include_hosts: [], exclude_hosts: [], include_names: [], exclude_names: [],
19
+ include_vendors: [], exclude_vendors: [], include_models: [], exclude_models: [],
20
+ name_match: :contains, model_match: :contains)
21
+ @include_hosts, @exclude_hosts = AddressSet.new(include_hosts), AddressSet.new(exclude_hosts)
22
+ @names = [text_list(include_names), text_list(exclude_names), mode(name_match)].freeze
23
+ @models = [text_list(include_models), text_list(exclude_models), mode(model_match)].freeze
24
+ @vendors = [vendor_list(include_vendors), vendor_list(exclude_vendors)].freeze
25
+ freeze
26
+ end
27
+
28
+ # 同字段包含取并集,不同字段取交集;排除优先,原因不携带设备正文。
29
+ def evaluate(host:, vendor: nil, name: nil, model: nil)
30
+ reasons = []
31
+ if @exclude_hosts.include?(host)
32
+ reasons << :host_excluded
33
+ elsif !@include_hosts.empty? && !@include_hosts.include?(host)
34
+ reasons << :host_not_included
35
+ end
36
+ vendor = vendor.to_s
37
+ if @vendors.last.include?(vendor)
38
+ reasons << :vendor_excluded
39
+ elsif !@vendors.first.empty? && !@vendors.first.include?(vendor)
40
+ reasons << :vendor_not_included
41
+ end
42
+ reasons << text_reason(:name, name, @names)
43
+ reasons << text_reason(:model, model, @models)
44
+ Decision.new(reasons: reasons.compact)
45
+ end
46
+
47
+ def selected?(host, vendor = nil, name: nil, model: nil)
48
+ evaluate(host: host, vendor: vendor, name: name, model: model).selected?
49
+ end
50
+
51
+ def inspect = "#<#{self.class}>"
52
+ alias to_s inspect
53
+
54
+ private
55
+
56
+ def mode(value)
57
+ unless (value.is_a?(String) || value.is_a?(Symbol)) && %w[contains prefix glob].include?(value.to_s)
58
+ raise ArgumentError, "match mode must be contains, prefix or glob"
59
+ end
60
+ value.to_sym
61
+ end
62
+
63
+ def text_list(values)
64
+ unless values.is_a?(Array) && values.all? { |v| v.is_a?(String) && v.valid_encoding? && !v.strip.empty? && !/[\x00-\x1f\x7f]/.match?(v) }
65
+ raise ArgumentError, "filters must be arrays of nonempty text"
66
+ end
67
+ values.map { |v| v.downcase.freeze }.uniq.freeze
68
+ end
69
+
70
+ def vendor_list(values)
71
+ unless values.is_a?(Array) && values.all? { |v| v.is_a?(String) || v.is_a?(Symbol) }
72
+ raise ArgumentError, "vendors must be strings or symbols"
73
+ end
74
+ text_list(values.map(&:to_s))
75
+ end
76
+
77
+ def text_reason(field, value, rules)
78
+ includes, excludes, match = rules
79
+ value = value&.to_s&.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")&.downcase
80
+ return :"#{field}_excluded" if value && excludes.any? { |pattern| matches?(pattern, value, match) }
81
+ return :"#{field}_not_included" if !includes.empty? && (!value || includes.none? { |pattern| matches?(pattern, value, match) })
82
+
83
+ nil
84
+ end
85
+
86
+ def matches?(pattern, value, match)
87
+ case match
88
+ when :contains then value.include?(pattern)
89
+ when :prefix then value.start_with?(pattern)
90
+ when :glob then File.fnmatch?(pattern, value)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -6,6 +6,7 @@ require_relative "../planner"
6
6
  require_relative "../config"
7
7
  require_relative "../../config"
8
8
  require_relative "help"
9
+ require_relative "../../inventory/filters"
9
10
 
10
11
  module Net
11
12
  module Connector
@@ -208,25 +209,16 @@ module Net
208
209
 
209
210
  # 同类 CLI 筛选替换 YAML;重复参数在本次调用内合并,绝不把范围展开为地址列表。
210
211
  def self.add_filters(opts, values)
211
- {
212
- "--hosts PATTERNS" => ["NC_INCLUDE_HOSTS", "包含地址、网段或范围,逗号分隔"],
213
- "--exclude-hosts PATTERNS" => ["NC_EXCLUDE_HOSTS", "排除地址、网段或范围"],
214
- "--name PATTERNS" => ["NC_INCLUDE_NAMES", "包含设备名称,可重复或逗号分隔"],
215
- "--exclude-name PATTERNS" => ["NC_EXCLUDE_NAMES", "排除设备名称,可重复或逗号分隔"],
216
- "--vendor NAMES" => ["NC_INCLUDE_VENDORS", "包含连接器厂商名"],
217
- "--exclude-vendor NAMES" => ["NC_EXCLUDE_VENDORS", "排除连接器厂商名"]
218
- }.each do |flag, (key, description)|
219
- opts.on(flag, description) do |value|
220
- items = value.split(",", -1).map(&:strip)
221
- raise OptionParser::InvalidArgument if items.empty? || items.any?(&:empty?)
222
-
223
- previous = values[:environment][key]&.split(",") || []
224
- values[:environment][key] = (previous + items).uniq.join(",")
212
+ Net::Connector::Inventory::Filters.add_options(opts, single_host: false) do |key, value|
213
+ field = Net::Connector::Inventory::Filters::LISTS.fetch(key, key).to_s
214
+ environment_key = "NC_#{field.upcase}"
215
+ if value.is_a?(Array)
216
+ previous = values[:environment][environment_key]&.split(",") || []
217
+ values[:environment][environment_key] = (previous + value).uniq.join(",")
218
+ else
219
+ values[:environment][environment_key] = value
225
220
  end
226
221
  end
227
- opts.on("--name-match MODE", %w[contains prefix glob], "名称匹配方式:contains / prefix / glob") do |value|
228
- values[:environment]["NC_NAME_MATCH"] = value
229
- end
230
222
  opts.on("--host IP", "选择单台设备,可重复") do |value|
231
223
  raise OptionParser::InvalidArgument if value.empty? || value.match?(/[\/,]/)
232
224
 
@@ -15,7 +15,8 @@ module Net
15
15
  "cache_directory" => "./.cache/net-connector/inventory", "inventory_timeout" => 300,
16
16
  "page_size" => 500, "allow_insecure_http" => false },
17
17
  "inventory" => { "include_hosts" => [], "exclude_hosts" => [], "include_names" => [], "exclude_names" => [],
18
- "include_vendors" => [], "exclude_vendors" => [], "name_match" => "contains" },
18
+ "include_vendors" => [], "exclude_vendors" => [], "name_match" => "contains",
19
+ "include_models" => [], "exclude_models" => [], "model_match" => "contains" },
19
20
  "ssh" => { "protocol" => "ssh", "host_key_policy" => "accept_new", "log_level" => "info" },
20
21
  "backup" => { "directory" => directory || "./backups", "parallel" => 4 }
21
22
  }
@@ -102,10 +102,11 @@ module Net
102
102
  if document.dig("netdisco", "source") == "http"
103
103
  field(document, "netdisco", "url", "清单 URL", mode.nil? ? "https://netdisco.example.invalid" : nil)
104
104
  end
105
- %w[include_hosts exclude_hosts include_names exclude_names].each do |key|
105
+ %w[include_hosts exclude_hosts include_names exclude_names include_models exclude_models].each do |key|
106
106
  field(document, "inventory", key, key, [], list: true)
107
107
  end
108
108
  field(document, "inventory", "name_match", "名称匹配 contains/prefix/glob", "contains")
109
+ field(document, "inventory", "model_match", "型号匹配 contains/prefix/glob", "contains")
109
110
  %w[include_vendors exclude_vendors].each { |key| field(document, "inventory", key, key, [], list: true) }
110
111
  if document.key?("topology")
111
112
  field(document, "topology", "parallel", "拓扑并发数 1..500", inherited_parallel)
@@ -45,6 +45,9 @@ module Net
45
45
  "include_names" => ["NC_INCLUDE_NAMES", :list],
46
46
  "exclude_names" => ["NC_EXCLUDE_NAMES", :list],
47
47
  "name_match" => ["NC_NAME_MATCH", :string],
48
+ "include_models" => ["NC_INCLUDE_MODELS", :list],
49
+ "exclude_models" => ["NC_EXCLUDE_MODELS", :list],
50
+ "model_match" => ["NC_MODEL_MATCH", :string],
48
51
  "exclude_vendors" => ["NC_EXCLUDE_VENDORS", :list],
49
52
  "include_vendors" => ["NC_INCLUDE_VENDORS", :list],
50
53
  "vendor_overrides" => ["NC_VENDOR_OVERRIDES", :json],
@@ -38,7 +38,7 @@ module Net
38
38
  end
39
39
  vendor = rules.resolve(row)
40
40
  name = [row["name"], row["dns"]].find { |value| value && !value.strip.empty? }
41
- issue ||= :filtered unless rules.selected?(host, vendor, name: name&.strip)
41
+ issue ||= :filtered unless rules.selected?(host, vendor, name: name&.strip, model: row["model"])
42
42
  issue ||= :unsupported_vendor unless vendor
43
43
  new(host: host, source_ip: source_ip, name: name&.strip, vendor: vendor,
44
44
  source_vendor: row["vendor"], model: row["model"], os: row["os"],
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "ipaddr"
4
4
  require_relative "../../connector"
5
- require_relative "address_set"
5
+ require_relative "../inventory/selection"
6
6
 
7
7
  module Net
8
8
  module Connector
@@ -12,17 +12,11 @@ module Net
12
12
  # 校验并保存清单过滤和厂商映射规则。
13
13
  def initialize(include_hosts: [], exclude_hosts: [], include_vendors: [], vendor_overrides: {},
14
14
  host_overrides: {}, mappings: [], include_names: [], exclude_names: [],
15
- name_match: :contains, exclude_vendors: [])
16
- @include_hosts = AddressSet.new(include_hosts)
17
- @exclude_hosts = AddressSet.new(exclude_hosts)
18
- @include_vendors = vendor_list(include_vendors)
19
- @exclude_vendors = vendor_list(exclude_vendors)
20
- @include_names = name_list(include_names)
21
- @exclude_names = name_list(exclude_names)
22
- unless (name_match.is_a?(String) || name_match.is_a?(Symbol)) && %w[contains prefix glob].include?(name_match.to_s)
23
- raise ArgumentError, "name_match must be contains, prefix or glob"
24
- end
25
- @name_match = name_match.to_sym
15
+ name_match: :contains, exclude_vendors: [], include_models: [], exclude_models: [], model_match: :contains)
16
+ @selection = Net::Connector::Inventory::Selection.new(include_hosts: include_hosts, exclude_hosts: exclude_hosts,
17
+ include_names: include_names, exclude_names: exclude_names, name_match: name_match,
18
+ include_vendors: vendor_list(include_vendors), exclude_vendors: vendor_list(exclude_vendors),
19
+ include_models: include_models, exclude_models: exclude_models, model_match: model_match)
26
20
  @vendor_overrides = vendor_mapping(vendor_overrides)
27
21
  @host_overrides = host_mapping(host_overrides)
28
22
  @mappings = device_mappings(mappings)
@@ -153,19 +147,8 @@ module Net
153
147
  end
154
148
 
155
149
  # 判断设备是否通过地址和厂商过滤。
156
- def selected?(host, vendor, name: nil)
157
- return false if @exclude_hosts.include?(host)
158
- return false if !@include_hosts.empty? && !@include_hosts.include?(host)
159
- return false if @exclude_vendors.include?(vendor)
160
- return false if !@include_vendors.empty? && !@include_vendors.include?(vendor)
161
-
162
- return true if @include_names.empty? && @exclude_names.empty?
163
-
164
- value = name.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "").downcase
165
- return false if @exclude_names.any? { |pattern| name_matches?(pattern, value) }
166
- return false if !@include_names.empty? && @include_names.none? { |pattern| name_matches?(pattern, value) }
167
-
168
- true
150
+ def selected?(host, vendor, name: nil, model: nil)
151
+ @selection.selected?(host, vendor, name: name, model: model)
169
152
  end
170
153
 
171
154
  private
@@ -176,23 +159,6 @@ module Net
176
159
  wireless ? :h3c_wireless : :h3c
177
160
  end
178
161
 
179
- # 校验名称过滤项为有效非空文本,统一小写并去重冻结。
180
- def name_list(values)
181
- unless values.is_a?(Array) && values.all? { |value| value.is_a?(String) && !value.strip.empty? && value.valid_encoding? }
182
- raise ArgumentError, "name filters must be arrays of nonempty strings"
183
- end
184
- values.map { |value| value.downcase.freeze }.uniq.freeze
185
- end
186
-
187
- # 按包含、前缀或通配符模式匹配已规范化的设备名称。
188
- def name_matches?(pattern, value)
189
- case @name_match
190
- when :contains then value.include?(pattern)
191
- when :prefix then value.start_with?(pattern)
192
- when :glob then File.fnmatch?(pattern, value)
193
- end
194
- end
195
-
196
162
  # 将设备标签转为便于匹配的小写词串。
197
163
  def normalize(value) = value.to_s.downcase.gsub(/[^a-z0-9]+/, " ").strip
198
164
  end
@@ -190,6 +190,8 @@ module Net
190
190
  exclude_vendors: list("NC_EXCLUDE_VENDORS"),
191
191
  include_names: list("NC_INCLUDE_NAMES"), exclude_names: list("NC_EXCLUDE_NAMES"),
192
192
  name_match: raw("NC_NAME_MATCH", "contains"),
193
+ include_models: list("NC_INCLUDE_MODELS"), exclude_models: list("NC_EXCLUDE_MODELS"),
194
+ model_match: raw("NC_MODEL_MATCH", "contains"),
193
195
  vendor_overrides: json("NC_VENDOR_OVERRIDES", {}),
194
196
  host_overrides: json("NC_HOST_OVERRIDES", {}),
195
197
  mappings: json("NC_DEVICE_RULES", []))
@@ -381,6 +383,8 @@ module Net
381
383
  exclude_vendors: list("NC_EXCLUDE_VENDORS"),
382
384
  include_names: list("NC_INCLUDE_NAMES"), exclude_names: list("NC_EXCLUDE_NAMES"),
383
385
  name_match: raw("NC_NAME_MATCH", "contains"),
386
+ include_models: list("NC_INCLUDE_MODELS"), exclude_models: list("NC_EXCLUDE_MODELS"),
387
+ model_match: raw("NC_MODEL_MATCH", "contains"),
384
388
  vendor_overrides: json("NC_VENDOR_OVERRIDES", {}),
385
389
  host_overrides: json("NC_HOST_OVERRIDES", {}),
386
390
  device_rules: json("NC_DEVICE_RULES", []) },
@@ -3,6 +3,6 @@
3
3
  module Net
4
4
  module Connector
5
5
  # gemspec、CLI 与发布工具共用此版本,运行时读取不触发设备依赖加载。
6
- VERSION = "0.16.1"
6
+ VERSION = "0.17.0"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-connector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.1
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - net-connector contributors
@@ -417,8 +417,10 @@ files:
417
417
  - lib/net/connector/engine/terminal/renderer.rb
418
418
  - lib/net/connector/engine/terminal/text.rb
419
419
  - lib/net/connector/engine/transports.rb
420
+ - lib/net/connector/inventory/address_set.rb
421
+ - lib/net/connector/inventory/filters.rb
422
+ - lib/net/connector/inventory/selection.rb
420
423
  - lib/net/connector/netdisco.rb
421
- - lib/net/connector/netdisco/address_set.rb
422
424
  - lib/net/connector/netdisco/batch.rb
423
425
  - lib/net/connector/netdisco/cli.rb
424
426
  - lib/net/connector/netdisco/cli/backup.rb