ctx_driver 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +22 -0
- data/README-zh.md +108 -0
- data/README.md +106 -0
- data/docs/architecture-zh.md +45 -0
- data/docs/release.md +13 -0
- data/examples/offline.rb +34 -0
- data/lib/ctx_driver/capture.rb +36 -0
- data/lib/ctx_driver/failure.rb +33 -0
- data/lib/ctx_driver/net_connector_session.rb +45 -0
- data/lib/ctx_driver/reader.rb +100 -0
- data/lib/ctx_driver/version.rb +6 -0
- data/lib/ctx_driver.rb +8 -0
- metadata +100 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b1f792a12c386d5fdef44c86c9743d2f916ad1c654d552dccb22158710ef0025
|
|
4
|
+
data.tar.gz: afe56046cd35730cc233bafd0de69cd25886cec841c65aa379557b1ea0287302
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d289d193573088f0524ce654497fa3b6eb010b0829a66c89fd327a8bb5a89a8417f0d2a0329be0c404438a7dc33e49bcf33d917f62529c7eea1841c6d0457f54
|
|
7
|
+
data.tar.gz: 77b1a08cb2e1db7884017d030587cf679f0437bdb71109ded4e352335c0509effca89e9f9e97382b560bbaf5cf50abe5179622fad781a691a848c3916c48432d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Borrowed-session command execution, immutable captures, safe diagnostics, and failure isolation.
|
|
10
|
+
- Standalone RubyGems packaging under `ctx_driver` with the `ctx_driver` require path and `CtxDriver` namespace.
|
|
11
|
+
- English and Chinese documentation, an offline example, and standalone RSpec/RuboCop development tasks.
|
|
12
|
+
- Linux/macOS CI, isolated package verification, and automatic RubyGems and GitHub Releases with identical artifacts.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Adapt native net-connector sessions without implicit reconnect or privilege escalation; preserve explicit session failure semantics.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Cross-module dependencies resolve through RubyGems/Bundler instead of relative sibling paths.
|
|
21
|
+
- Motor persistence and application-specific adapters remain in the host application.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Song Huang
|
|
4
|
+
Copyright (c) 2026 Gatework contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/README-zh.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# ctx_driver
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [源码](https://github.com/gatework/ctx_driver) | [变更记录](CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
借用网络会话的单命令执行边界,验证连接器回执、保存安全证据,并在会话不可用后保留首个原因,将后续命令明确标记为跳过。
|
|
6
|
+
|
|
7
|
+
## 能力
|
|
8
|
+
|
|
9
|
+
- `Reader` 通过注入会话逐条执行已声明命令。
|
|
10
|
+
- `Capture` 将命令正文与安全元数据、指纹分开。
|
|
11
|
+
- 单命令拒绝后,已确认同步的会话可以继续执行其他查询。
|
|
12
|
+
- 会话断开后停止提交,不自动重连或重放。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
要求 **Ruby 4.0.6 或更新版本**。在应用的 `Gemfile` 中添加:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "ctx_driver"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
然后执行 `bundle install`;也可直接执行 `gem install ctx_driver`。Ruby 加载入口为 `ctx_driver`,公开命名空间为 `CtxDriver`;gem 包名与 Ruby 命名空间分别维护。
|
|
23
|
+
|
|
24
|
+
模块运行依赖:[`ctx_parser`](https://github.com/gatework/ctx_parser)。 gemspec 的运行依赖不添加版本约束;应用应提交自己的 `Gemfile.lock`,保持部署可复现。使用本库不需要启动 Rails 或配置兄弟模块的本地路径。
|
|
25
|
+
|
|
26
|
+
发布产物和校验值见 [GitHub Releases](https://github.com/gatework/ctx_driver/releases),已发布版本以 [RubyGems](https://rubygems.org/gems/ctx_driver) 为准。
|
|
27
|
+
|
|
28
|
+
## 离线示例
|
|
29
|
+
|
|
30
|
+
在开发检出目录安装依赖后执行:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bundle exec ruby examples/offline.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
示例使用合成数据,不访问网络。完整代码见 [examples/offline.rb](examples/offline.rb):
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "ctx_driver"
|
|
40
|
+
|
|
41
|
+
# 用本地替身表示单条连接器结果,不打开套接字。
|
|
42
|
+
ExampleResponse = Data.define(:output, :steps) do
|
|
43
|
+
# 确认本例合成命令已成功完成。
|
|
44
|
+
def success? = true
|
|
45
|
+
|
|
46
|
+
# 返回已去除终端修饰的命令正文。
|
|
47
|
+
def value! = output
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 借用会话示例仅实现 Reader 所需的传输协议。
|
|
51
|
+
class ExampleSession
|
|
52
|
+
# 本演示会话始终处于可用状态。
|
|
53
|
+
def connected? = true
|
|
54
|
+
|
|
55
|
+
# 接受声明的查询并返回单条合成命令结果。
|
|
56
|
+
def send_commands(commands)
|
|
57
|
+
raise ArgumentError, "Unexpected query" unless commands == [ "show interfaces" ]
|
|
58
|
+
|
|
59
|
+
ExampleResponse.new(output: "Ethernet1/1 is up\n", steps: [ { command: commands.first } ])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
capture = CtxDriver::Reader.new(session: ExampleSession.new).read("show interfaces")
|
|
64
|
+
abort "Capture failed: #{capture.failure[:code]}" unless capture.success?
|
|
65
|
+
|
|
66
|
+
puts "Capture status: success"
|
|
67
|
+
puts "Command: #{capture.command}"
|
|
68
|
+
puts "Bytes: #{capture.evidence.first[:bytes]}"
|
|
69
|
+
puts "SHA-256: #{capture.evidence.first[:sha256]}"
|
|
70
|
+
# 通过 capture.body 显式访问正文;默认序列化不包含正文。
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 公开契约与限制
|
|
74
|
+
|
|
75
|
+
`Reader.new(session:, clock:).read(command)` 返回 `Capture`。通过 `success?`、`skipped?`、`failure`、`evidence` 检查结果;正文仅显式通过 `body` 读取,`to_h`、`as_json`、`inspect` 不包含正文。
|
|
76
|
+
|
|
77
|
+
自定义会话实现 `connected?` 和 `send_commands([command])`。回执提供 `output`、布尔 `success?`、数组 `steps`、成功正文 `value!` 及失败时的 `error`。成功读取必须只有一个完成步骤。只有会话显式声明 `preserve_session_on_rejection:` 关键字时才传入该选项。
|
|
78
|
+
|
|
79
|
+
单个 reader 顺序使用;跨设备并发分别创建 reader。连接、关闭、凭据和批准命令选择由宿主负责,Reader 不判断任意命令是否只读。普通执行/回执异常转为安全诊断,取消信号继续传播。
|
|
80
|
+
|
|
81
|
+
详细厂商语义、历史验证记录和 Motor 专用集成说明保留在[架构参考](docs/architecture-zh.md)。其中宿主集成代码需要 Motor,不属于独立 gem API。适配层位于 [Motor 仓库](https://github.com/gatework/motor/tree/main/backend/lib) 的 `backend/lib/baseline`、`backend/lib/vxlan` 和 `backend/lib/wireless`。
|
|
82
|
+
|
|
83
|
+
原生 `Net::Connector::Base` 会话通过 `execute_operation` 自动适配,不连接、不提权。只去除已匹配的提示符和首行命令回显;连接器因拒绝命令而关闭时,后续读取跳过,不自动重连。自定义会话仍可实现 `send_commands` 协议。
|
|
84
|
+
|
|
85
|
+
## 开发与测试
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
bundle install
|
|
89
|
+
bundle exec rake
|
|
90
|
+
bundle exec ruby examples/offline.rb
|
|
91
|
+
bundle exec rake build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`bundle exec rake` 执行 RSpec、RuboCop 及离线示例。测试放在 `spec/`,沿用项目 RSpec 约定;样例采用合成或脱敏证据。新增解析格式时补充成功、不完整、不支持及非法输入回归。离线测试不代表所有设备版本均通过实机验证。
|
|
95
|
+
|
|
96
|
+
使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
|
|
97
|
+
|
|
98
|
+
## 发布
|
|
99
|
+
|
|
100
|
+
本目录可独立维护为 [ctx_driver](https://github.com/gatework/ctx_driver) 仓库。更新 `VERSION` 和 `CHANGELOG.md`,运行上述验证与构建命令,检查包内文件,再创建 `vVERSION` 标签。
|
|
101
|
+
|
|
102
|
+
CI 在 Linux、macOS 上执行测试、lint、示例、构建和隔离安装。`main` 的 CI 通过后推送 `vVERSION` 标签,工作流将已验证的 Linux 产物发布到 RubyGems 和 GitHub Release,附带 `SHA256SUMS` 并回读下载字节。GitHub `release` environment 可配置 `RUBYGEMS_API_KEY` secret;未配置时使用 RubyGems Trusted Publishing,绑定本仓库、`release.yml` 和 `release` environment。失败时重跑失败的发布 job,以复用原 CI 产物;同版本已存在时必须校验字节一致。详见[发布说明](docs/release.md)。
|
|
103
|
+
|
|
104
|
+
先发布依赖模块。标签、构建成功或工作流文件都不等于已经发布,需单独确认 RubyGems 的实际结果。版本变化见 [CHANGELOG.md](CHANGELOG.md)。
|
|
105
|
+
|
|
106
|
+
## 许可证
|
|
107
|
+
|
|
108
|
+
MIT,见 [LICENSE](LICENSE)。
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ctx_driver
|
|
2
|
+
|
|
3
|
+
[简体中文](README-zh.md) | [Source](https://github.com/gatework/ctx_driver) | [Changelog](CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
An atomic command-execution boundary for borrowed network sessions. It records safe evidence for one command, validates connector responses, and preserves the first unavailable-session reason across subsequent skipped commands.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- `Reader` sends one declared command at a time through an injected session.
|
|
10
|
+
- `Capture` separates command bodies from safe metadata and fingerprints.
|
|
11
|
+
- Independent command rejection can leave a synchronized session usable.
|
|
12
|
+
- Session loss stops further submissions without reconnecting or replaying.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Requires **Ruby 4.0.6 or newer**. Add this line to your application's `Gemfile`:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "ctx_driver"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run `bundle install`, or install directly with `gem install ctx_driver`. The require path is `ctx_driver` and the public namespace is `CtxDriver`; package names and Ruby namespaces are intentionally separate.
|
|
23
|
+
|
|
24
|
+
Internal runtime dependencies: [`ctx_parser`](https://github.com/gatework/ctx_parser). The gemspec declares runtime dependencies without version constraints. Applications should commit their own `Gemfile.lock` to make deployments reproducible. No Rails boot or sibling checkout paths are required.
|
|
25
|
+
|
|
26
|
+
Release artifacts and checksums are available from [GitHub Releases](https://github.com/gatework/ctx_driver/releases). Check [RubyGems](https://rubygems.org/gems/ctx_driver) for published versions.
|
|
27
|
+
|
|
28
|
+
## Offline example
|
|
29
|
+
|
|
30
|
+
From a development checkout after installing dependencies:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bundle exec ruby examples/offline.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The example uses synthetic data and performs no network access:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "ctx_driver"
|
|
40
|
+
|
|
41
|
+
# 用本地替身表示单条连接器结果,不打开套接字。
|
|
42
|
+
ExampleResponse = Data.define(:output, :steps) do
|
|
43
|
+
# 确认本例合成命令已成功完成。
|
|
44
|
+
def success? = true
|
|
45
|
+
|
|
46
|
+
# 返回已去除终端修饰的命令正文。
|
|
47
|
+
def value! = output
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 借用会话示例仅实现 Reader 所需的传输协议。
|
|
51
|
+
class ExampleSession
|
|
52
|
+
# 本演示会话始终处于可用状态。
|
|
53
|
+
def connected? = true
|
|
54
|
+
|
|
55
|
+
# 接受声明的查询并返回单条合成命令结果。
|
|
56
|
+
def send_commands(commands)
|
|
57
|
+
raise ArgumentError, "Unexpected query" unless commands == [ "show interfaces" ]
|
|
58
|
+
|
|
59
|
+
ExampleResponse.new(output: "Ethernet1/1 is up\n", steps: [ { command: commands.first } ])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
capture = CtxDriver::Reader.new(session: ExampleSession.new).read("show interfaces")
|
|
64
|
+
abort "Capture failed: #{capture.failure[:code]}" unless capture.success?
|
|
65
|
+
|
|
66
|
+
puts "Capture status: success"
|
|
67
|
+
puts "Command: #{capture.command}"
|
|
68
|
+
puts "Bytes: #{capture.evidence.first[:bytes]}"
|
|
69
|
+
puts "SHA-256: #{capture.evidence.first[:sha256]}"
|
|
70
|
+
# 通过 capture.body 显式访问正文;默认序列化不包含正文。
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Public contract and limits
|
|
74
|
+
|
|
75
|
+
`Reader.new(session:, clock:).read(command)` returns a `Capture`. Inspect `success?`, `skipped?`, `failure`, and `evidence`; access the body explicitly through `body`. `to_h`, `as_json`, and `inspect` omit the body.
|
|
76
|
+
|
|
77
|
+
A custom session implements `connected?` and `send_commands([command])`. Already loaded `Net::Connector::Base` sessions are adapted automatically through `execute_operation`, without connecting or requesting privilege. The adapter removes the matched prompt and first-line command echo, and keeps raw bytes only in the explicit capture evidence boundary. A native connector that closes on command rejection stays closed; later reads are skipped. A response exposes `output`, a boolean `success?`, an array `steps`, `value!` for a successful body, and `error` on failure. A successful read must contain exactly one completed step. The optional `preserve_session_on_rejection:` keyword is supplied only when the session explicitly declares it.
|
|
78
|
+
|
|
79
|
+
Use each reader sequentially; create separate readers for concurrent devices. The caller owns connection, disconnection, credentials, and approved command selection. Reader does not decide whether arbitrary command text is read-only. Standard execution/response errors become safe diagnostics; cancellation signals propagate.
|
|
80
|
+
|
|
81
|
+
Detailed vendor behavior, historical validation notes, and Motor-only integration are preserved in [the architecture reference](docs/architecture-zh.md). Host integration examples in that reference require Motor; they are not standalone gem APIs. Motor adapters live under `backend/lib/baseline`, `backend/lib/vxlan`, and `backend/lib/wireless` in the [Motor repository](https://github.com/gatework/motor/tree/main/backend/lib).
|
|
82
|
+
|
|
83
|
+
## Development and tests
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
bundle install
|
|
87
|
+
bundle exec rake
|
|
88
|
+
bundle exec ruby examples/offline.rb
|
|
89
|
+
bundle exec rake build
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`bundle exec rake` runs RSpec, RuboCop, and the offline examples. Tests live in `spec/`, following the existing project's RSpec convention; fixtures use synthetic or sanitized evidence. Add regression cases for successful, incomplete, unsupported, and malformed inputs when extending a parser. Offline tests do not certify all device versions or replace live integration checks.
|
|
93
|
+
|
|
94
|
+
Use the declared minimum Ruby version for compatibility checks. Document source methods and non-obvious business logic in concise Chinese, preserve public return/error contracts, and never add raw device captures or credentials to examples, fixtures, logs, or release archives.
|
|
95
|
+
|
|
96
|
+
## Release
|
|
97
|
+
|
|
98
|
+
This directory can be maintained as the independent [ctx_driver](https://github.com/gatework/ctx_driver) repository. Update `VERSION` and `CHANGELOG.md`, run the validation and build commands above, and inspect the packaged file list before tagging `vVERSION`.
|
|
99
|
+
|
|
100
|
+
CI runs tests, lint, examples, build, and an isolated installation on Linux and macOS. After CI passes on `main`, push `vVERSION` to run the release workflow. It publishes the tested Linux artifact to RubyGems and GitHub Releases, attaches `SHA256SUMS`, and verifies downloaded bytes. The GitHub `release` environment accepts a `RUBYGEMS_API_KEY` secret; without it, configure RubyGems Trusted Publishing for this repository, `release.yml`, and the `release` environment. Retry a failed publish job to reuse its CI artifact; an already published version must have identical bytes. See [release details](docs/release.md).
|
|
101
|
+
|
|
102
|
+
Publish dependency gems first. A Git tag, a successful build, or a workflow file does not establish that the gem has reached RubyGems; confirm the registry result separately. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
> 本文保留模块从 Motor 提取前的领域设计、覆盖范围、限制与验证记录,供兼容迁移和故障分析使用。
|
|
2
|
+
> 当前独立 gem 的安装、依赖、开发与发布入口以 [README-zh.md](../README-zh.md) 为准。
|
|
3
|
+
> 涉及 `Motor::*`、`Sync`、`Baseline::Check`、数据库、Rails、Rake 运维任务及私有实机证据的内容均属于 Motor 宿主集成,不随 gem 提供;历史验证记录不代表当前用户设备已经验证。
|
|
4
|
+
|
|
5
|
+
# 独立命令采集边界
|
|
6
|
+
|
|
7
|
+
`CtxDriver` 是 `ctx_checker`、`vxlan` 和 `wireless` 共用的纯 Ruby 执行组件。
|
|
8
|
+
它借用已打开的会话,一次执行一条已声明查询,不连接、关闭、重试或选择设备命令。
|
|
9
|
+
领域 Collector 继续负责准备步骤、命令分组、发现依赖、解析与 Observation 状态。
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "ctx_driver"
|
|
13
|
+
|
|
14
|
+
reader = CtxDriver::Reader.new(session: session)
|
|
15
|
+
capture = reader.read("show interfaces")
|
|
16
|
+
if capture.success?
|
|
17
|
+
# 正文仅显式用于解析;to_h / as_json / inspect 不包含正文。
|
|
18
|
+
result = parser.parse(capture.body, complete: true)
|
|
19
|
+
else
|
|
20
|
+
capture.failure # { code:, stage:, error_type: (有异常时) }
|
|
21
|
+
capture.skipped?
|
|
22
|
+
end
|
|
23
|
+
capture.evidence # command / sha256 / bytes / observed_at
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
会话协议为 `connected?` 和 `send_commands([command])`。返回值须提供 `output`、布尔 `success?`、数组 `steps`;
|
|
27
|
+
成功必须只有一个步骤,`value!` 必须返回正文字符串,失败通过 `error` 提供原因。
|
|
28
|
+
`output` 用于原始来源摘要,`value!` 是连接器处理后的正文,两者不要求相等。
|
|
29
|
+
自定义协议由宿主清理正文;原生 `Net::Connector::Base` 由可选适配器绑定当前完整提示符,并在证据摘要生成后去除命令回显及提示符。适配器不主动加载 net-connector,不连接、不提权。调用方只传目录中已批准的查询。
|
|
30
|
+
|
|
31
|
+
只有会话方法显式声明 `preserve_session_on_rejection:` 时,才传 `true`。
|
|
32
|
+
原生 net-connector 关闭拒绝命令后的会话时,Reader 保留该关闭状态;自定义适配器只有确认会话仍可用,才允许下一条独立命令。
|
|
33
|
+
断线或状态未知时记忆首个原因,后续命令返回 skipped,不自动重连或重放。
|
|
34
|
+
`reader.failure` 可供不提交命令的分支检查会话状态。
|
|
35
|
+
|
|
36
|
+
响应处理异常保留此前取得的 SHA256、字节数与时间。没有回执时不伪造摘要。
|
|
37
|
+
`Capture` 的正文与元数据分别复制、冻结;正常序列化不包含正文、异常对象、消息或凭据。
|
|
38
|
+
字段 `started_at/completed_at` 为采集阶段时间;证据的 `observed_at` 为实际收到响应的时间。
|
|
39
|
+
命令失败与领域健康状态分开,Reader 不产生 pass/fail 或完整库存判断。
|
|
40
|
+
|
|
41
|
+
`StandardError` 在单命令执行、响应验证和状态查询边界转换为安全诊断;取消信号继续抛出。
|
|
42
|
+
解析异常由领域 Collector 在本条命令或本次 AP 子任务内隔离。
|
|
43
|
+
调用方须顺序使用同一个 Reader;跨设备并发各自建立 Reader。
|
|
44
|
+
|
|
45
|
+
运行依赖由 gemspec 声明;会话所有权与命令来源响应时间契约保持不变。没有环境变量、数据库表或调度入口。
|
data/docs/release.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Release
|
|
2
|
+
|
|
3
|
+
1. Update `lib/ctx_driver/version.rb` and `CHANGELOG.md`; run `bundle exec rake` and `bundle exec rake build`.
|
|
4
|
+
2. Run `ruby script/verify_package.rb pkg/ctx_driver-VERSION.gem`. This installs and loads the built gem outside Bundler and the checkout.
|
|
5
|
+
3. Commit and push `main`; wait for both Linux and macOS CI jobs.
|
|
6
|
+
4. Push the matching `vVERSION` tag. The Release workflow repeats CI, then publishes its exact Linux artifact to RubyGems and GitHub with `SHA256SUMS`.
|
|
7
|
+
5. Verify the workflow, registry version, tag commit, and downloaded checksums. A local build is not publication evidence.
|
|
8
|
+
|
|
9
|
+
Configure `RUBYGEMS_API_KEY` as an encrypted secret in the GitHub `release` environment, with noninteractive gem-push permission. Alternatively configure [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) for `gatework/ctx_driver`, `release.yml`, environment `release`, and omit that secret. Never commit credentials.
|
|
10
|
+
|
|
11
|
+
A failed publish is not permission to overwrite an existing version. Rerun the failed publish job to retain the original CI artifact. The script verifies an existing RubyGems version before proceeding and rejects different bytes or yanked versions. `workflow_dispatch` is restricted to version tags by the publish validation.
|
|
12
|
+
|
|
13
|
+
Raw device `*.log`, local secrets, and build outputs are ignored. Package files use an explicit gemspec allowlist; review both the Git index and gem contents before publishing. Tests and examples must use synthetic or sanitized evidence.
|
data/examples/offline.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_driver"
|
|
4
|
+
|
|
5
|
+
# 用本地替身表示单条连接器结果,不打开套接字。
|
|
6
|
+
ExampleResponse = Data.define(:output, :steps) do
|
|
7
|
+
# 确认本例合成命令已成功完成。
|
|
8
|
+
def success? = true
|
|
9
|
+
|
|
10
|
+
# 返回已去除终端修饰的命令正文。
|
|
11
|
+
def value! = output
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# 借用会话示例仅实现 Reader 所需的传输协议。
|
|
15
|
+
class ExampleSession
|
|
16
|
+
# 本演示会话始终处于可用状态。
|
|
17
|
+
def connected? = true
|
|
18
|
+
|
|
19
|
+
# 接受声明的查询并返回单条合成命令结果。
|
|
20
|
+
def send_commands(commands)
|
|
21
|
+
raise ArgumentError, "Unexpected query" unless commands == [ "show interfaces" ]
|
|
22
|
+
|
|
23
|
+
ExampleResponse.new(output: "Ethernet1/1 is up\n", steps: [ { command: commands.first } ])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
capture = CtxDriver::Reader.new(session: ExampleSession.new).read("show interfaces")
|
|
28
|
+
abort "Capture failed: #{capture.failure[:code]}" unless capture.success?
|
|
29
|
+
|
|
30
|
+
puts "Capture status: success"
|
|
31
|
+
puts "Command: #{capture.command}"
|
|
32
|
+
puts "Bytes: #{capture.evidence.first[:bytes]}"
|
|
33
|
+
puts "SHA-256: #{capture.evidence.first[:sha256]}"
|
|
34
|
+
# 通过 capture.body 显式访问正文;默认序列化不包含正文。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
|
|
5
|
+
module CtxDriver
|
|
6
|
+
# 原始正文须显式读取;默认检查与序列化仅展示采集元数据。
|
|
7
|
+
class Capture
|
|
8
|
+
attr_reader :command, :body, :started_at, :completed_at, :evidence, :failure
|
|
9
|
+
|
|
10
|
+
# 冻结单条命令回执及其正文、时间、证据和失败元数据。
|
|
11
|
+
def initialize(command:, body: nil, started_at:, completed_at:, evidence: [], failure: nil, skipped: false)
|
|
12
|
+
@command = command.dup.freeze
|
|
13
|
+
@body = body&.dup&.freeze
|
|
14
|
+
@started_at, @completed_at = started_at.dup.freeze, completed_at.dup.freeze
|
|
15
|
+
@evidence, @failure = [ evidence, failure ].map { |value| CtxParser::Immutable.copy(value) }
|
|
16
|
+
@skipped = skipped
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 判断命令是否完成且未捕获失败。
|
|
21
|
+
def success? = failure.nil?
|
|
22
|
+
# 判断是否因会话不可用而抑制了本次执行。
|
|
23
|
+
def skipped? = @skipped
|
|
24
|
+
|
|
25
|
+
# 返回公开的数据结构,并保留状态与诊断信息。
|
|
26
|
+
def to_h
|
|
27
|
+
{ command: command, started_at: started_at, completed_at: completed_at,
|
|
28
|
+
evidence: evidence, failure: failure, skipped: skipped? }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 序列化诊断元数据,不包含原始来源文本。
|
|
32
|
+
def as_json(*) = to_h
|
|
33
|
+
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
34
|
+
def inspect = "#<#{self.class.name} success=#{success?} skipped=#{skipped?}>"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CtxDriver
|
|
4
|
+
# 命令诊断保留阶段与安全类型,不携带设备正文、异常消息或凭据。
|
|
5
|
+
module Failure
|
|
6
|
+
ERROR_TYPE = /\A(?:[A-Z][A-Za-z0-9_]*::)*[A-Z][A-Za-z0-9_]*\z/
|
|
7
|
+
|
|
8
|
+
# 将异常归纳为安全错误元数据,不暴露异常消息。
|
|
9
|
+
def self.details(error, fallback:, stage:)
|
|
10
|
+
details = { code: code(error, fallback), stage: stage }
|
|
11
|
+
if error
|
|
12
|
+
name = error.class.name
|
|
13
|
+
details[:error_type] = name.is_a?(String) && ERROR_TYPE.match?(name) ? name.dup.freeze : "StandardError"
|
|
14
|
+
end
|
|
15
|
+
details.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 识别传输异常,不要求特定连接器实现。
|
|
19
|
+
def self.connection_error?(error, owner)
|
|
20
|
+
(defined?(Net::Connector::Error) && error.is_a?(Net::Connector::Error)) ||
|
|
21
|
+
(owner.class.const_defined?(:Error) && error.is_a?(owner.class.const_get(:Error))) ||
|
|
22
|
+
error.is_a?(IOError)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 已加载 net-connector 时复用其稳定诊断词表,否则使用调用方的回退代码。
|
|
26
|
+
def self.code(error, fallback)
|
|
27
|
+
value = error.code if error.respond_to?(:code)
|
|
28
|
+
defined?(Net::Connector::ErrorMetadata) ? Net::Connector::ErrorMetadata.code(value) || fallback : fallback
|
|
29
|
+
rescue StandardError
|
|
30
|
+
fallback
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CtxDriver
|
|
4
|
+
# 适配已加载的 net-connector;不连接、不提权,也不恢复已关闭的借用会话。
|
|
5
|
+
class NetConnectorSession
|
|
6
|
+
# 先交出原始回执,Reader 建立证据后才按需解码正文。
|
|
7
|
+
Response = Data.define(:result) do
|
|
8
|
+
def output = result.output
|
|
9
|
+
def steps = result.steps
|
|
10
|
+
def success? = result.success?
|
|
11
|
+
def error = result.error
|
|
12
|
+
|
|
13
|
+
# 只移除匹配的首行命令回显;保留正文缩进及后续相同行。
|
|
14
|
+
def value!
|
|
15
|
+
result.value!
|
|
16
|
+
step = result.steps.fetch(0)
|
|
17
|
+
body = Net::Connector::TerminalText.render(step.output.delete_suffix(step.prompt.to_s))
|
|
18
|
+
first = body.lines.first
|
|
19
|
+
first && first.strip == step.command.text ? body.delete_prefix(first) : body
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
private_constant :Response
|
|
23
|
+
|
|
24
|
+
# 保存调用方拥有的连接器;该可选适配器不主动加载 net-connector。
|
|
25
|
+
def initialize(session)
|
|
26
|
+
@session = session
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 复用连接器的真实状态,避免失败后隐式重连。
|
|
30
|
+
def connected? = @session.connected?
|
|
31
|
+
|
|
32
|
+
# 单命令操作保护响应正文,并绑定当前完整提示符。
|
|
33
|
+
def send_commands(commands)
|
|
34
|
+
raise ArgumentError, "Expected one query" unless commands.one?
|
|
35
|
+
|
|
36
|
+
prompt = Net::Connector::TerminalText.render(@session.current_prompt.to_s).strip
|
|
37
|
+
raise ArgumentError, "Session prompt is unavailable" if prompt.empty?
|
|
38
|
+
|
|
39
|
+
pattern = /(?:\A|(?<=[\r\n]))[ \t\x00]*#{Regexp.escape(prompt)}[ \t\x00]*[\r\n]*\z/
|
|
40
|
+
command = Net::Connector::Command.new(commands.first, prompt: pattern, output_sensitive: true)
|
|
41
|
+
result = @session.execute_operation(Net::Connector::Script.new([ command ]), name: :ctx_driver, privilege: false)
|
|
42
|
+
Response.new(result)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "capture"
|
|
6
|
+
require_relative "failure"
|
|
7
|
+
require_relative "net_connector_session"
|
|
8
|
+
|
|
9
|
+
module CtxDriver
|
|
10
|
+
# 借用已打开会话逐条独立查询;连接归属与领域判断由外部负责。
|
|
11
|
+
class Reader
|
|
12
|
+
# 借用已打开会话,并初始化首个失败的跟踪状态。
|
|
13
|
+
def initialize(session:, clock: -> { Time.now.utc })
|
|
14
|
+
if defined?(Net::Connector::Base) && session.is_a?(Net::Connector::Base)
|
|
15
|
+
session = NetConnectorSession.new(session)
|
|
16
|
+
end
|
|
17
|
+
unless session.respond_to?(:send_commands)
|
|
18
|
+
raise ArgumentError, "Session must implement #send_commands for the command driver protocol"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@session, @clock = session, clock
|
|
22
|
+
@unavailable = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 执行一条经过校验的查询,保留独立结果或失败。
|
|
26
|
+
def read(command)
|
|
27
|
+
unless command.is_a?(String) && !command.strip.empty? && !/[\x00-\x1f\x7f]/.match?(command)
|
|
28
|
+
raise ArgumentError, "One command without control characters is required"
|
|
29
|
+
end
|
|
30
|
+
command = command.dup.freeze
|
|
31
|
+
started = @clock.call
|
|
32
|
+
if failure
|
|
33
|
+
return Capture.new(command: command, started_at: started, completed_at: @clock.call,
|
|
34
|
+
failure: @unavailable, skipped: true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
acquire(command, started)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 不发送命令即可检查会话可用性,并保留首次失败原因。
|
|
41
|
+
def failure
|
|
42
|
+
@unavailable ||= session_failure
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# 分离执行与回执校验;后续校验失败时仍保留已取得的证据。
|
|
48
|
+
def acquire(command, started)
|
|
49
|
+
stage, evidence, failure, body = :execute, [], nil, nil
|
|
50
|
+
begin
|
|
51
|
+
result = if preserves_rejection?
|
|
52
|
+
@session.send_commands([ command ], preserve_session_on_rejection: true)
|
|
53
|
+
else
|
|
54
|
+
@session.send_commands([ command ])
|
|
55
|
+
end
|
|
56
|
+
stage = :response
|
|
57
|
+
output = result.output
|
|
58
|
+
raise TypeError, "Command output must be text" unless output.is_a?(String)
|
|
59
|
+
|
|
60
|
+
evidence = [ { command: command, sha256: Digest::SHA256.hexdigest(output), bytes: output.bytesize,
|
|
61
|
+
observed_at: @clock.call.getutc.iso8601(6) } ]
|
|
62
|
+
success = result.success?
|
|
63
|
+
unless [ true, false ].include?(success) && result.steps.is_a?(Array) && (!success || result.steps.one?)
|
|
64
|
+
raise TypeError, "One query must return one completed command"
|
|
65
|
+
end
|
|
66
|
+
if success
|
|
67
|
+
body = result.value!
|
|
68
|
+
raise TypeError, "Command body must be text" unless body.is_a?(String)
|
|
69
|
+
else
|
|
70
|
+
failure = Failure.details(result.error, fallback: :command_failed, stage: :execute)
|
|
71
|
+
end
|
|
72
|
+
rescue StandardError => error
|
|
73
|
+
fallback = stage == :execute ? :command_execution_failed : :invalid_command_result
|
|
74
|
+
failure = Failure.details(error, fallback: fallback, stage: stage)
|
|
75
|
+
end
|
|
76
|
+
state_failure = session_failure
|
|
77
|
+
# 单命令失败但会话仍可用时继续独立查询;断链或状态不明才阻止后续提交。
|
|
78
|
+
@unavailable = failure || state_failure if state_failure
|
|
79
|
+
Capture.new(command: command, body: failure ? nil : body, started_at: started, completed_at: @clock.call,
|
|
80
|
+
evidence: evidence, failure: failure)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 仅在明确支持时复用命令拒绝后的会话;未知提示符仍由连接器关闭处理。
|
|
84
|
+
def preserves_rejection?
|
|
85
|
+
@session.method(:send_commands).parameters.any? do |kind, name|
|
|
86
|
+
%i[key keyreq].include?(kind) && name == :preserve_session_on_rejection
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 连接状态不确定时视为不可用,后续跳过的查询保留首次失败原因。
|
|
91
|
+
def session_failure
|
|
92
|
+
state = @session.connected?
|
|
93
|
+
raise TypeError, "Connection state must be boolean" unless [ true, false ].include?(state)
|
|
94
|
+
|
|
95
|
+
state ? nil : { code: :session_closed, stage: :session }.freeze
|
|
96
|
+
rescue StandardError => error
|
|
97
|
+
Failure.details(error, fallback: :session_state_failed, stage: :session)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/ctx_driver.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ctx_driver
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Gatework contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ctx_parser
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: digest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: time
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: Sequential declared command execution over injected sessions, with immutable
|
|
55
|
+
receipts, failure classification and source digests.
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- CHANGELOG.md
|
|
61
|
+
- LICENSE
|
|
62
|
+
- README-zh.md
|
|
63
|
+
- README.md
|
|
64
|
+
- docs/architecture-zh.md
|
|
65
|
+
- docs/release.md
|
|
66
|
+
- examples/offline.rb
|
|
67
|
+
- lib/ctx_driver.rb
|
|
68
|
+
- lib/ctx_driver/capture.rb
|
|
69
|
+
- lib/ctx_driver/failure.rb
|
|
70
|
+
- lib/ctx_driver/net_connector_session.rb
|
|
71
|
+
- lib/ctx_driver/reader.rb
|
|
72
|
+
- lib/ctx_driver/version.rb
|
|
73
|
+
homepage: https://github.com/gatework/ctx_driver
|
|
74
|
+
licenses:
|
|
75
|
+
- MIT
|
|
76
|
+
metadata:
|
|
77
|
+
source_code_uri: https://github.com/gatework/ctx_driver
|
|
78
|
+
bug_tracker_uri: https://github.com/gatework/ctx_driver/issues
|
|
79
|
+
changelog_uri: https://github.com/gatework/ctx_driver/blob/main/CHANGELOG.md
|
|
80
|
+
documentation_uri: https://github.com/gatework/ctx_driver#readme
|
|
81
|
+
allowed_push_host: https://rubygems.org
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: 4.0.6
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 4.0.16
|
|
98
|
+
specification_version: 4
|
|
99
|
+
summary: Collect network commands with isolated failure evidence.
|
|
100
|
+
test_files: []
|