vxlan 0.6.0 → 0.7.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 +7 -0
- data/README.md +3 -1
- data/lib/vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +7 -5
- data/lib/vxlan/cli/apply.rb +22 -87
- data/lib/vxlan/cli/collect.rb +1 -1
- data/lib/vxlan/cli/options.rb +2 -2
- data/lib/vxlan/cli/report.rb +2 -1
- data/lib/vxlan/cli/review/report.rb +5 -4
- data/lib/vxlan/cli/review.rb +1 -1
- data/lib/vxlan/collector.rb +8 -14
- data/lib/vxlan/config/evidence.rb +1 -1
- data/lib/vxlan/fabric/review.rb +2 -1
- data/lib/vxlan/recommendations.rb +3 -2
- data/lib/vxlan/snapshot.rb +19 -31
- data/lib/vxlan/version.rb +1 -1
- metadata +34 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c96781edce4b5dcc8990ced65d1287cfb86ee2c431ee3c0b0714365513955d75
|
|
4
|
+
data.tar.gz: '0187135b1f3cff8988732b9fb3ed58633e780b3538b569b1ea611f87d09af08a'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5386c55fa4f48e6e121ac63dbca3c8148754857df0fa68841d345301aadf6f1678dc682d86713030110203de4fc8f7f22ed2d2fbb44135bebb9e39157df6386
|
|
7
|
+
data.tar.gz: 5c1434eb7eac8ea5649e136523020579c1abf6d0ae57309ee20335989c164922789e70ca62322d55de1775752eb5902b805c50ef80d7cf48151e1968a449d3fe
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ Changes to the public contract are recorded here. Versions follow Semantic Versi
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.7.0 (2026-10-29)
|
|
8
|
+
|
|
9
|
+
- Compose the shared snapshot contract and remove the synthetic collection observation. Whitelist public target identities and validate timestamps and dimensions.
|
|
10
|
+
- Normalize OSPF process_id, router_id, neighbor_address, symbolic state and neighbor_role while retaining priority and uptime.
|
|
11
|
+
- Delegate interactive apply to ctx_cli, resolving credentials through net-connector, including empty overrides and platform enable passwords.
|
|
12
|
+
- Use schema 3 reports and named checksummed plans. Older files must be regenerated.
|
|
13
|
+
|
|
7
14
|
## 0.6.0 (2026-10-29)
|
|
8
15
|
|
|
9
16
|
- Require RuntimeParser platform at construction and add parse_many. Rename Snapshot Ruby members to observations/observation while retaining dimensions in persisted reports. Use Catalog and shared Credentials in CLI; reject selected duplicate addresses across platforms before connecting.
|
data/README.md
CHANGED
|
@@ -146,6 +146,8 @@ Offline review requires `snapshot.target.host` to match the canonical device hos
|
|
|
146
146
|
|
|
147
147
|
## API migration
|
|
148
148
|
|
|
149
|
-
`RuntimeParser.new(platform:, model: nil)` fixes the platform for `parse(name, text, complete: false, command: nil)` and `parse_many(names, text, complete: false, command: nil)`. Snapshots use `observations:` and expose `observations` / `observation(name)`; the old Ruby `dimensions` member and Wireless `dimension` accessor are removed. Known missing dimensions return `not_requested`; unknown names raise `ArgumentError`. Persisted JSON
|
|
149
|
+
`RuntimeParser.new(platform:, model: nil)` fixes the platform for `parse(name, text, complete: false, command: nil)` and `parse_many(names, text, complete: false, command: nil)`. Snapshots use `observations:` and expose `observations` / `observation(name)`; the old Ruby `dimensions` member and Wireless `dimension` accessor are removed. Known missing dimensions return `not_requested`; unknown names raise `ArgumentError`. Persisted schema 3 JSON uses `observations` and an independent `collection_status`; regenerate older reports.
|
|
150
150
|
|
|
151
151
|
Dynamic inventory uses `Netdisco::Catalog`. Any duplicate row for a selected address, including another platform, prevents connection; excluded conflicts retain safe diagnostics. `NC_<PLATFORM>_ENABLE_PASSWORD` takes precedence over `NC_ENABLE_PASSWORD`.
|
|
152
|
+
|
|
153
|
+
本次契约升级要求重新生成旧报告和计划;Ruby 快照使用 `observations`、`collection_status` 和 `Time`,JSON 通过 `CtxRunner::Document` 输出 UTC ISO8601(解析基础库仅提供 `Result`)。跨模块约定见 [ctx_runner 契约迁移](https://github.com/gatework/ctx_runner/blob/main/docs/contracts-2026-10-29.md)。
|
|
@@ -8,11 +8,11 @@ module Vxlan
|
|
|
8
8
|
# 逐进程、VRF 独立核对 OSPF 邻居总数。
|
|
9
9
|
class ShowIpOspfNeighbors < Parser
|
|
10
10
|
EMPTY = "(?:OSPF )?neighbors"
|
|
11
|
-
IDENTITY = %i[
|
|
11
|
+
IDENTITY = %i[process_id vrf router_id interface].freeze
|
|
12
12
|
BOUNDARY = /^\s*OSPF Process ID /
|
|
13
13
|
HEADER = /\ANeighbor ID\s+Pri\s+State\s+Up Time\s+Address\s+Interface\z/i
|
|
14
14
|
ROW = /\A([\d.]+)\s+(\d+)\s+(\S+(?:\/\s*\S+)?)\s+(\S+)\s+([\d.]+)\s+(\S+)\z/
|
|
15
|
-
FIELDS = %i[
|
|
15
|
+
FIELDS = %i[router_id priority state uptime neighbor_address interface].freeze
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
@@ -23,18 +23,20 @@ module Vxlan
|
|
|
23
23
|
if HEADER.match?(line)
|
|
24
24
|
@header = true
|
|
25
25
|
elsif (match = /\AOSPF Process ID (\S+) VRF (\S+)\z/i.match(line))
|
|
26
|
-
scope = {
|
|
26
|
+
scope = { process_id: match[1], vrf: match[2] }
|
|
27
27
|
elsif (match = /\ATotal number of neighbors:\s*(\d+)\z/i.match(line))
|
|
28
28
|
totals << match[1].to_i
|
|
29
29
|
elsif (match = ROW.match(line))
|
|
30
30
|
row = FIELDS.zip(match.captures).to_h.merge(scope)
|
|
31
31
|
row[:priority] = row[:priority].to_i
|
|
32
|
-
unless ip_address?(row[:
|
|
32
|
+
unless ip_address?(row[:router_id]) && ip_address?(row[:neighbor_address]) && (0..255).cover?(row[:priority]) &&
|
|
33
33
|
row[:state].match?(/\A(?:DOWN|ATTEMPT|INIT|2WAY|EXSTART|EXCHANGE|LOADING|FULL)\/(?:\s*(?:DR|BDR|DROTHER|-))\z/i)
|
|
34
34
|
unparsed_records(number)
|
|
35
35
|
next
|
|
36
36
|
end
|
|
37
|
-
|
|
37
|
+
state, role = row[:state].downcase.delete(" ").split("/", 2)
|
|
38
|
+
@rows << row.merge(state: state == "2way" ? :two_way : state.to_sym,
|
|
39
|
+
neighbor_role: role == "-" ? :unknown : role.to_sym)
|
|
38
40
|
else
|
|
39
41
|
unparsed_records(number)
|
|
40
42
|
end
|
data/lib/vxlan/cli/apply.rb
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ctx_cli/
|
|
4
|
-
require "ctx_cli/terminal"
|
|
5
|
-
require "ctx_cli/input"
|
|
6
|
-
require "ctx_cli/output"
|
|
7
|
-
require "json"
|
|
3
|
+
require "ctx_cli/apply"
|
|
8
4
|
require "ctx_runner/document"
|
|
9
|
-
require "
|
|
5
|
+
require "ctx_runner/evidence"
|
|
10
6
|
require "ctx_runner/execution"
|
|
11
|
-
require "ipaddr"
|
|
12
7
|
require "vxlan"
|
|
13
8
|
require_relative "report"
|
|
14
9
|
require_relative "../change_plan"
|
|
@@ -35,100 +30,44 @@ module Vxlan
|
|
|
35
30
|
# 每次只发一条命令;失败立即交还宿主,原始输出不进入终端和日志报告。
|
|
36
31
|
def write(command)
|
|
37
32
|
result = @native.execute_command(command.strip, output_sensitive: true)
|
|
38
|
-
raise IOError, "Device command is unconfirmed" unless result.success? && result.steps.one?
|
|
33
|
+
raise IOError, "Device command is unconfirmed" unless result.success? && result.steps.one? && result.steps.first.command.text == command.strip
|
|
39
34
|
true
|
|
40
35
|
end
|
|
41
36
|
end
|
|
42
37
|
|
|
43
|
-
#
|
|
38
|
+
# 领域适配器只维护计划类型、凭据来源与独占设备会话。
|
|
44
39
|
class Apply
|
|
45
|
-
# 注入连接工厂仅用于宿主集成和离线失败测试。
|
|
46
40
|
def initialize(env: ENV, out: $stdout, error: $stderr, input: $stdin, connector_factory: nil, discovery: {})
|
|
47
|
-
@env, @
|
|
48
|
-
@
|
|
49
|
-
@output = CtxCli::Output.new(out: out, error: error)
|
|
41
|
+
@env, @factory, @discovery = env, connector_factory, discovery
|
|
42
|
+
@entry = CtxCli::Apply.new(adapter: self, execution: CtxRunner::Execution, name: "vxlan", out: out, error: error, input: input)
|
|
50
43
|
end
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
def run(argv)
|
|
54
|
-
@store, @execution = nil, nil
|
|
55
|
-
command = CtxCli::Command.new(banner: "Usage: vxlan apply --plan FILE --item ID [--env-file FILE] (requires explicit confirmation in an interactive terminal)",
|
|
56
|
-
defaults: { directory: File.join(Dir.pwd, "vxlan-executions"), port: 22 }, out: @out) do |p, options|
|
|
57
|
-
p.on("--plan FILE", "Batch plan.json; requires a SHA256 checksum file in the same directory") { |value| options[:plan] = value }
|
|
58
|
-
p.on("--item ID", "Explicitly select one change from the batch plan; bulk execution is prohibited") { |value| options[:item] = value }
|
|
59
|
-
p.on("--env-file FILE", "Explicit credentials file; process environment takes precedence") { |value| options[:env_file] = value }
|
|
60
|
-
p.on("--directory DIR", "Private execution log directory") { |value| options[:directory] = value }
|
|
61
|
-
p.on("--port N", Integer, "SSH port, default 22") { |value| options[:port] = value }
|
|
62
|
-
p.on("-h", "--help", "Show help without connecting to devices") { options[:help] = true }
|
|
63
|
-
end
|
|
64
|
-
options = command.parse(argv)
|
|
65
|
-
return 0 if command.informational?
|
|
66
|
-
terminal = CtxCli::Terminal.new(input: @input, out: @out)
|
|
67
|
-
terminal.require_tty!(@error)
|
|
68
|
-
raise ArgumentError, "A single plan is required" unless options[:plan] && options[:port].between?(1, 65_535)
|
|
69
|
-
body = CtxCli::Input.read(path: options[:plan])
|
|
70
|
-
hash = Digest::SHA256.hexdigest(body)
|
|
71
|
-
plan = CtxParser::Immutable.copy(select_plan(JSON.parse(body), options, hash))
|
|
72
|
-
change = validate_plan(plan)
|
|
73
|
-
host = plan.fetch("device").fetch("host")
|
|
74
|
-
@out.puts "Target: #{plan['device']['name']}(#{host}); #{plan['scope']}; #{plan['title']}"
|
|
75
|
-
@out.puts "Impact: #{plan['impact']}"
|
|
76
|
-
@out.puts Array(plan["prerequisites"]).map { |value| "Prerequisite: #{value}" }
|
|
77
|
-
@out.puts "Plan SHA256: #{hash}\nProposed commands:\n#{change.commands.join("\n")}\nConditional rollback (not executed automatically):\n#{change.rollback.join("\n")}"
|
|
78
|
-
token = "APPLY #{host} #{hash[0, 12]} #{options[:item]}"
|
|
79
|
-
confirmed = terminal.confirm(token: token, prompt: "After confirming the service policy and maintenance window, enter #{token}; any other input cancels: ")
|
|
80
|
-
return @out.puts("Cancelled; no device connection opened.") || 1 unless confirmed
|
|
81
|
-
settings = credentials(options)
|
|
82
|
-
@store = Report.new(options[:directory])
|
|
83
|
-
@execution = CtxRunner::Execution.new(plan: plan, change: change, journal: @store, plan_sha256: hash, item: options[:item])
|
|
84
|
-
@execution.run { |operation| with_session(plan["device"], settings, &operation) }
|
|
85
|
-
@out.puts "Configuration readback confirmed; service acceptance is still required. Configuration was not saved to startup configuration. Log: #{@store.directory}/execution.json"
|
|
86
|
-
0
|
|
87
|
-
rescue Interrupt
|
|
88
|
-
report_journal_failure
|
|
89
|
-
@output.diagnostic "Operation interrupted; #{@execution&.started? ? 'Device effects may be incomplete; read back and verify before proceeding. Do not replay commands.' : 'No service configuration was sent.'}"
|
|
90
|
-
130
|
|
91
|
-
rescue StandardError, LoadError => error
|
|
92
|
-
report_journal_failure
|
|
93
|
-
@output.diagnostic "Apply stopped (#{error.class.name}); #{@execution&.started? ? 'Effects are not fully confirmed; read back first. No automatic replay or rollback.' : 'No service configuration was sent; check the terminal, plan, credentials and current configuration for consistency.'}"
|
|
94
|
-
2
|
|
95
|
-
end
|
|
45
|
+
def run(argv) = @entry.run(argv)
|
|
96
46
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# 聚合文件必须校验哈希并选中唯一动作;绝不隐式选择第一项或循环执行。
|
|
100
|
-
def select_plan(document, options, hash)
|
|
101
|
-
id = options[:item]
|
|
102
|
-
unless document.is_a?(Hash) && document["schema_version"] == 2 && document["platform"] == "cisco_nxos" && document["changes"].is_a?(Hash) &&
|
|
103
|
-
id.is_a?(String) && id.match?(/\A[A-Za-z0-9_.-]+\z/) && document["changes"].key?(id)
|
|
104
|
-
raise ArgumentError, "A batch plan and valid --item are required"
|
|
105
|
-
end
|
|
106
|
-
raise ArgumentError, "Plan checksum verification failed" unless CtxCli::Input.read(path: "#{options[:plan]}.sha256", max_bytes: 128).strip == hash
|
|
107
|
-
document["changes"].fetch(id)
|
|
47
|
+
def select_plan(document, item:)
|
|
48
|
+
CtxRunner::Evidence.plan(document, item: item, platform: "cisco_nxos")
|
|
108
49
|
end
|
|
109
50
|
|
|
110
|
-
#
|
|
51
|
+
# 从类型化增量重新生成命令,拒绝修改后的任意脚本。
|
|
111
52
|
def validate_plan(plan)
|
|
112
|
-
raise ArgumentError, "Plan type mismatch" unless plan.is_a?(Hash) && plan["schema_version"] == 1 && plan["platform"] == "cisco_nxos"
|
|
113
|
-
host = plan.fetch("device").fetch("host")
|
|
114
|
-
raise ArgumentError, "Invalid target address" unless host.is_a?(String) && !host.include?("/") && IPAddr.new(host).to_s == host
|
|
115
|
-
raise ArgumentError, "Invalid configuration fingerprint" unless plan["expected_config_sha256"].is_a?(String) && plan["expected_config_sha256"].match?(/\A[0-9a-f]{64}\z/)
|
|
116
53
|
change = ChangePlan.build(plan.fetch("change"))
|
|
117
|
-
|
|
54
|
+
unless change.commands == plan["commands"] && change.rollback == plan["rollback"]
|
|
55
|
+
raise ArgumentError, "Plan commands were modified"
|
|
56
|
+
end
|
|
118
57
|
change
|
|
119
58
|
end
|
|
120
59
|
|
|
121
|
-
# 共用分层环境加载器,显式文件最高、进程环境优先;凭据不进入计划或日志。
|
|
122
60
|
def credentials(options)
|
|
123
61
|
require "net/connector/config"
|
|
62
|
+
require "net/connector/credentials"
|
|
124
63
|
env = Net::Connector::Config.new(name: "vxlan", env: @env, env_file: options[:env_file], **@discovery).load_environment.environment
|
|
125
|
-
|
|
126
|
-
raise ArgumentError, "Device username is missing" unless
|
|
127
|
-
|
|
128
|
-
port: options[:port], host_key_policy: "strict", command_timeout: 60, login_timeout: 15 }
|
|
64
|
+
credentials = Net::Connector::Credentials.new(env: env).resolve(platform: :cisco_nxos)
|
|
65
|
+
raise ArgumentError, "Device username is missing" unless credentials
|
|
66
|
+
credentials.merge(port: options[:port], host_key_policy: "strict", command_timeout: 60, login_timeout: 15)
|
|
129
67
|
end
|
|
130
68
|
|
|
131
|
-
|
|
69
|
+
def report(options) = Report.new(options.fetch(:directory))
|
|
70
|
+
|
|
132
71
|
def with_session(device, settings, &operation)
|
|
133
72
|
return @factory.call(device, settings, &operation) if @factory
|
|
134
73
|
require "net/connector"
|
|
@@ -137,13 +76,9 @@ module Vxlan
|
|
|
137
76
|
end
|
|
138
77
|
end
|
|
139
78
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@output.diagnostic "Execution log finalization is unconfirmed; existing checkpoints retained. Do not replay configuration based on this state."
|
|
144
|
-
rescue Interrupt
|
|
145
|
-
nil
|
|
146
|
-
end
|
|
79
|
+
def inspect = "#<#{self.class.name}>"
|
|
80
|
+
alias to_s inspect
|
|
81
|
+
def pretty_print(printer) = printer.text(inspect)
|
|
147
82
|
end
|
|
148
83
|
end
|
|
149
84
|
end
|
data/lib/vxlan/cli/collect.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Vxlan
|
|
|
34
34
|
def load_network
|
|
35
35
|
require "net/connector"
|
|
36
36
|
rescue LoadError
|
|
37
|
-
raise LoadError, "Online collection requires net-connector >= 0.
|
|
37
|
+
raise LoadError, "Online collection requires net-connector >= 0.20.0; add it to the host Gemfile or run gem install net-connector", cause: nil
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# YAML 静态设备或显式 Netdisco 来源二选一;选中地址的所有冲突记录在连接前拒绝。
|
data/lib/vxlan/cli/options.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "optparse"
|
|
|
6
6
|
require "ctx_cli/options"
|
|
7
7
|
require "ctx_cli/terminal"
|
|
8
8
|
require "ctx_cli/initializer"
|
|
9
|
-
gem "net-connector", "~> 0.
|
|
9
|
+
gem "net-connector", "~> 0.20.0"
|
|
10
10
|
require "net/connector/inventory/filters"
|
|
11
11
|
require "yaml"
|
|
12
12
|
require "ipaddr"
|
|
@@ -67,7 +67,7 @@ module Vxlan
|
|
|
67
67
|
@environment = @files.environment
|
|
68
68
|
@config, @env_file = @files.config, @files.env_file
|
|
69
69
|
rescue LoadError
|
|
70
|
-
raise LoadError, "Collection configuration requires net-connector >= 0.
|
|
70
|
+
raise LoadError, "Collection configuration requires net-connector >= 0.20.0; reading .env also requires dotenv", cause: nil
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# 公共与专用文件先合并,进程值和 CLI 最后覆盖;数组不追加设备。
|
data/lib/vxlan/cli/report.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "time"
|
|
5
5
|
require "ctx_runner/report"
|
|
6
6
|
require "ctx_runner/document"
|
|
7
|
+
require "ctx_runner/evidence"
|
|
7
8
|
require_relative "review/report"
|
|
8
9
|
require_relative "review/fabric"
|
|
9
10
|
|
|
@@ -37,7 +38,7 @@ module Vxlan
|
|
|
37
38
|
name = self.class.device_basename(device)
|
|
38
39
|
# 保存状态属于批次宿主;不能让正在发布的文件预先声明自身已保存。
|
|
39
40
|
body = result.reject { |key, _| key == :report_saved }.merge(snapshot: snapshot && snapshot.to_h)
|
|
40
|
-
document = CtxRunner::
|
|
41
|
+
document = CtxRunner::Evidence.wrap(body, kind: "collection")
|
|
41
42
|
advice = report_advice(Recommendations.new(document).call)
|
|
42
43
|
document = document.merge("recommendations" => advice)
|
|
43
44
|
json = JSON.pretty_generate(document) + "\n"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ctx_runner/evidence"
|
|
3
4
|
require "digest"
|
|
4
5
|
require "ctx_runner/fingerprint"
|
|
5
6
|
require_relative "../../recommendations"
|
|
@@ -42,7 +43,7 @@ module Vxlan
|
|
|
42
43
|
change = plan.to_h
|
|
43
44
|
id = [ basename, change["kind"], "vlan#{change['vlan']}", change["direction"], CtxRunner::Fingerprint.call(change)[0, 12] ].compact.join("--")
|
|
44
45
|
config_sha256 ||= CtxRunner::Fingerprint.call(config)
|
|
45
|
-
entry[:changes][id] = { schema_version:
|
|
46
|
+
entry[:changes][id] = { schema_version: CtxRunner::Evidence::VERSION, kind: "change", platform: "cisco_nxos", device: document.fetch("device"), expected_config_sha256: config_sha256,
|
|
46
47
|
source: source, source_sha256: source_sha256, change: change, commands: plan.commands, rollback: plan.rollback,
|
|
47
48
|
scope: item[:scope], title: item[:title], prerequisites: item[:prerequisites], impact: item[:impact], verify: item[:verify] }
|
|
48
49
|
item[:plan_id] = id
|
|
@@ -65,10 +66,10 @@ module Vxlan
|
|
|
65
66
|
def finish_review(entries, source_batch:, standalone: true)
|
|
66
67
|
counts = entries.each_with_object(Hash.new(0)) { |entry, total| entry[:counts].each { |key, count| total[key] += count } }
|
|
67
68
|
public_entries = entries.map { |entry| entry.reject { |key, _| %i[advice changes].include?(key) } }
|
|
68
|
-
summary = { layout_version: 3, mode: "review", status: "incomplete", review_only: true, devices: entries.size, counts: counts, source_batch: source_batch,
|
|
69
|
+
summary = { schema_version: CtxRunner::Evidence::VERSION, kind: "review", layout_version: 3, mode: "review", status: "incomplete", review_only: true, devices: entries.size, counts: counts, source_batch: source_batch,
|
|
69
70
|
reports: public_entries, plan: "plan.json", plan_sha256: nil, fabric_findings: @fabric_audit ? @fabric_audit[:findings].size : 0 }
|
|
70
71
|
checkpoint("summary.json", summary) if standalone
|
|
71
|
-
plan = { schema_version:
|
|
72
|
+
plan = { schema_version: CtxRunner::Evidence::VERSION, kind: "change_plan", platform: "cisco_nxos", mode: "review", devices: public_entries, audits: entries.map { |entry| entry[:advice] },
|
|
72
73
|
changes: entries.each_with_object({}) { |entry, all| all.merge!(entry[:changes]) }, fabric: @fabric_audit && @fabric_audit.reject { |key, _| key == :device_items } }
|
|
73
74
|
body = JSON.pretty_generate(plan) + "\n"
|
|
74
75
|
summary[:plan_sha256] = Digest::SHA256.hexdigest(body)
|
|
@@ -113,7 +114,7 @@ module Vxlan
|
|
|
113
114
|
lines = [ "Device: #{device['name']} (#{device['host']})", "Mode: diagnostics and planning; no device configuration executed", "Evidence time: #{advice[:observed_at] || 'unknown'}", "Source: #{entry[:source]}", "Source file SHA256: #{entry[:source_sha256]}",
|
|
114
115
|
"Recommendations: #{entry[:counts]}", "Command file: #{entry[:script] || 'not generated'}", "Command SHA256: #{entry[:script_sha256] || 'none'}", "Plan file: plan.json; interactive plan items: #{entry[:plans].size} items", "" ]
|
|
115
116
|
lines << "Collection status: #{document['status']}" if document["status"]
|
|
116
|
-
configuration_time = document.dig("snapshot", "
|
|
117
|
+
configuration_time = document.dig("snapshot", "observations", "configuration", "evidence", 0, "observed_at")
|
|
117
118
|
lines << "Configuration evidence time: #{configuration_time}" if configuration_time
|
|
118
119
|
lines << "No current rules matched; this does not establish network health." if advice[:items].empty?
|
|
119
120
|
advice[:items].each_with_index do |item, index|
|
data/lib/vxlan/cli/review.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Vxlan
|
|
|
53
53
|
|
|
54
54
|
# 先校验公共证据外壳,再验证领域记录。
|
|
55
55
|
def validate(document)
|
|
56
|
-
CtxRunner::Evidence.validate(document) { |record, dimension| validate_record(record, dimension) }
|
|
56
|
+
CtxRunner::Evidence.validate(document, names: CiscoNxos::Profile::COMMANDS.keys) { |record, dimension| validate_record(record, dimension) }
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# 在输出目录创建前验证所有会参与建议生成的嵌套字段。
|
data/lib/vxlan/collector.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Vxlan
|
|
|
21
21
|
|
|
22
22
|
# 通过调用方持有的连接器顺序采集请求维度。
|
|
23
23
|
def collect(connector:, target:, fabric_key:, model: nil, dimensions: COMMANDS.keys)
|
|
24
|
+
target = CtxRunner::Target.snapshot(target)
|
|
24
25
|
target, fabric_key, dimensions, profile = request(target, fabric_key, dimensions, connector.platform, model)
|
|
25
26
|
|
|
26
27
|
observations = {}
|
|
@@ -29,9 +30,8 @@ module Vxlan
|
|
|
29
30
|
if collection.failure
|
|
30
31
|
code = collection.failure.fetch(:code)
|
|
31
32
|
dimensions.each { |name| observations[name] ||= failure(name, code) }
|
|
32
|
-
observations[:collection] = failure(:collection, code)
|
|
33
33
|
end
|
|
34
|
-
snapshot(target, fabric_key, observations)
|
|
34
|
+
snapshot(target, fabric_key, observations, issues: collection.failure ? [ collection.failure ] : [])
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# 借用已打开的会话;调用方负责连接和关闭,本方法不重连、不提权。
|
|
@@ -44,7 +44,10 @@ module Vxlan
|
|
|
44
44
|
|
|
45
45
|
# 连接前固定请求数据,生命周期入口和借用会话入口共用相同校验。
|
|
46
46
|
def request(target, fabric_key, dimensions, platform, model)
|
|
47
|
-
|
|
47
|
+
target = CtxRunner::Target.snapshot(target)
|
|
48
|
+
unless fabric_key.is_a?(String) && !fabric_key.strip.empty? && !/[\x00-\x1f\x7f]/.match?(fabric_key)
|
|
49
|
+
raise ArgumentError, "Fabric identity must be nonempty text"
|
|
50
|
+
end
|
|
48
51
|
unless dimensions.is_a?(Array) && dimensions.any? && dimensions.uniq == dimensions && (dimensions - COMMANDS.keys).empty?
|
|
49
52
|
raise ArgumentError, "Unknown or duplicate VXLAN dimensions"
|
|
50
53
|
end
|
|
@@ -60,7 +63,6 @@ module Vxlan
|
|
|
60
63
|
reader = CtxDriver::Reader.new(session: session, clock: @clock)
|
|
61
64
|
if (code = prepare_terminal(reader))
|
|
62
65
|
dimensions.each { |name| observations[name] = failure(name, code) }
|
|
63
|
-
observations[:collection] = failure(:collection, code)
|
|
64
66
|
return observations
|
|
65
67
|
end
|
|
66
68
|
ordered = dimensions.include?(:configuration) ? [ :configuration, *dimensions.reject { |name| name == :configuration } ] : dimensions
|
|
@@ -76,16 +78,8 @@ module Vxlan
|
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
# 汇总证据完整性,不将查询成功解释为 Fabric 健康。
|
|
79
|
-
def snapshot(target, fabric_key, observations)
|
|
80
|
-
|
|
81
|
-
:complete
|
|
82
|
-
elsif observations.values.any? { |value| %i[complete partial].include?(value.status) }
|
|
83
|
-
:partial
|
|
84
|
-
else
|
|
85
|
-
:failed
|
|
86
|
-
end
|
|
87
|
-
observations[:collection] ||= Observation.new(name: :collection, status: status, completed_at: @clock.call)
|
|
88
|
-
Snapshot.new(target: target, fabric_key: fabric_key, observations: observations, observed_at: @clock.call)
|
|
81
|
+
def snapshot(target, fabric_key, observations, issues: [])
|
|
82
|
+
Snapshot.new(target: target, fabric_key: fabric_key, observations: observations, observed_at: @clock.call, issues: issues)
|
|
89
83
|
end
|
|
90
84
|
|
|
91
85
|
# 采集设备回显前,禁用分页并归一化终端宽度。
|
|
@@ -6,7 +6,7 @@ module Vxlan
|
|
|
6
6
|
module Evidence
|
|
7
7
|
# 外层采集成功不能覆盖记录层的部分解析或多份配置歧义。
|
|
8
8
|
def self.complete_record(document)
|
|
9
|
-
observation = document&.dig("snapshot", "
|
|
9
|
+
observation = document&.dig("snapshot", "observations", "configuration")
|
|
10
10
|
return unless observation && observation["status"] == "complete"
|
|
11
11
|
records = observation["records"]
|
|
12
12
|
return unless records.is_a?(Array) && records.one?
|
data/lib/vxlan/fabric/review.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ctx_runner/evidence"
|
|
3
4
|
require "ipaddr"
|
|
4
5
|
require "json"
|
|
5
6
|
require "digest"
|
|
@@ -43,7 +44,7 @@ module Vxlan
|
|
|
43
44
|
@role = nil
|
|
44
45
|
review_subnets(fabric, configs)
|
|
45
46
|
end
|
|
46
|
-
{ schema_version:
|
|
47
|
+
{ schema_version: CtxRunner::Evidence::VERSION, kind: "fabric_review", scope: "same_role_same_deployed_vni", inventory: inventory, findings: @findings, device_items: @items.to_h, coverage: @coverage }
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ctx_runner/evidence"
|
|
3
4
|
require_relative "cisco_nxos/profile"
|
|
4
5
|
require_relative "config/evidence"
|
|
5
6
|
|
|
@@ -35,7 +36,7 @@ module Vxlan
|
|
|
35
36
|
# 输入为报告 JSON 的字符串键结构,离线与实时报告共用此边界。
|
|
36
37
|
def initialize(document)
|
|
37
38
|
@document = document
|
|
38
|
-
@dimensions = document.dig("snapshot", "
|
|
39
|
+
@dimensions = document.dig("snapshot", "observations") || {}
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# 建议与采集完整性分开;没有命中规则并不代表网络健康。
|
|
@@ -47,7 +48,7 @@ module Vxlan
|
|
|
47
48
|
items.concat(undefined_acl_items(config))
|
|
48
49
|
end
|
|
49
50
|
items.concat(runtime_tasks)
|
|
50
|
-
{ schema_version:
|
|
51
|
+
{ schema_version: CtxRunner::Evidence::VERSION, kind: "recommendations", review_only: true, health_assessment: "not_performed", device: @document["device"],
|
|
51
52
|
observed_at: @document.dig("snapshot", "observed_at"), items: items, counts: items.map { |item| item[:status] }.tally }
|
|
52
53
|
end
|
|
53
54
|
|
data/lib/vxlan/snapshot.rb
CHANGED
|
@@ -1,44 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require "ctx_runner/snapshot"
|
|
4
5
|
require_relative "observation"
|
|
5
6
|
require_relative "parser_registry"
|
|
6
7
|
|
|
7
8
|
module Vxlan
|
|
8
|
-
#
|
|
9
|
-
Snapshot
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
# 领域组合公共快照,身份、维度、时间及采集结论共用同一契约。
|
|
10
|
+
class Snapshot
|
|
11
|
+
include CtxRunner::Document::Value
|
|
12
|
+
extend Forwardable
|
|
13
|
+
def_delegators :@snapshot, :target, :observations, :observed_at, :issues, :collection_status,
|
|
14
|
+
:observation, :complete?, :same_target?
|
|
15
|
+
attr_reader :fabric_key
|
|
16
|
+
|
|
17
|
+
def initialize(target:, fabric_key:, observations:, observed_at:, issues: [])
|
|
18
|
+
unless fabric_key.is_a?(String) && !fabric_key.strip.empty? && !/[\x00-\x1f\x7f]/.match?(fabric_key)
|
|
19
|
+
raise ArgumentError, "Fabric identity must be nonempty text"
|
|
14
20
|
end
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# 未请求的已知维度保留明确状态;未知维度属于调用错误。
|
|
21
|
-
def observation(name)
|
|
22
|
-
unless name == :collection || CiscoNxos::Profile::COMMANDS.key?(name)
|
|
23
|
-
raise ArgumentError, "Unknown VXLAN dimension"
|
|
24
|
-
end
|
|
25
|
-
observations.fetch(name) { Observation.new(name: name, status: :not_requested) }
|
|
21
|
+
@fabric_key = fabric_key.dup.freeze
|
|
22
|
+
@snapshot = CtxRunner::Snapshot.new(target: target, observations: observations, observed_at: observed_at,
|
|
23
|
+
names: CiscoNxos::Profile::COMMANDS.keys, issues: issues)
|
|
24
|
+
freeze
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
# 判断所有已选事实是否都有完整证据支持。
|
|
32
|
-
def complete?
|
|
33
|
-
selected = observations.values.reject { |dimension| dimension.status == :not_requested }
|
|
34
|
-
selected.any? && selected.all?(&:complete?)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
38
|
-
def inspect = "#<#{self.class.name} fabric=#{fabric_key.inspect} complete=#{complete?}>"
|
|
27
|
+
def to_h = @snapshot.to_h.merge(fabric_key: fabric_key)
|
|
28
|
+
def inspect = "#<#{self.class.name} collection_status=#{collection_status}>"
|
|
39
29
|
alias to_s inspect
|
|
40
|
-
|
|
41
|
-
# PP 默认展开 Data 成员,显式沿用脱敏展示。
|
|
42
30
|
def pretty_print(printer) = printer.text(inspect)
|
|
43
31
|
end
|
|
44
32
|
end
|
data/lib/vxlan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vxlan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -10,7 +10,7 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: forwardable
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
@@ -23,66 +23,86 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ctx_cli
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.4.0
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '0.5'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 0.4.0
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0.5'
|
|
26
46
|
- !ruby/object:Gem::Dependency
|
|
27
47
|
name: ctx_runner
|
|
28
48
|
requirement: !ruby/object:Gem::Requirement
|
|
29
49
|
requirements:
|
|
30
50
|
- - ">="
|
|
31
51
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
52
|
+
version: 0.4.0
|
|
33
53
|
- - "<"
|
|
34
54
|
- !ruby/object:Gem::Version
|
|
35
|
-
version: '0.
|
|
55
|
+
version: '0.5'
|
|
36
56
|
type: :runtime
|
|
37
57
|
prerelease: false
|
|
38
58
|
version_requirements: !ruby/object:Gem::Requirement
|
|
39
59
|
requirements:
|
|
40
60
|
- - ">="
|
|
41
61
|
- !ruby/object:Gem::Version
|
|
42
|
-
version: 0.
|
|
62
|
+
version: 0.4.0
|
|
43
63
|
- - "<"
|
|
44
64
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: '0.
|
|
65
|
+
version: '0.5'
|
|
46
66
|
- !ruby/object:Gem::Dependency
|
|
47
67
|
name: ctx_parser
|
|
48
68
|
requirement: !ruby/object:Gem::Requirement
|
|
49
69
|
requirements:
|
|
50
70
|
- - ">="
|
|
51
71
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: 0.
|
|
72
|
+
version: 0.4.0
|
|
53
73
|
- - "<"
|
|
54
74
|
- !ruby/object:Gem::Version
|
|
55
|
-
version: '0.
|
|
75
|
+
version: '0.5'
|
|
56
76
|
type: :runtime
|
|
57
77
|
prerelease: false
|
|
58
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
59
79
|
requirements:
|
|
60
80
|
- - ">="
|
|
61
81
|
- !ruby/object:Gem::Version
|
|
62
|
-
version: 0.
|
|
82
|
+
version: 0.4.0
|
|
63
83
|
- - "<"
|
|
64
84
|
- !ruby/object:Gem::Version
|
|
65
|
-
version: '0.
|
|
85
|
+
version: '0.5'
|
|
66
86
|
- !ruby/object:Gem::Dependency
|
|
67
87
|
name: ctx_driver
|
|
68
88
|
requirement: !ruby/object:Gem::Requirement
|
|
69
89
|
requirements:
|
|
70
90
|
- - ">="
|
|
71
91
|
- !ruby/object:Gem::Version
|
|
72
|
-
version: 0.
|
|
92
|
+
version: 0.4.0
|
|
73
93
|
- - "<"
|
|
74
94
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0.
|
|
95
|
+
version: '0.5'
|
|
76
96
|
type: :runtime
|
|
77
97
|
prerelease: false
|
|
78
98
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
99
|
requirements:
|
|
80
100
|
- - ">="
|
|
81
101
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: 0.
|
|
102
|
+
version: 0.4.0
|
|
83
103
|
- - "<"
|
|
84
104
|
- !ruby/object:Gem::Version
|
|
85
|
-
version: '0.
|
|
105
|
+
version: '0.5'
|
|
86
106
|
- !ruby/object:Gem::Dependency
|
|
87
107
|
name: digest
|
|
88
108
|
requirement: !ruby/object:Gem::Requirement
|