agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
1 parent
d5ebf29e57
commit
ad5efc62ff
206 files changed
+4202
-4964
No files matched your search
@@ -2,267 +2,36 @@ using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace MoShiro.Backend.Storage.Sqlite;
|
||||
|
||||
/// <summary>表结构。版本记录在 <c>PRAGMA user_version</c>;以后的迁移是按版本号追加的内置 SQL。</summary>
|
||||
/// <summary>
|
||||
/// 表结构。版本记录在 <c>PRAGMA user_version</c>。当前只有一个版本:不是这个版本的数据库(包括更早的构建留下的)不会被迁移,
|
||||
/// 启动时报错,删除 <c>moshiro.db</c> 和 <c>raw/</c> 之后重新开始。以后的结构变更按版本号追加内置 SQL。
|
||||
/// </summary>
|
||||
internal static class Schema
|
||||
{
|
||||
public const int CurrentVersion = 16;
|
||||
public const int CurrentVersion = 1;
|
||||
|
||||
public static void Migrate(SqliteConnection connection)
|
||||
{
|
||||
int version = connection.Scalar<int>("PRAGMA user_version");
|
||||
if (version > CurrentVersion)
|
||||
if (version == CurrentVersion)
|
||||
{
|
||||
throw new InvalidOperationException($"database schema version {version} is newer than this build supports ({CurrentVersion})");
|
||||
return;
|
||||
}
|
||||
|
||||
if (version < 1)
|
||||
if (version != 0)
|
||||
{
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec(V1);
|
||||
connection.Exec("PRAGMA user_version = 1");
|
||||
transaction.Commit();
|
||||
throw new InvalidOperationException($"database schema version {version} is not supported by this build (expected {CurrentVersion}); delete moshiro.db and the raw/ directory to start over");
|
||||
}
|
||||
|
||||
if (version < 2)
|
||||
{
|
||||
// 测试属于一条线路(Probe,Agent):旧测试没有 Probe,留空
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE tests ADD COLUMN probe_id TEXT NOT NULL DEFAULT ''");
|
||||
connection.Exec("CREATE INDEX ix_tests_line ON tests (agent_id, probe_id, requested_at)");
|
||||
connection.Exec("PRAGMA user_version = 2");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 3)
|
||||
{
|
||||
// 主机资源占用的历史(每个 Agent 每分钟一行)
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("""
|
||||
CREATE TABLE host_samples (
|
||||
agent_id TEXT NOT NULL,
|
||||
at INTEGER NOT NULL,
|
||||
cpu REAL,
|
||||
mem_used INTEGER,
|
||||
mem_total INTEGER,
|
||||
disk_used INTEGER,
|
||||
disk_total INTEGER,
|
||||
net_rx_bps INTEGER,
|
||||
net_tx_bps INTEGER,
|
||||
uptime INTEGER,
|
||||
PRIMARY KEY (agent_id, at)
|
||||
) WITHOUT ROWID;
|
||||
""");
|
||||
connection.Exec("PRAGMA user_version = 3");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 4)
|
||||
{
|
||||
// 节点的展示信息:系统、架构、国家、运营商;geo_host 是查询用的地址,只在 Backend 内部用
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("""
|
||||
CREATE TABLE node_meta (
|
||||
node_id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL,
|
||||
os TEXT,
|
||||
os_name TEXT,
|
||||
arch TEXT,
|
||||
country TEXT,
|
||||
isp_key TEXT,
|
||||
isp_name TEXT,
|
||||
geo_host TEXT,
|
||||
geo_at INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
) WITHOUT ROWID
|
||||
""");
|
||||
connection.Exec("PRAGMA user_version = 4");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 5)
|
||||
{
|
||||
// 节点自报的构建信息:版本、tag、提交、构建时间
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE node_meta ADD COLUMN version TEXT");
|
||||
connection.Exec("ALTER TABLE node_meta ADD COLUMN tag TEXT");
|
||||
connection.Exec("ALTER TABLE node_meta ADD COLUMN commit_id TEXT");
|
||||
connection.Exec("ALTER TABLE node_meta ADD COLUMN built_at TEXT");
|
||||
connection.Exec("PRAGMA user_version = 5");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 6)
|
||||
{
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_host TEXT NOT NULL DEFAULT ''");
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_supported INTEGER NOT NULL DEFAULT 0");
|
||||
connection.Exec("ALTER TABLE tests ADD COLUMN ip_version TEXT NOT NULL DEFAULT 'ipv4'");
|
||||
connection.Exec("CREATE INDEX ix_tests_path ON tests (agent_id, probe_id, ip_version, requested_at)");
|
||||
connection.Exec("PRAGMA user_version = 6");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 7)
|
||||
{
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_diagnostics TEXT");
|
||||
connection.Exec("ALTER TABLE probes ADD COLUMN ipv6_diagnostics TEXT");
|
||||
connection.Exec("PRAGMA user_version = 7");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 8)
|
||||
{
|
||||
// Probe 用加入令牌注册:令牌带种类;Probe 有两把独立的密钥,对等面公钥单独存
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE join_tokens ADD COLUMN kind TEXT NOT NULL DEFAULT ''");
|
||||
connection.Exec("ALTER TABLE probes ADD COLUMN peer_pubkey BLOB");
|
||||
connection.Exec("PRAGMA user_version = 8");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 9)
|
||||
{
|
||||
// 对外地址自动识别:手填的标记为 manual,自动识别不会覆盖;Agent 上报的地址存下来,撤销手填时马上能重新识别
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN public_host_manual INTEGER NOT NULL DEFAULT 0");
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_host_manual INTEGER NOT NULL DEFAULT 0");
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN reported_addresses TEXT NOT NULL DEFAULT '[]'");
|
||||
connection.Exec("PRAGMA user_version = 9");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 10)
|
||||
{
|
||||
// 预握手敲门:Probe 需要 Agent 的证书公钥(不只是指纹)来派生配对密钥,由 Agent 在 hello 里上报
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN tls_pubkey BLOB");
|
||||
connection.Exec("PRAGMA user_version = 10");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 11)
|
||||
{
|
||||
// 权限从三个作用域改成细粒度的能力。破坏性变更,不做兼容:旧的 API 令牌全部作废,需要重新创建
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("DROP TABLE api_tokens");
|
||||
connection.Exec("""
|
||||
CREATE TABLE api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
capabilities TEXT NOT NULL,
|
||||
token_sha256 BLOB NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER,
|
||||
expires_at INTEGER,
|
||||
revoked_at INTEGER
|
||||
)
|
||||
""");
|
||||
connection.Exec("PRAGMA user_version = 11");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 12)
|
||||
{
|
||||
// passkey 登录:每个用户可以注册若干 passkey(凭证公钥 SPKI、签名计数器);会话记录是怎么登录的,只有 passkey 登录的会话以后能提权
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("""
|
||||
CREATE TABLE passkeys (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
public_key BLOB NOT NULL,
|
||||
sign_count INTEGER NOT NULL DEFAULT 0,
|
||||
transports TEXT NOT NULL DEFAULT '[]',
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER
|
||||
)
|
||||
""");
|
||||
connection.Exec("CREATE INDEX ix_passkeys_user ON passkeys (user_id)");
|
||||
connection.Exec("ALTER TABLE sessions ADD COLUMN auth_method TEXT NOT NULL DEFAULT 'password'");
|
||||
connection.Exec("PRAGMA user_version = 12");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 13)
|
||||
{
|
||||
// 归属:加入码是谁创建的,节点是用哪个加入码(也就是谁)加进来的;审计日志
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE join_tokens ADD COLUMN created_by TEXT");
|
||||
connection.Exec("ALTER TABLE agents ADD COLUMN added_by TEXT");
|
||||
connection.Exec("ALTER TABLE probes ADD COLUMN added_by TEXT");
|
||||
connection.Exec("""
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
at INTEGER NOT NULL,
|
||||
user_id TEXT,
|
||||
actor TEXT NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
target TEXT,
|
||||
detail TEXT,
|
||||
remote TEXT,
|
||||
success INTEGER NOT NULL
|
||||
)
|
||||
""");
|
||||
connection.Exec("CREATE INDEX ix_audit_time ON audit_log (at)");
|
||||
connection.Exec(
|
||||
"""
|
||||
CREATE TABLE operator_sets (
|
||||
version INTEGER PRIMARY KEY,
|
||||
statement TEXT NOT NULL,
|
||||
credential_id TEXT NOT NULL,
|
||||
authenticator_data TEXT NOT NULL,
|
||||
client_data_json TEXT NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)
|
||||
""");
|
||||
connection.Exec("PRAGMA user_version = 13");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 14)
|
||||
{
|
||||
// 节点的二进制绑定:第一次握手成功时绑定正在运行的二进制的 SHA-256
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec(
|
||||
"""
|
||||
CREATE TABLE node_bindings (
|
||||
node_id TEXT PRIMARY KEY,
|
||||
sha256 TEXT NOT NULL,
|
||||
seq INTEGER NOT NULL,
|
||||
signed INTEGER NOT NULL,
|
||||
bound_at INTEGER NOT NULL,
|
||||
violation TEXT,
|
||||
violation_at INTEGER
|
||||
)
|
||||
""");
|
||||
connection.Exec("PRAGMA user_version = 14");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 15)
|
||||
{
|
||||
// 运行时二进制绑定整体移除(节点自报的哈希不可信,来源证明在安装时):旧版本留下的表不再使用
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("DROP TABLE IF EXISTS node_bindings");
|
||||
connection.Exec("PRAGMA user_version = 15");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
if (version < 16)
|
||||
{
|
||||
// 运营商可以手动指定(自动识别会错,例如 CN2 线路):手动的不会被后面的 IP 归属查询覆盖
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec("ALTER TABLE node_meta ADD COLUMN isp_manual INTEGER NOT NULL DEFAULT 0");
|
||||
connection.Exec("PRAGMA user_version = 16");
|
||||
transaction.Commit();
|
||||
}
|
||||
using SqliteTransaction transaction = connection.BeginTransaction();
|
||||
connection.Exec(V1);
|
||||
connection.Exec("PRAGMA user_version = 1");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增 Kind 或指标都不需要改表结构:指标是 <c>metric_*.metrics</c> 里的一个键,Kind 只是 <c>raw_chunks.kind</c> 的一个取值。
|
||||
/// 节点只有一张表(<c>agents</c>):监听的节点有地址和端口,不监听的节点只拨出。线路 <c>(agent_id, series)</c> 里的 agent_id 是监听方,series 里是拨号方。
|
||||
/// </summary>
|
||||
private const string V1 = """
|
||||
CREATE TABLE agents (
|
||||
@@ -271,9 +40,17 @@ internal static class Schema
|
||||
status TEXT NOT NULL,
|
||||
pubkey BLOB NOT NULL,
|
||||
tls_spki BLOB NOT NULL,
|
||||
tls_pubkey BLOB NOT NULL,
|
||||
listens INTEGER NOT NULL DEFAULT 0,
|
||||
public_host TEXT NOT NULL DEFAULT '',
|
||||
probe_tcp_port INTEGER NOT NULL DEFAULT 7443,
|
||||
probe_udp_ports TEXT NOT NULL DEFAULT '[7443]',
|
||||
tcp_port INTEGER NOT NULL DEFAULT 0,
|
||||
quic_ports TEXT NOT NULL DEFAULT '[]',
|
||||
ipv6_host TEXT NOT NULL DEFAULT '',
|
||||
ipv6_supported INTEGER NOT NULL DEFAULT 0,
|
||||
ipv6_diagnostics TEXT,
|
||||
public_host_manual INTEGER NOT NULL DEFAULT 0,
|
||||
ipv6_host_manual INTEGER NOT NULL DEFAULT 0,
|
||||
reported_addresses TEXT NOT NULL DEFAULT '[]',
|
||||
labels TEXT NOT NULL DEFAULT '[]',
|
||||
public_visible INTEGER NOT NULL DEFAULT 1,
|
||||
kinds TEXT NOT NULL DEFAULT '[]',
|
||||
@@ -284,12 +61,67 @@ internal static class Schema
|
||||
rejected TEXT,
|
||||
task_hash TEXT,
|
||||
meta TEXT,
|
||||
added_by TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_seen_at INTEGER,
|
||||
row_version INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
-- 同名节点只允许一个已建立的(pending 的可以重名,等待管理员接管或改名)
|
||||
CREATE UNIQUE INDEX ux_agents_name ON agents (name) WHERE status <> 'pending';
|
||||
|
||||
CREATE TABLE analysis_queue (
|
||||
agent_id TEXT NOT NULL,
|
||||
series TEXT NOT NULL,
|
||||
window_start INTEGER NOT NULL,
|
||||
not_before INTEGER NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
seq INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (agent_id, series, window_start)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
capabilities TEXT NOT NULL,
|
||||
token_sha256 BLOB NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER,
|
||||
expires_at INTEGER,
|
||||
revoked_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
at INTEGER NOT NULL,
|
||||
user_id TEXT,
|
||||
actor TEXT NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
target TEXT,
|
||||
detail TEXT,
|
||||
remote TEXT,
|
||||
success INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE events (
|
||||
id INTEGER PRIMARY KEY,
|
||||
agent_id TEXT,
|
||||
kind TEXT NOT NULL,
|
||||
at INTEGER NOT NULL,
|
||||
detail TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE host_samples (
|
||||
agent_id TEXT NOT NULL,
|
||||
at INTEGER NOT NULL,
|
||||
cpu REAL,
|
||||
mem_used INTEGER,
|
||||
mem_total INTEGER,
|
||||
disk_used INTEGER,
|
||||
disk_total INTEGER,
|
||||
net_rx_bps INTEGER,
|
||||
net_tx_bps INTEGER,
|
||||
uptime INTEGER,
|
||||
PRIMARY KEY (agent_id, at)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE join_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -301,41 +133,105 @@ internal static class Schema
|
||||
uses INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at INTEGER NOT NULL,
|
||||
revoked_at INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE probes (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
pubkey BLOB NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
revoked_at INTEGER
|
||||
kind TEXT NOT NULL DEFAULT '',
|
||||
created_by TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash BLOB NOT NULL,
|
||||
password_salt BLOB NOT NULL,
|
||||
iterations INTEGER NOT NULL,
|
||||
CREATE TABLE metric_10s (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE metric_1h (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE metric_1m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE metric_5m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE metric_versions (
|
||||
metric_id TEXT PRIMARY KEY,
|
||||
fingerprint TEXT NOT NULL,
|
||||
recompute_from INTEGER,
|
||||
recompute_cursor INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE node_meta (
|
||||
node_id TEXT PRIMARY KEY,
|
||||
os TEXT,
|
||||
os_name TEXT,
|
||||
arch TEXT,
|
||||
country TEXT,
|
||||
isp_key TEXT,
|
||||
isp_name TEXT,
|
||||
isp_manual INTEGER NOT NULL DEFAULT 0,
|
||||
geo_host TEXT,
|
||||
geo_at INTEGER NOT NULL DEFAULT 0,
|
||||
version TEXT,
|
||||
tag TEXT,
|
||||
commit_id TEXT,
|
||||
built_at TEXT,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE operator_sets (
|
||||
version INTEGER PRIMARY KEY,
|
||||
statement TEXT NOT NULL,
|
||||
credential_id TEXT NOT NULL,
|
||||
authenticator_data TEXT NOT NULL,
|
||||
client_data_json TEXT NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE passkeys (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
public_key BLOB NOT NULL,
|
||||
sign_count INTEGER NOT NULL DEFAULT 0,
|
||||
transports TEXT NOT NULL DEFAULT '[]',
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE raw_chunks (
|
||||
chunk_id TEXT PRIMARY KEY,
|
||||
segment_id INTEGER NOT NULL REFERENCES raw_segments(id),
|
||||
offset INTEGER NOT NULL,
|
||||
length INTEGER NOT NULL,
|
||||
agent_id TEXT NOT NULL,
|
||||
series TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
task_id TEXT,
|
||||
test_id TEXT,
|
||||
phase INTEGER,
|
||||
start_ts INTEGER NOT NULL,
|
||||
duration_ms INTEGER NOT NULL,
|
||||
partial INTEGER NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE raw_segments (
|
||||
id INTEGER PRIMARY KEY,
|
||||
day TEXT NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
bytes INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE rollup_dirty (
|
||||
level INTEGER NOT NULL,
|
||||
agent_id TEXT NOT NULL,
|
||||
series TEXT NOT NULL,
|
||||
bucket_start INTEGER NOT NULL,
|
||||
PRIMARY KEY (level, agent_id, series, bucket_start)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id_sha256 BLOB PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
csrf_token BLOB NOT NULL,
|
||||
auth_method TEXT NOT NULL DEFAULT 'password',
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
token_sha256 BLOB NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER,
|
||||
revoked_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
|
||||
CREATE TABLE tasks (
|
||||
@@ -352,9 +248,17 @@ internal static class Schema
|
||||
row_version INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE test_results (
|
||||
test_id TEXT PRIMARY KEY,
|
||||
metrics BLOB NOT NULL,
|
||||
series BLOB NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE tests (
|
||||
id TEXT PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL,
|
||||
dialer_id TEXT NOT NULL,
|
||||
ip_version TEXT NOT NULL DEFAULT 'ipv4',
|
||||
plan TEXT NOT NULL,
|
||||
params TEXT NOT NULL,
|
||||
trigger TEXT NOT NULL,
|
||||
@@ -366,79 +270,42 @@ internal static class Schema
|
||||
started_at INTEGER,
|
||||
finished_at INTEGER
|
||||
);
|
||||
CREATE INDEX ix_tests_agent ON tests (agent_id, requested_at);
|
||||
CREATE TABLE test_results (
|
||||
test_id TEXT PRIMARY KEY,
|
||||
metrics BLOB NOT NULL,
|
||||
series BLOB NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE raw_segments (
|
||||
id INTEGER PRIMARY KEY,
|
||||
day TEXT NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
bytes INTEGER NOT NULL DEFAULT 0
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash BLOB NOT NULL,
|
||||
password_salt BLOB NOT NULL,
|
||||
iterations INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE raw_chunks (
|
||||
chunk_id TEXT PRIMARY KEY,
|
||||
segment_id INTEGER NOT NULL REFERENCES raw_segments(id),
|
||||
offset INTEGER NOT NULL,
|
||||
length INTEGER NOT NULL,
|
||||
agent_id TEXT NOT NULL,
|
||||
series TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
task_id TEXT,
|
||||
test_id TEXT,
|
||||
phase INTEGER,
|
||||
start_ts INTEGER NOT NULL,
|
||||
duration_ms INTEGER NOT NULL,
|
||||
partial INTEGER NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
CREATE INDEX ix_raw_series ON raw_chunks (agent_id, series, start_ts);
|
||||
CREATE INDEX ix_raw_test ON raw_chunks (test_id) WHERE test_id IS NOT NULL;
|
||||
CREATE INDEX ix_raw_segment ON raw_chunks (segment_id);
|
||||
|
||||
CREATE TABLE metric_10s (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
CREATE TABLE metric_1m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
CREATE TABLE metric_5m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
CREATE TABLE metric_1h (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
|
||||
CREATE INDEX ix_audit_time ON audit_log (at);
|
||||
|
||||
CREATE INDEX ix_events_agent ON events (agent_id, at);
|
||||
|
||||
CREATE INDEX ix_events_time ON events (at);
|
||||
|
||||
CREATE INDEX ix_metric_10s_time ON metric_10s (window_start);
|
||||
|
||||
CREATE INDEX ix_metric_1m_time ON metric_1m (window_start);
|
||||
|
||||
CREATE INDEX ix_metric_5m_time ON metric_5m (window_start);
|
||||
|
||||
CREATE TABLE metric_versions (
|
||||
metric_id TEXT PRIMARY KEY,
|
||||
fingerprint TEXT NOT NULL,
|
||||
recompute_from INTEGER,
|
||||
recompute_cursor INTEGER
|
||||
);
|
||||
CREATE INDEX ix_passkeys_user ON passkeys (user_id);
|
||||
|
||||
CREATE TABLE analysis_queue (
|
||||
agent_id TEXT NOT NULL,
|
||||
series TEXT NOT NULL,
|
||||
window_start INTEGER NOT NULL,
|
||||
not_before INTEGER NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
seq INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (agent_id, series, window_start)
|
||||
) WITHOUT ROWID;
|
||||
CREATE INDEX ix_queue_due ON analysis_queue (not_before);
|
||||
CREATE TABLE rollup_dirty (
|
||||
level INTEGER NOT NULL,
|
||||
agent_id TEXT NOT NULL,
|
||||
series TEXT NOT NULL,
|
||||
bucket_start INTEGER NOT NULL,
|
||||
PRIMARY KEY (level, agent_id, series, bucket_start)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE events (
|
||||
id INTEGER PRIMARY KEY,
|
||||
agent_id TEXT,
|
||||
kind TEXT NOT NULL,
|
||||
at INTEGER NOT NULL,
|
||||
detail TEXT
|
||||
);
|
||||
CREATE INDEX ix_events_time ON events (at);
|
||||
CREATE INDEX ix_events_agent ON events (agent_id, at);
|
||||
CREATE INDEX ix_raw_segment ON raw_chunks (segment_id);
|
||||
|
||||
CREATE INDEX ix_raw_series ON raw_chunks (agent_id, series, start_ts);
|
||||
|
||||
CREATE INDEX ix_raw_test ON raw_chunks (test_id) WHERE test_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX ix_tests_agent ON tests (agent_id, requested_at);
|
||||
|
||||
CREATE INDEX ix_tests_path ON tests (agent_id, dialer_id, ip_version, requested_at);
|
||||
|
||||
CREATE UNIQUE INDEX ux_agents_name ON agents (name) WHERE status <> 'pending';
|
||||
""";
|
||||
}
|
||||
@@ -13,8 +13,8 @@ namespace MoShiro.Backend.Storage.Sqlite;
|
||||
public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
{
|
||||
private const string AgentColumns =
|
||||
"id, name, status, pubkey, tls_spki, public_host, probe_tcp_port, probe_udp_ports, labels, public_visible, kinds, agent_version, local_limits, " +
|
||||
"task_version, applied_task_version, rejected, created_at, last_seen_at, row_version, ipv6_host, ipv6_supported, ipv6_diagnostics, public_host_manual, ipv6_host_manual, reported_addresses, tls_pubkey, added_by";
|
||||
"id, name, status, pubkey, tls_spki, public_host, tcp_port, quic_ports, labels, public_visible, kinds, agent_version, local_limits, " +
|
||||
"task_version, applied_task_version, rejected, created_at, last_seen_at, row_version, ipv6_host, ipv6_supported, ipv6_diagnostics, public_host_manual, ipv6_host_manual, reported_addresses, tls_pubkey, added_by, listens";
|
||||
|
||||
private readonly SqliteDatabase _db;
|
||||
|
||||
@@ -35,10 +35,10 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
_db.ExecuteAsync(c =>
|
||||
{
|
||||
c.Exec(
|
||||
"UPDATE agents SET public_host = CASE WHEN public_host = '' THEN @host ELSE public_host END, probe_tcp_port = @tcp, probe_udp_ports = @udp, " +
|
||||
"UPDATE agents SET public_host = CASE WHEN public_host = '' THEN @host ELSE public_host END, tcp_port = @tcp, quic_ports = @quic, listens = @listens, " +
|
||||
"kinds = CASE WHEN kinds = '[]' THEN @kinds ELSE kinds END, row_version = row_version + 1 WHERE id = @id",
|
||||
("@id", id.Value), ("@host", enrollment.PublicHost), ("@tcp", enrollment.ProbeTcpPort),
|
||||
("@udp", StorageJson.ToJson(enrollment.ProbeUdpPorts)), ("@kinds", StorageJson.ToJson(enrollment.Kinds)));
|
||||
("@id", id.Value), ("@host", enrollment.PublicHost), ("@tcp", enrollment.TcpPort), ("@listens", enrollment.TcpPort > 0 ? 1 : 0),
|
||||
("@quic", StorageJson.ToJson(enrollment.QuicPorts)), ("@kinds", StorageJson.ToJson(enrollment.Kinds)));
|
||||
}, ct);
|
||||
|
||||
public ValueTask<Result<AgentRecord>> PatchAsync(NodeId id, AgentPatch patch, long? expectedRowVersion, CancellationToken ct) =>
|
||||
@@ -111,8 +111,8 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
// 旧记录换成新公钥,历史数据保留;待审批记录删除
|
||||
c.Exec("DELETE FROM agents WHERE id = @id", ("@id", pendingId.Value));
|
||||
c.Exec(
|
||||
"UPDATE agents SET pubkey = @pubkey, tls_spki = @spki, status = 'active', task_hash = NULL, row_version = row_version + 1 WHERE id = @id",
|
||||
("@id", existingId), ("@pubkey", pending.PublicKey), ("@spki", pending.TlsSpki));
|
||||
"UPDATE agents SET pubkey = @pubkey, tls_spki = @spki, tls_pubkey = @peer, status = 'active', task_hash = NULL, row_version = row_version + 1 WHERE id = @id",
|
||||
("@id", existingId), ("@pubkey", pending.PublicKey), ("@spki", pending.TlsSpki), ("@peer", pending.TlsPublicKey));
|
||||
return Result<AgentRecord>.Ok(GetInternal(c, new NodeId(existingId))!);
|
||||
}, ct);
|
||||
|
||||
@@ -122,13 +122,15 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
public ValueTask ApplyHelloAsync(NodeId id, AgentHello hello, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(c =>
|
||||
{
|
||||
ListenInfo? listen = hello.Listen;
|
||||
c.Exec(
|
||||
"UPDATE agents SET kinds = @kinds, agent_version = @version, local_limits = COALESCE(@limits, local_limits), tls_spki = COALESCE(@spki, tls_spki), tls_pubkey = COALESCE(@pub, tls_pubkey), " +
|
||||
"probe_tcp_port = COALESCE(@tcp, probe_tcp_port), probe_udp_ports = COALESCE(@udp, probe_udp_ports), ipv6_supported = @v6, ipv6_diagnostics = @diag, " +
|
||||
"UPDATE agents SET kinds = @kinds, agent_version = @version, local_limits = COALESCE(@limits, local_limits), listens = @listens, " +
|
||||
"tcp_port = COALESCE(@tcp, tcp_port), quic_ports = COALESCE(@quic, quic_ports), ipv6_supported = @v6, ipv6_diagnostics = @diag, " +
|
||||
"reported_addresses = COALESCE(@addr, reported_addresses), row_version = row_version + 1 WHERE id = @id",
|
||||
("@id", id.Value), ("@kinds", StorageJson.ToJson(hello.Kinds)), ("@version", hello.AgentVersion), ("@limits", hello.LocalLimitsJson),
|
||||
("@spki", hello.TlsSpki), ("@pub", hello.TlsPublicKey), ("@tcp", hello.ProbeTcpPort), ("@udp", hello.ProbeUdpPorts is null ? null : StorageJson.ToJson(hello.ProbeUdpPorts)), ("@v6", hello.Ipv6Supported ? 1 : 0), ("@diag", hello.Ipv6DiagnosticsJson),
|
||||
("@addr", hello.Addresses is null ? null : StorageJson.ToJson(hello.Addresses)));
|
||||
("@listens", listen is null ? 0 : 1), ("@tcp", listen?.TcpPort), ("@quic", listen is null ? null : StorageJson.ToJson(listen.QuicPorts)),
|
||||
("@v6", listen?.Ipv6Supported == true ? 1 : 0), ("@diag", hello.Ipv6DiagnosticsJson),
|
||||
("@addr", listen?.Addresses is null ? null : StorageJson.ToJson(listen.Addresses)));
|
||||
}, ct);
|
||||
|
||||
public ValueTask ApplyAutoAddressesAsync(NodeId id, string? publicHost, string? ipv6Host, CancellationToken ct) =>
|
||||
@@ -141,6 +143,9 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
("@id", id.Value), ("@host", publicHost), ("@v6", ipv6Host));
|
||||
}, ct);
|
||||
|
||||
public ValueTask SetIpv6DiagnosticsAsync(NodeId id, string diagnosticsJson, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(c => c.Exec("UPDATE agents SET ipv6_diagnostics = @diag WHERE id = @id", ("@diag", diagnosticsJson), ("@id", id.Value)), ct);
|
||||
|
||||
public ValueTask SetTaskVersionAsync(NodeId id, long version, string taskHash, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(c => c.Exec("UPDATE agents SET task_version = @v, task_hash = @h WHERE id = @id", ("@id", id.Value), ("@v", version), ("@h", taskHash)), ct);
|
||||
|
||||
@@ -173,53 +178,20 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
public ValueTask<Result<NodeRecord>> CreateNodeAsync(NewNode node, CancellationToken ct) =>
|
||||
_db.WriteAsync(c =>
|
||||
{
|
||||
if (node.Kind == JoinTokenService.ProbeRole)
|
||||
{
|
||||
return CreateProbe(c, node);
|
||||
}
|
||||
|
||||
if (node.Status != NodeStatus.Pending && NameTaken(c, node.Name, node.Id))
|
||||
{
|
||||
return Result<NodeRecord>.Fail(FleetErrors.NameConflict, "a node with this name already exists");
|
||||
}
|
||||
|
||||
c.Exec(
|
||||
"INSERT INTO agents (id, name, status, pubkey, tls_spki, labels, meta, agent_version, created_at, added_by) VALUES (@id, @name, @status, @pubkey, @spki, @labels, @meta, @version, @now, @by)",
|
||||
("@id", node.Id.Value), ("@name", node.Name), ("@status", StatusText(node.Status)), ("@pubkey", node.PublicKey), ("@spki", node.TlsSpki),
|
||||
"INSERT INTO agents (id, name, status, pubkey, tls_spki, tls_pubkey, labels, meta, agent_version, created_at, added_by) VALUES (@id, @name, @status, @pubkey, @spki, @peer, @labels, @meta, @version, @now, @by)",
|
||||
("@id", node.Id.Value), ("@name", node.Name), ("@status", StatusText(node.Status)), ("@pubkey", node.PublicKey), ("@spki", node.TlsSpki), ("@peer", node.TlsPublicKey),
|
||||
("@labels", StorageJson.ToJson(node.Labels)), ("@meta", node.MetaJson), ("@version", node.NodeVersion), ("@now", node.NowMs), ("@by", node.AddedBy));
|
||||
return Result<NodeRecord>.Ok(ToNode(GetInternal(c, node.Id)!, node.MetaJson));
|
||||
}, ct);
|
||||
|
||||
/// <summary>Probe 用加入令牌注册:直接进 probes 表,有令牌就算批准(Probe 没有 pending 状态);两把公钥都是它自己生成的。</summary>
|
||||
private static Result<NodeRecord> CreateProbe(SqliteConnection c, NewNode node)
|
||||
{
|
||||
if (node.TlsPublicKey is not { Length: > 0 } peerKey)
|
||||
{
|
||||
return Result<NodeRecord>.Fail(FleetErrors.InvalidParams, "a probe needs its peer key");
|
||||
}
|
||||
|
||||
if (c.Scalar<long>("SELECT COUNT(*) FROM probes WHERE name = @name COLLATE NOCASE", ("@name", node.Name)) > 0)
|
||||
{
|
||||
return Result<NodeRecord>.Fail(FleetErrors.NameConflict, "a probe with this name already exists");
|
||||
}
|
||||
|
||||
c.Exec(
|
||||
"INSERT INTO probes (id, name, pubkey, peer_pubkey, created_at, added_by) VALUES (@id, @name, @pubkey, @peer, @now, @by)",
|
||||
("@id", node.Id.Value), ("@name", node.Name), ("@pubkey", node.PublicKey), ("@peer", peerKey), ("@now", node.NowMs), ("@by", node.AddedBy));
|
||||
return Result<NodeRecord>.Ok(new NodeRecord(node.Id, node.Name, NodeStatus.Active, node.PublicKey, node.TlsSpki, node.Labels, node.MetaJson, node.NodeVersion, node.NowMs, null));
|
||||
}
|
||||
|
||||
/// <summary>Agent 在 agents 表里;Probe 的身份在 probes 表里(吊销之后按「禁用」处理,连不上)。</summary>
|
||||
async ValueTask<NodeRecord?> IFleetStore.GetNodeAsync(NodeId id, CancellationToken ct) =>
|
||||
await GetAsync(id, ct) is { } agent
|
||||
? ToNode(agent, null)
|
||||
: await _db.ReadAsync(
|
||||
c => c.QueryOne(
|
||||
"SELECT id, name, pubkey, created_at, revoked_at FROM probes WHERE id = @id",
|
||||
r => new NodeRecord(
|
||||
new NodeId(r.GetString(0)), r.GetString(1), r.IsDBNull(4) ? NodeStatus.Active : NodeStatus.Disabled, r.Bytes(2), [], [], null, null, r.GetInt64(3), null),
|
||||
("@id", id.Value)),
|
||||
ct);
|
||||
ValueTask<NodeRecord?> IFleetStore.GetNodeAsync(NodeId id, CancellationToken ct) =>
|
||||
_db.ReadAsync(c => GetInternal(c, id) is { } agent ? ToNode(agent, null) : null, ct);
|
||||
|
||||
public ValueTask<NodeRecord?> FindEstablishedNodeByNameAsync(string name, CancellationToken ct) =>
|
||||
_db.ReadAsync(
|
||||
@@ -268,7 +240,8 @@ public sealed class SqliteAgentStore : IAgentStore, IFleetStore
|
||||
r.GetInt64(23) != 0,
|
||||
StorageJson.Strings(r.GetString(24)),
|
||||
r.NullableBytes(25),
|
||||
r.NullableString(26));
|
||||
r.NullableString(26),
|
||||
r.GetInt64(27) != 0);
|
||||
|
||||
private static NodeRecord ToNode(AgentRecord a, string? metaJson) =>
|
||||
new(a.Id, a.Name, a.Status, a.PublicKey, a.TlsSpki, a.Labels, metaJson, a.AgentVersion, a.CreatedAtMs, a.LastSeenAtMs);
|
||||
|
||||
@@ -47,44 +47,6 @@ public sealed class SqliteJoinTokenStore : IJoinTokenStore
|
||||
_db.WriteAsync(c => c.Exec("DELETE FROM join_tokens WHERE id = @id", ("@id", id)) == 1, ct);
|
||||
}
|
||||
|
||||
public sealed class SqliteProbeStore : IProbeStore
|
||||
{
|
||||
private readonly SqliteDatabase _db;
|
||||
|
||||
public SqliteProbeStore(SqliteDatabase db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public ValueTask<IReadOnlyList<ProbeRecord>> ListAsync(CancellationToken ct) =>
|
||||
_db.ReadAsync<IReadOnlyList<ProbeRecord>>(
|
||||
c => c.Query("SELECT id, name, pubkey, created_at, revoked_at, ipv6_diagnostics, peer_pubkey, added_by FROM probes ORDER BY created_at, id", r => new ProbeRecord(r.GetString(0), r.GetString(1), r.Bytes(2), r.GetInt64(3), r.NullableLong(4), r.NullableString(5), r.NullableBytes(6), r.NullableString(7))),
|
||||
ct);
|
||||
|
||||
public ValueTask<ProbeRecord?> GetAsync(string id, CancellationToken ct) =>
|
||||
_db.ReadAsync(
|
||||
c => c.QueryOne("SELECT id, name, pubkey, created_at, revoked_at, ipv6_diagnostics, peer_pubkey, added_by FROM probes WHERE id = @id", r => new ProbeRecord(r.GetString(0), r.GetString(1), r.Bytes(2), r.GetInt64(3), r.NullableLong(4), r.NullableString(5), r.NullableBytes(6), r.NullableString(7)), ("@id", id)),
|
||||
ct);
|
||||
|
||||
public ValueTask SetIpv6DiagnosticsAsync(string id, string diagnosticsJson, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(c => c.Exec("UPDATE probes SET ipv6_diagnostics = @diag WHERE id = @id", ("@diag", diagnosticsJson), ("@id", id)), ct);
|
||||
|
||||
public ValueTask<bool> RevokeAsync(string id, long nowMs, CancellationToken ct) =>
|
||||
_db.WriteAsync(c => c.Exec("UPDATE probes SET revoked_at = @now WHERE id = @id AND revoked_at IS NULL", ("@id", id), ("@now", nowMs)) == 1, ct);
|
||||
|
||||
public ValueTask<bool> DeleteAsync(string id, CancellationToken ct) =>
|
||||
_db.WriteAsync(c => c.Exec("DELETE FROM probes WHERE id = @id", ("@id", id)) == 1, ct);
|
||||
|
||||
public ValueTask SaveAsync(ProbeRecord probe, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(c =>
|
||||
{
|
||||
c.Exec(
|
||||
"INSERT INTO probes (id, name, pubkey, peer_pubkey, created_at, revoked_at) VALUES (@id, @name, @pubkey, @peer, @at, @revoked) " +
|
||||
"ON CONFLICT(id) DO UPDATE SET name = excluded.name, pubkey = excluded.pubkey, peer_pubkey = excluded.peer_pubkey, created_at = excluded.created_at, revoked_at = excluded.revoked_at",
|
||||
("@id", probe.Id), ("@name", probe.Name), ("@pubkey", probe.PublicKey), ("@peer", probe.PeerPublicKey), ("@at", probe.CreatedAtMs), ("@revoked", probe.RevokedAtMs));
|
||||
}, ct);
|
||||
}
|
||||
|
||||
public sealed class SqliteSettingsStore : ISettingsStore
|
||||
{
|
||||
private readonly SqliteDatabase _db;
|
||||
|
||||
@@ -14,18 +14,18 @@ public sealed class SqliteNodeMetaStore : INodeMetaStore
|
||||
|
||||
public ValueTask<IReadOnlyList<NodeMeta>> ListAsync(CancellationToken ct) =>
|
||||
_db.ReadAsync<IReadOnlyList<NodeMeta>>(
|
||||
c => c.Query("SELECT node_id, kind, os, os_name, arch, country, isp_key, isp_name, geo_host, geo_at, updated_at, version, tag, commit_id, built_at, isp_manual FROM node_meta", Read),
|
||||
c => c.Query("SELECT node_id, os, os_name, arch, country, isp_key, isp_name, geo_host, geo_at, updated_at, version, tag, commit_id, built_at, isp_manual FROM node_meta", Read),
|
||||
ct);
|
||||
|
||||
public ValueTask SaveAsync(NodeMeta meta, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(
|
||||
c => c.Exec(
|
||||
"INSERT INTO node_meta (node_id, kind, os, os_name, arch, country, isp_key, isp_name, geo_host, geo_at, updated_at, version, tag, commit_id, built_at, isp_manual) " +
|
||||
"VALUES (@id, @kind, @os, @osn, @arch, @cc, @ik, @in, @gh, @ga, @ua, @ver, @tag, @cm, @bt, @im) ON CONFLICT(node_id) DO UPDATE SET " +
|
||||
"kind = excluded.kind, os = excluded.os, os_name = excluded.os_name, arch = excluded.arch, country = excluded.country, " +
|
||||
"INSERT INTO node_meta (node_id, os, os_name, arch, country, isp_key, isp_name, geo_host, geo_at, updated_at, version, tag, commit_id, built_at, isp_manual) " +
|
||||
"VALUES (@id, @os, @osn, @arch, @cc, @ik, @in, @gh, @ga, @ua, @ver, @tag, @cm, @bt, @im) ON CONFLICT(node_id) DO UPDATE SET " +
|
||||
"os = excluded.os, os_name = excluded.os_name, arch = excluded.arch, country = excluded.country, " +
|
||||
"isp_key = excluded.isp_key, isp_name = excluded.isp_name, geo_host = excluded.geo_host, geo_at = excluded.geo_at, updated_at = excluded.updated_at, " +
|
||||
"version = excluded.version, tag = excluded.tag, commit_id = excluded.commit_id, built_at = excluded.built_at, isp_manual = excluded.isp_manual",
|
||||
("@id", meta.Info.NodeId), ("@kind", meta.Info.Kind), ("@os", meta.Info.Os), ("@osn", meta.Info.OsName), ("@arch", meta.Info.Arch), ("@cc", meta.Info.Country),
|
||||
("@id", meta.Info.NodeId), ("@os", meta.Info.Os), ("@osn", meta.Info.OsName), ("@arch", meta.Info.Arch), ("@cc", meta.Info.Country),
|
||||
("@ik", meta.Info.IspKey), ("@in", meta.Info.IspName), ("@gh", meta.GeoHost), ("@ga", meta.GeoAtMs), ("@ua", meta.UpdatedAtMs),
|
||||
("@ver", meta.Info.Version), ("@tag", meta.Info.Tag), ("@cm", meta.Info.Commit), ("@bt", meta.Info.BuiltAt), ("@im", meta.Info.IspManual ? 1 : 0)),
|
||||
ct);
|
||||
@@ -36,6 +36,6 @@ public sealed class SqliteNodeMetaStore : INodeMetaStore
|
||||
private static NodeMeta Read(SqliteDataReader r)
|
||||
{
|
||||
static string? S(SqliteDataReader reader, int i) => reader.IsDBNull(i) ? null : reader.GetString(i);
|
||||
return new NodeMeta(new NodeInfo(r.GetString(0), r.GetString(1), S(r, 2), S(r, 3), S(r, 4), S(r, 5), S(r, 6), S(r, 7), S(r, 11), S(r, 12), S(r, 13), S(r, 14), r.GetInt64(15) != 0), S(r, 8), r.GetInt64(9), r.GetInt64(10));
|
||||
return new NodeMeta(new NodeInfo(r.GetString(0), S(r, 1), S(r, 2), S(r, 3), S(r, 4), S(r, 5), S(r, 6), S(r, 10), S(r, 11), S(r, 12), S(r, 13), r.GetInt64(14) != 0), S(r, 7), r.GetInt64(8), r.GetInt64(9));
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace MoShiro.Backend.Storage.Sqlite;
|
||||
|
||||
public sealed class SqliteTestStore : ITestStore
|
||||
{
|
||||
private const string Columns = "id, agent_id, plan, params, trigger, status, error_code, phase, phase_count, requested_at, started_at, finished_at, probe_id, ip_version";
|
||||
private const string Columns = "id, agent_id, plan, params, trigger, status, error_code, phase, phase_count, requested_at, started_at, finished_at, dialer_id, ip_version";
|
||||
|
||||
private readonly SqliteDatabase _db;
|
||||
|
||||
@@ -19,22 +19,22 @@ public sealed class SqliteTestStore : ITestStore
|
||||
public ValueTask CreateAsync(TestRecord test, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(
|
||||
c => c.Exec(
|
||||
"INSERT INTO tests (id, agent_id, probe_id, plan, params, trigger, status, error_code, phase, phase_count, requested_at, ip_version) VALUES (@id, @agent, @probe, @plan, @params, @trigger, @status, @err, @phase, @count, @at, @ip)",
|
||||
("@id", test.Id.Value), ("@agent", test.AgentId.Value), ("@probe", test.ProbeId.Value), ("@plan", test.Plan), ("@params", test.ParamsJson), ("@trigger", test.Trigger),
|
||||
"INSERT INTO tests (id, agent_id, dialer_id, plan, params, trigger, status, error_code, phase, phase_count, requested_at, ip_version) VALUES (@id, @agent, @dialer, @plan, @params, @trigger, @status, @err, @phase, @count, @at, @ip)",
|
||||
("@id", test.Id.Value), ("@agent", test.AgentId.Value), ("@dialer", test.DialerId.Value), ("@plan", test.Plan), ("@params", test.ParamsJson), ("@trigger", test.Trigger),
|
||||
("@status", test.Status), ("@err", test.ErrorCode), ("@phase", test.Phase), ("@count", test.PhaseCount), ("@at", test.RequestedAtMs), ("@ip", test.IpVersion)),
|
||||
ct);
|
||||
|
||||
public ValueTask<TestRecord?> GetAsync(TestId id, CancellationToken ct) =>
|
||||
_db.ReadAsync(c => c.QueryOne($"SELECT {Columns} FROM tests WHERE id = @id", Read, ("@id", id.Value)), ct);
|
||||
|
||||
public ValueTask<IReadOnlyList<TestRecord>> ListAsync(NodeId? agentId, ProbeId? probeId, int limit, CancellationToken ct) =>
|
||||
ListAsync(agentId, probeId, limit, null, ct);
|
||||
public ValueTask<IReadOnlyList<TestRecord>> ListAsync(NodeId? agentId, NodeId? dialerId, int limit, CancellationToken ct) =>
|
||||
ListAsync(agentId, dialerId, limit, null, ct);
|
||||
|
||||
public ValueTask<IReadOnlyList<TestRecord>> ListAsync(NodeId? agentId, ProbeId? probeId, int limit, string? ipVersion, CancellationToken ct) =>
|
||||
public ValueTask<IReadOnlyList<TestRecord>> ListAsync(NodeId? agentId, NodeId? dialerId, int limit, string? ipVersion, CancellationToken ct) =>
|
||||
_db.ReadAsync<IReadOnlyList<TestRecord>>(
|
||||
c => c.Query(
|
||||
$"SELECT {Columns} FROM tests WHERE (@agent IS NULL OR agent_id = @agent) AND (@probe IS NULL OR probe_id = @probe) AND (@ip IS NULL OR ip_version = @ip) ORDER BY requested_at DESC, id DESC LIMIT @limit",
|
||||
Read, ("@agent", agentId?.Value), ("@probe", probeId?.Value), ("@limit", limit), ("@ip", ipVersion)),
|
||||
$"SELECT {Columns} FROM tests WHERE (@agent IS NULL OR agent_id = @agent) AND (@dialer IS NULL OR dialer_id = @dialer) AND (@ip IS NULL OR ip_version = @ip) ORDER BY requested_at DESC, id DESC LIMIT @limit",
|
||||
Read, ("@agent", agentId?.Value), ("@dialer", dialerId?.Value), ("@limit", limit), ("@ip", ipVersion)),
|
||||
ct);
|
||||
|
||||
public ValueTask<bool> UpdateStatusAsync(TestId id, string status, int? phase, string? errorCode, long nowMs, CancellationToken ct) =>
|
||||
@@ -49,8 +49,8 @@ public sealed class SqliteTestStore : ITestStore
|
||||
public ValueTask SetPhaseCountAsync(TestId id, int phaseCount, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(c => c.Exec("UPDATE tests SET phase_count = @n WHERE id = @id", ("@id", id.Value), ("@n", phaseCount)), ct);
|
||||
|
||||
public ValueTask<bool> HasActiveAsync(NodeId agentId, ProbeId probeId, CancellationToken ct) =>
|
||||
_db.ReadAsync(c => c.Scalar<long>("SELECT COUNT(*) FROM tests WHERE agent_id = @a AND probe_id = @p AND status IN ('queued', 'running')", ("@a", agentId.Value), ("@p", probeId.Value)) > 0, ct);
|
||||
public ValueTask<bool> HasActiveAsync(NodeId agentId, NodeId dialerId, CancellationToken ct) =>
|
||||
_db.ReadAsync(c => c.Scalar<long>("SELECT COUNT(*) FROM tests WHERE agent_id = @a AND dialer_id = @p AND status IN ('queued', 'running')", ("@a", agentId.Value), ("@p", dialerId.Value)) > 0, ct);
|
||||
|
||||
public ValueTask SaveResultAsync(TestResultRecord result, CancellationToken ct) =>
|
||||
_db.ExecuteAsync(
|
||||
@@ -81,7 +81,7 @@ public sealed class SqliteTestStore : ITestStore
|
||||
private static TestRecord Read(SqliteDataReader r) => new(
|
||||
new TestId(r.GetString(0)),
|
||||
new NodeId(r.GetString(1)),
|
||||
new ProbeId(r.GetString(12)),
|
||||
new NodeId(r.GetString(12)),
|
||||
r.GetString(2),
|
||||
r.GetString(3),
|
||||
r.GetString(4),
|
||||
|
||||
@@ -14,7 +14,6 @@ public static class StorageServiceCollectionExtensions
|
||||
services.AddSingleton<IAgentStore>(p => p.GetRequiredService<SqliteAgentStore>());
|
||||
services.AddSingleton<IFleetStore>(p => p.GetRequiredService<SqliteAgentStore>());
|
||||
services.AddSingleton<IJoinTokenStore, SqliteJoinTokenStore>();
|
||||
services.AddSingleton<IProbeStore, SqliteProbeStore>();
|
||||
services.AddSingleton<ISettingsStore, SqliteSettingsStore>();
|
||||
services.AddSingleton<ITaskStore, SqliteTaskStore>();
|
||||
services.AddSingleton<ITestStore, SqliteTestStore>();
|
||||
|
||||
Reference in new issue
Block a user