convergio_provisioning/
ext.rs1use std::sync::Arc;
4
5use convergio_db::pool::ConnPool;
6use convergio_types::extension::{AppContext, Extension, Health, McpToolDef, Metric, Migration};
7use convergio_types::manifest::{Capability, Manifest, ModuleKind};
8
9use crate::routes::{provision_routes, ProvisionState};
10
11pub struct ProvisioningExtension {
12 pool: ConnPool,
13}
14
15impl ProvisioningExtension {
16 pub fn new(pool: ConnPool) -> Self {
17 Self { pool }
18 }
19}
20
21impl Extension for ProvisioningExtension {
22 fn manifest(&self) -> Manifest {
23 Manifest {
24 id: "convergio-provisioning".to_string(),
25 description: "Node provisioning — sync config, keys, binary to remote peers"
26 .to_string(),
27 version: env!("CARGO_PKG_VERSION").to_string(),
28 kind: ModuleKind::Extension,
29 provides: vec![Capability {
30 name: "node-provisioning".to_string(),
31 version: "1.0.0".to_string(),
32 description: "Sync config/keys/binary to peers via rsync/SSH".to_string(),
33 }],
34 requires: vec![],
35 agent_tools: vec![],
36 required_roles: vec!["orchestrator".into(), "all".into()],
37 }
38 }
39
40 fn routes(&self, _ctx: &AppContext) -> Option<axum::Router> {
41 let state = Arc::new(ProvisionState {
42 pool: self.pool.clone(),
43 });
44 Some(provision_routes(state))
45 }
46
47 fn migrations(&self) -> Vec<Migration> {
48 vec![Migration {
49 version: 1,
50 description: "provisioning tables",
51 up: "CREATE TABLE IF NOT EXISTS provision_runs (\
52 id INTEGER PRIMARY KEY,\
53 peer_name TEXT NOT NULL,\
54 ssh_target TEXT NOT NULL,\
55 status TEXT DEFAULT 'pending',\
56 items_total INTEGER DEFAULT 0,\
57 items_done INTEGER DEFAULT 0,\
58 started_at TEXT DEFAULT (datetime('now')),\
59 completed_at TEXT,\
60 error_message TEXT\
61 );\
62 CREATE INDEX IF NOT EXISTS idx_pr_peer \
63 ON provision_runs(peer_name);\
64 CREATE INDEX IF NOT EXISTS idx_pr_status \
65 ON provision_runs(status);\
66 CREATE TABLE IF NOT EXISTS provision_items (\
67 id INTEGER PRIMARY KEY,\
68 run_id INTEGER NOT NULL REFERENCES provision_runs(id),\
69 item_type TEXT NOT NULL,\
70 source_path TEXT,\
71 dest_path TEXT,\
72 status TEXT DEFAULT 'pending',\
73 bytes_transferred INTEGER DEFAULT 0,\
74 duration_ms INTEGER DEFAULT 0,\
75 error_message TEXT\
76 );\
77 CREATE INDEX IF NOT EXISTS idx_pi_run \
78 ON provision_items(run_id);",
79 }]
80 }
81
82 fn health(&self) -> Health {
83 match self.pool.get() {
84 Ok(_) => Health::Ok,
85 Err(e) => Health::Degraded {
86 reason: format!("db: {e}"),
87 },
88 }
89 }
90
91 fn metrics(&self) -> Vec<Metric> {
92 let run_count: f64 = self
93 .pool
94 .get()
95 .ok()
96 .and_then(|c| {
97 c.query_row("SELECT COUNT(*) FROM provision_runs", [], |r| {
98 r.get::<_, i64>(0)
99 })
100 .ok()
101 })
102 .unwrap_or(0) as f64;
103 vec![Metric {
104 name: "provision_runs_total".to_string(),
105 value: run_count,
106 labels: vec![],
107 }]
108 }
109
110 fn mcp_tools(&self) -> Vec<McpToolDef> {
111 crate::mcp_defs::provisioning_tools()
112 }
113}