network-infra-utility 0.3.0 → 0.6.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/.gitignore +21 -0
- data/GUIDE.md +212 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/Rakefile +1 -1
- data/bin/dns-query +836 -0
- data/bin/geo-doc +135 -0
- data/bin/geo-get +1 -1
- data/bin/geo-update +429 -0
- data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
- data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
- data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
- data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
- data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
- data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
- data/network-infra-utility.gemspec +4 -2
- data/network.rb +3 -1
- data/service/geodb/geodb.rb +278 -1
- data/service/ssh/README.md +955 -0
- data/service/ssh/bin/ssh-client +198 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
- data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
- data/service/ssh/ext/ssh_core/config/sys.config +0 -0
- data/service/ssh/ext/ssh_core/config/vm.args +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/rebar.config +24 -0
- data/service/ssh/ext/ssh_core/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
- data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
- data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
- data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
- data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +117 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
- data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
- data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
- data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
- data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
- data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
- data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
- data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
- data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
- data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
- data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
- data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
- data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
- data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
- data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
- data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
- data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
- data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
- data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
- data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
- data/support/basic/as_num.rb +221 -0
- data/support/basic/mac_address.rb +281 -0
- data/version.rb +1 -1
- metadata +142 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
//! SFTP 管理器 — 管理所有 SFTP 会话的打开、列目录、上传、下载、创建目录、删除、stat。
|
|
2
|
+
//! 对应 Erlang 端的 ssh_sftp_session.erl。
|
|
3
|
+
//!
|
|
4
|
+
//! 核心设计:
|
|
5
|
+
//! - 每个 SFTP 会话在 SSH 连接上打开一个 subsystem("sftp") 通道
|
|
6
|
+
//! - 通道通过 into_stream() 转为 AsyncRead+AsyncWrite 流
|
|
7
|
+
//! - 使用 russh-sftp crate 的 SftpSession 高层 API
|
|
8
|
+
//! - 会话状态由 SftpManager 管理(HashMap<sftp_id, SftpEntry>)
|
|
9
|
+
//! - SftpEntry 内部持有 Arc<SftpSession>,避免长时间持有 RwLock 读锁
|
|
10
|
+
//! - 连接断开时自动清理该连接下的所有 SFTP 会话
|
|
11
|
+
|
|
12
|
+
use std::collections::HashMap;
|
|
13
|
+
use std::sync::Arc;
|
|
14
|
+
|
|
15
|
+
use russh_sftp::client::SftpSession;
|
|
16
|
+
use serde_json::{json, Value};
|
|
17
|
+
use tokio::sync::RwLock;
|
|
18
|
+
use tracing::{debug, info, warn};
|
|
19
|
+
|
|
20
|
+
use crate::codec;
|
|
21
|
+
use crate::conn::ConnManager;
|
|
22
|
+
use crate::proto;
|
|
23
|
+
|
|
24
|
+
/// SFTP 管理器
|
|
25
|
+
pub struct SftpManager {
|
|
26
|
+
/// sftp_id → 会话状态
|
|
27
|
+
sessions: RwLock<HashMap<String, SftpEntry>>,
|
|
28
|
+
/// 连接管理器引用
|
|
29
|
+
conn_mgr: Arc<ConnManager>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// 单个 SFTP 会话的状态
|
|
33
|
+
struct SftpEntry {
|
|
34
|
+
conn_id: String,
|
|
35
|
+
/// 使用 Arc 以便在释放 RwLock 读锁后仍可安全调用异步操作
|
|
36
|
+
session: Arc<SftpSession>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl SftpManager {
|
|
40
|
+
pub fn new(conn_mgr: Arc<ConnManager>) -> Arc<Self> {
|
|
41
|
+
Arc::new(Self {
|
|
42
|
+
sessions: RwLock::new(HashMap::new()),
|
|
43
|
+
conn_mgr,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// 获取 SFTP session 的 Arc clone,释放 RwLock 读锁后仍可安全使用
|
|
48
|
+
async fn get_session(&self, sftp_id: &str) -> Result<Arc<SftpSession>, String> {
|
|
49
|
+
let sessions = self.sessions.read().await;
|
|
50
|
+
let entry = sessions.get(sftp_id).ok_or("sftp_not_found")?;
|
|
51
|
+
Ok(Arc::clone(&entry.session))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// RPC: sftp.open — 在指定连接上打开 SFTP 会话
|
|
55
|
+
pub async fn rpc_open(&self, params: Value) -> Result<Value, String> {
|
|
56
|
+
let conn_id = proto::get_str(¶ms, "conn_id")
|
|
57
|
+
.ok_or("missing conn_id")?
|
|
58
|
+
.to_string();
|
|
59
|
+
|
|
60
|
+
// 获取连接的 handle
|
|
61
|
+
let handle = self
|
|
62
|
+
.conn_mgr
|
|
63
|
+
.get_handle(&conn_id)
|
|
64
|
+
.await
|
|
65
|
+
.ok_or("conn_not_found")?;
|
|
66
|
+
|
|
67
|
+
// 打开 session 通道
|
|
68
|
+
let channel = handle
|
|
69
|
+
.channel_open_session()
|
|
70
|
+
.await
|
|
71
|
+
.map_err(|e| format!("channel_open_failed: {}", e))?;
|
|
72
|
+
|
|
73
|
+
// 请求 sftp subsystem
|
|
74
|
+
channel
|
|
75
|
+
.request_subsystem(true, "sftp")
|
|
76
|
+
.await
|
|
77
|
+
.map_err(|e| format!("subsystem_failed: {}", e))?;
|
|
78
|
+
|
|
79
|
+
// 将通道转为 AsyncRead+AsyncWrite 流
|
|
80
|
+
let stream = channel.into_stream();
|
|
81
|
+
|
|
82
|
+
// 创建 SftpSession
|
|
83
|
+
let sftp_session = SftpSession::new(stream)
|
|
84
|
+
.await
|
|
85
|
+
.map_err(|e| format!("sftp_init_failed: {}", e))?;
|
|
86
|
+
|
|
87
|
+
let sftp_id = codec::gen_sftp_id(&conn_id);
|
|
88
|
+
|
|
89
|
+
let entry = SftpEntry {
|
|
90
|
+
conn_id: conn_id.clone(),
|
|
91
|
+
session: Arc::new(sftp_session),
|
|
92
|
+
};
|
|
93
|
+
self.sessions
|
|
94
|
+
.write()
|
|
95
|
+
.await
|
|
96
|
+
.insert(sftp_id.clone(), entry);
|
|
97
|
+
|
|
98
|
+
info!("sftp.open: conn={} sftp_id={}", conn_id, sftp_id);
|
|
99
|
+
|
|
100
|
+
Ok(json!({
|
|
101
|
+
"sftp_id": sftp_id
|
|
102
|
+
}))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// RPC: sftp.list_dir — 列出远程目录内容
|
|
106
|
+
pub async fn rpc_list_dir(&self, params: Value) -> Result<Value, String> {
|
|
107
|
+
let sftp_id = proto::get_str(¶ms, "sftp_id")
|
|
108
|
+
.ok_or("missing sftp_id")?
|
|
109
|
+
.to_string();
|
|
110
|
+
let path = proto::get_str(¶ms, "path")
|
|
111
|
+
.ok_or("missing path")?;
|
|
112
|
+
|
|
113
|
+
let sftp = self.get_session(&sftp_id).await?;
|
|
114
|
+
let read_dir = sftp
|
|
115
|
+
.read_dir(path.to_string())
|
|
116
|
+
.await
|
|
117
|
+
.map_err(|e| format!("list_dir_failed: {}", e))?;
|
|
118
|
+
|
|
119
|
+
let mut entries = vec![];
|
|
120
|
+
for entry in read_dir {
|
|
121
|
+
// DirEntry::file_name() 返回 String
|
|
122
|
+
let name = entry.file_name();
|
|
123
|
+
// DirEntry::file_type() 返回 FileType 枚举(Dir/File/Symlink/Other)
|
|
124
|
+
let ftype = entry.file_type();
|
|
125
|
+
// DirEntry::metadata() 返回 FileAttributes
|
|
126
|
+
let metadata = entry.metadata();
|
|
127
|
+
|
|
128
|
+
let item = json!({
|
|
129
|
+
"name": name,
|
|
130
|
+
"size": metadata.len(), // FileAttributes::len() → u64
|
|
131
|
+
"is_dir": ftype.is_dir(),
|
|
132
|
+
"is_file": ftype.is_file(),
|
|
133
|
+
"is_symlink": ftype.is_symlink(),
|
|
134
|
+
"modified": metadata.mtime.unwrap_or(0),
|
|
135
|
+
});
|
|
136
|
+
entries.push(item);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
debug!("sftp.list_dir: sftp={} path={} entries={}", sftp_id, path, entries.len());
|
|
140
|
+
Ok(json!({
|
|
141
|
+
"entries": Value::Array(entries)
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// RPC: sftp.download — 从远程下载文件到本地
|
|
146
|
+
pub async fn rpc_download(&self, params: Value) -> Result<Value, String> {
|
|
147
|
+
let sftp_id = proto::get_str(¶ms, "sftp_id")
|
|
148
|
+
.ok_or("missing sftp_id")?
|
|
149
|
+
.to_string();
|
|
150
|
+
let remote = proto::get_str(¶ms, "remote")
|
|
151
|
+
.ok_or("missing remote")?;
|
|
152
|
+
let local = proto::get_str(¶ms, "local")
|
|
153
|
+
.ok_or("missing local")?;
|
|
154
|
+
|
|
155
|
+
let sftp = self.get_session(&sftp_id).await?;
|
|
156
|
+
let data = sftp
|
|
157
|
+
.read(remote.to_string())
|
|
158
|
+
.await
|
|
159
|
+
.map_err(|e| format!("read_failed: {}", e))?;
|
|
160
|
+
|
|
161
|
+
let transferred = data.len() as u64;
|
|
162
|
+
|
|
163
|
+
// 写入本地文件
|
|
164
|
+
tokio::fs::write(local, &data)
|
|
165
|
+
.await
|
|
166
|
+
.map_err(|e| format!("local_write_failed: {}", e))?;
|
|
167
|
+
|
|
168
|
+
info!("sftp.download: sftp={} remote={} local={} bytes={}",
|
|
169
|
+
sftp_id, remote, local, transferred);
|
|
170
|
+
|
|
171
|
+
Ok(json!({
|
|
172
|
+
"transferred": transferred,
|
|
173
|
+
"total": transferred
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// RPC: sftp.upload — 从本地上传文件到远程
|
|
178
|
+
pub async fn rpc_upload(&self, params: Value) -> Result<Value, String> {
|
|
179
|
+
let sftp_id = proto::get_str(¶ms, "sftp_id")
|
|
180
|
+
.ok_or("missing sftp_id")?
|
|
181
|
+
.to_string();
|
|
182
|
+
let local = proto::get_str(¶ms, "local")
|
|
183
|
+
.ok_or("missing local")?;
|
|
184
|
+
let remote = proto::get_str(¶ms, "remote")
|
|
185
|
+
.ok_or("missing remote")?;
|
|
186
|
+
|
|
187
|
+
// 读取本地文件
|
|
188
|
+
let data = tokio::fs::read(local)
|
|
189
|
+
.await
|
|
190
|
+
.map_err(|e| format!("local_read_failed: {}", e))?;
|
|
191
|
+
|
|
192
|
+
let transferred = data.len() as u64;
|
|
193
|
+
|
|
194
|
+
let sftp = self.get_session(&sftp_id).await?;
|
|
195
|
+
sftp.write(remote.to_string(), &data)
|
|
196
|
+
.await
|
|
197
|
+
.map_err(|e| format!("write_failed: {}", e))?;
|
|
198
|
+
|
|
199
|
+
info!("sftp.upload: sftp={} local={} remote={} bytes={}",
|
|
200
|
+
sftp_id, local, remote, transferred);
|
|
201
|
+
|
|
202
|
+
Ok(json!({
|
|
203
|
+
"transferred": transferred,
|
|
204
|
+
"total": transferred
|
|
205
|
+
}))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// RPC: sftp.mkdir — 创建远程目录
|
|
209
|
+
pub async fn rpc_mkdir(&self, params: Value) -> Result<Value, String> {
|
|
210
|
+
let sftp_id = proto::get_str(¶ms, "sftp_id")
|
|
211
|
+
.ok_or("missing sftp_id")?
|
|
212
|
+
.to_string();
|
|
213
|
+
let path = proto::get_str(¶ms, "path")
|
|
214
|
+
.ok_or("missing path")?;
|
|
215
|
+
|
|
216
|
+
let sftp = self.get_session(&sftp_id).await?;
|
|
217
|
+
sftp.create_dir(path.to_string())
|
|
218
|
+
.await
|
|
219
|
+
.map_err(|e| format!("mkdir_failed: {}", e))?;
|
|
220
|
+
|
|
221
|
+
debug!("sftp.mkdir: sftp={} path={}", sftp_id, path);
|
|
222
|
+
Ok(json!({"ok": true}))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// RPC: sftp.remove — 删除远程文件或目录
|
|
226
|
+
pub async fn rpc_remove(&self, params: Value) -> Result<Value, String> {
|
|
227
|
+
let sftp_id = proto::get_str(¶ms, "sftp_id")
|
|
228
|
+
.ok_or("missing sftp_id")?
|
|
229
|
+
.to_string();
|
|
230
|
+
let path = proto::get_str(¶ms, "path")
|
|
231
|
+
.ok_or("missing path")?;
|
|
232
|
+
|
|
233
|
+
let sftp = self.get_session(&sftp_id).await?;
|
|
234
|
+
|
|
235
|
+
// 先尝试当文件删除,失败再当目录删除
|
|
236
|
+
let result = sftp.remove_file(path.to_string()).await;
|
|
237
|
+
if result.is_err() {
|
|
238
|
+
sftp.remove_dir(path.to_string())
|
|
239
|
+
.await
|
|
240
|
+
.map_err(|e| format!("remove_failed: {}", e))?;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
debug!("sftp.remove: sftp={} path={}", sftp_id, path);
|
|
244
|
+
Ok(json!({"ok": true}))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// RPC: sftp.stat — 获取远程文件/目录的元数据
|
|
248
|
+
pub async fn rpc_stat(&self, params: Value) -> Result<Value, String> {
|
|
249
|
+
let sftp_id = proto::get_str(¶ms, "sftp_id")
|
|
250
|
+
.ok_or("missing sftp_id")?
|
|
251
|
+
.to_string();
|
|
252
|
+
let path = proto::get_str(¶ms, "path")
|
|
253
|
+
.ok_or("missing path")?;
|
|
254
|
+
|
|
255
|
+
let sftp = self.get_session(&sftp_id).await?;
|
|
256
|
+
let metadata = sftp
|
|
257
|
+
.metadata(path.to_string())
|
|
258
|
+
.await
|
|
259
|
+
.map_err(|e| format!("stat_failed: {}", e))?;
|
|
260
|
+
|
|
261
|
+
// FileAttributes: 公开字段结构体 + 便捷方法
|
|
262
|
+
// len() → u64(从 size 字段展开)
|
|
263
|
+
// is_dir() → bool(检查 permissions 中的 FileMode::DIR)
|
|
264
|
+
// is_regular() → bool(FileMode::REG,注意不是 is_file())
|
|
265
|
+
// is_symlink() → bool(FileMode::LNK)
|
|
266
|
+
// mtime → Option<u32>(Unix 时间戳秒)
|
|
267
|
+
// permissions → Option<u32>(原始 POSIX 权限位)
|
|
268
|
+
Ok(json!({
|
|
269
|
+
"size": metadata.len(),
|
|
270
|
+
"is_dir": metadata.is_dir(),
|
|
271
|
+
"is_file": metadata.is_regular(),
|
|
272
|
+
"is_symlink": metadata.is_symlink(),
|
|
273
|
+
"modified": metadata.mtime.unwrap_or(0),
|
|
274
|
+
"permissions": format!("{:o}", metadata.permissions.unwrap_or(0)),
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// 关闭并移除指定连接的所有 SFTP 会话(连接断开时调用)
|
|
279
|
+
pub async fn cleanup_conn(&self, conn_id: &str) {
|
|
280
|
+
let mut sessions = self.sessions.write().await;
|
|
281
|
+
let to_remove: Vec<String> = sessions
|
|
282
|
+
.iter()
|
|
283
|
+
.filter(|(_, e)| e.conn_id == conn_id)
|
|
284
|
+
.map(|(k, _)| k.clone())
|
|
285
|
+
.collect();
|
|
286
|
+
|
|
287
|
+
for id in &to_remove {
|
|
288
|
+
if let Some(entry) = sessions.remove(id) {
|
|
289
|
+
let _ = entry.session.close().await;
|
|
290
|
+
warn!("sftp session {} cleaned up (conn {} disconnected)", id, conn_id);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module NetworkInfraUtility
|
|
6
|
+
module SSH
|
|
7
|
+
module Automation
|
|
8
|
+
# 登录宏引擎,连接后自动执行预设命令序列。
|
|
9
|
+
# 对应需求 FR-AUTO-001。
|
|
10
|
+
#
|
|
11
|
+
# LLD §7.9 修正:
|
|
12
|
+
# 1. wait_for 不轮询 buffer.last_line,改为订阅 channel.data 事件,在事件到达时进行匹配
|
|
13
|
+
# 2. on_fail: :ask 通过外部传入的 on_ask 回调处理,不依赖 block_given
|
|
14
|
+
#
|
|
15
|
+
# DoD(LLD §11.2):50 步宏执行;wait_pattern 匹配;on_fail 三分支
|
|
16
|
+
#
|
|
17
|
+
# on_fail 三分支:
|
|
18
|
+
# :continue — 超时后继续执行下一步
|
|
19
|
+
# :abort — 超时后中止整个宏
|
|
20
|
+
# :ask — 超时后调用 on_ask 回调询问用户
|
|
21
|
+
class MacroEngine
|
|
22
|
+
MAX_STEPS = 50
|
|
23
|
+
|
|
24
|
+
# 单步定义
|
|
25
|
+
# @!attribute action
|
|
26
|
+
# @return [String] 要发送的命令
|
|
27
|
+
# @!attribute wait_pattern
|
|
28
|
+
# @return [String, Regexp, nil] 等待匹配的文本/正则,nil 不等待
|
|
29
|
+
# @!attribute delay
|
|
30
|
+
# @return [Float] 发送前延迟(秒)
|
|
31
|
+
# @!attribute on_fail
|
|
32
|
+
# @return [Symbol] :continue / :abort / :ask
|
|
33
|
+
Step = Struct.new(:action, :wait_pattern, :delay, :on_fail, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
attr_reader :steps, :running, :current_step_index
|
|
36
|
+
|
|
37
|
+
# @param session [Session::Session] 关联的会话
|
|
38
|
+
def initialize(session)
|
|
39
|
+
@session = session
|
|
40
|
+
@steps = []
|
|
41
|
+
@running = false
|
|
42
|
+
@abort = false
|
|
43
|
+
@current_step_index = nil
|
|
44
|
+
@data_subscription_sid = nil
|
|
45
|
+
@on_ask_callback = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 设置 on_fail=:ask 时的回调
|
|
49
|
+
# 回调签名: ->(step, step_index) { :continue | :abort }
|
|
50
|
+
def on_ask(&block)
|
|
51
|
+
@on_ask_callback = block
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# 添加一步。
|
|
55
|
+
# @param action [String] 要发送的命令
|
|
56
|
+
# @param wait_pattern [String, Regexp, nil] 等待匹配的文本/正则
|
|
57
|
+
# @param delay [Float] 发送前延迟(秒),默认 0
|
|
58
|
+
# @param on_fail [Symbol] 失败时行为::continue / :abort / :ask
|
|
59
|
+
def add_step(action:, wait_pattern: nil, delay: 0, on_fail: :continue)
|
|
60
|
+
raise "Too many steps (max #{MAX_STEPS})" if @steps.size >= MAX_STEPS
|
|
61
|
+
|
|
62
|
+
@steps << Step.new(action: action, wait_pattern: wait_pattern,
|
|
63
|
+
delay: delay, on_fail: on_fail)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 从 YAML 配置加载步骤(sessions.yml macro.steps)
|
|
67
|
+
# @param config [Hash] { enabled: bool, steps: [{action, wait_pattern, delay, on_fail}] }
|
|
68
|
+
def load_from_config(config)
|
|
69
|
+
return unless config && config[:enabled]
|
|
70
|
+
|
|
71
|
+
Array(config[:steps]).each do |step_cfg|
|
|
72
|
+
add_step(
|
|
73
|
+
action: step_cfg[:action],
|
|
74
|
+
wait_pattern: step_cfg[:wait_pattern],
|
|
75
|
+
delay: step_cfg[:delay] || 0,
|
|
76
|
+
on_fail: (step_cfg[:on_fail] || :continue).to_sym
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 执行宏。
|
|
82
|
+
# @param on_progress [Proc, nil] 进度回调 ->(step, index, total) {}
|
|
83
|
+
# @return [Symbol] :completed | :aborted
|
|
84
|
+
def run(on_progress: nil)
|
|
85
|
+
raise "Macro already running" if @running
|
|
86
|
+
return :completed if @steps.empty?
|
|
87
|
+
|
|
88
|
+
@running = true
|
|
89
|
+
@abort = false
|
|
90
|
+
|
|
91
|
+
@steps.each_with_index do |step, i|
|
|
92
|
+
break if @abort
|
|
93
|
+
@current_step_index = i
|
|
94
|
+
on_progress&.call(step, i + 1, @steps.size)
|
|
95
|
+
|
|
96
|
+
sleep step.delay if step.delay > 0
|
|
97
|
+
|
|
98
|
+
@session.terminal.send(step.action)
|
|
99
|
+
|
|
100
|
+
if step.wait_pattern
|
|
101
|
+
result = wait_for(step.wait_pattern, timeout: 30)
|
|
102
|
+
handle_step_result(step, i, result)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@abort ? :aborted : :completed
|
|
107
|
+
ensure
|
|
108
|
+
@running = false
|
|
109
|
+
@current_step_index = nil
|
|
110
|
+
cleanup_subscription
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 中止执行中的宏
|
|
114
|
+
def abort!
|
|
115
|
+
@abort = true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# 清空步骤
|
|
119
|
+
def clear
|
|
120
|
+
raise "Cannot clear while running" if @running
|
|
121
|
+
@steps.clear
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 步骤数
|
|
125
|
+
def step_count
|
|
126
|
+
@steps.size
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# LLD §7.9 修正1:wait_for 不轮询 buffer.last_line,
|
|
132
|
+
# 改为订阅 channel.data 事件,在事件到达时匹配。
|
|
133
|
+
# @param pattern [String, Regexp] 等待匹配的文本/正则
|
|
134
|
+
# @param timeout [Integer] 超时秒数
|
|
135
|
+
# @return [Symbol] :matched | :timeout
|
|
136
|
+
def wait_for(pattern, timeout:)
|
|
137
|
+
regex = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern.to_s))
|
|
138
|
+
deadline = Time.now + timeout
|
|
139
|
+
accumulated = +""
|
|
140
|
+
matched = false
|
|
141
|
+
queue = Queue.new
|
|
142
|
+
|
|
143
|
+
# 订阅 channel.data 推送
|
|
144
|
+
@data_subscription_sid = @session.client.ipc.subscribe("channel.data") do |params|
|
|
145
|
+
next unless params[:conn_id] == @session.conn_id &&
|
|
146
|
+
params[:channel_id] == @session.terminal.channel_id
|
|
147
|
+
|
|
148
|
+
data = decode_data(params[:data])
|
|
149
|
+
accumulated << data
|
|
150
|
+
queue << :data if accumulated.match?(regex)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
deadline
|
|
154
|
+
loop do
|
|
155
|
+
remaining = deadline - Time.now
|
|
156
|
+
return :timeout if remaining <= 0
|
|
157
|
+
return :matched if accumulated.match?(regex)
|
|
158
|
+
|
|
159
|
+
# 等待新数据到达或超时
|
|
160
|
+
begin
|
|
161
|
+
Timeout.timeout(remaining) { queue.pop }
|
|
162
|
+
return :matched if accumulated.match?(regex)
|
|
163
|
+
rescue Timeout::Error
|
|
164
|
+
return :timeout
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
ensure
|
|
168
|
+
cleanup_subscription
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# LLD §7.9 修正2:on_fail=:ask 通过外部回调处理
|
|
172
|
+
# @param step [Step] 当前步骤
|
|
173
|
+
# @param index [Integer] 步骤索引
|
|
174
|
+
# @param result [Symbol] :matched | :timeout
|
|
175
|
+
def handle_step_result(step, index, result)
|
|
176
|
+
return if result == :matched
|
|
177
|
+
|
|
178
|
+
case step.on_fail
|
|
179
|
+
when :continue
|
|
180
|
+
# 超时后继续
|
|
181
|
+
when :abort
|
|
182
|
+
@abort = true
|
|
183
|
+
when :ask
|
|
184
|
+
if @on_ask_callback
|
|
185
|
+
user_decision = @on_ask_callback.call(step, index)
|
|
186
|
+
@abort = true if user_decision == :abort
|
|
187
|
+
else
|
|
188
|
+
# 无回调时默认 abort
|
|
189
|
+
@abort = true
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def cleanup_subscription
|
|
195
|
+
return unless @data_subscription_sid
|
|
196
|
+
|
|
197
|
+
@session.client.ipc.unsubscribe("channel.data", @data_subscription_sid)
|
|
198
|
+
@data_subscription_sid = nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# 解码 Base64 通道数据
|
|
202
|
+
def decode_data(encoded)
|
|
203
|
+
return "" unless encoded
|
|
204
|
+
|
|
205
|
+
require "base64"
|
|
206
|
+
Base64.strict_decode64(encoded)
|
|
207
|
+
rescue StandardError
|
|
208
|
+
""
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|