convergio-provisioning 0.1.9

Node provisioning — sync config, keys, binary to remote peers
Documentation
//! Extension trait implementation for provisioning.

use std::sync::Arc;

use convergio_db::pool::ConnPool;
use convergio_types::extension::{AppContext, Extension, Health, McpToolDef, Metric, Migration};
use convergio_types::manifest::{Capability, Manifest, ModuleKind};

use crate::routes::{provision_routes, ProvisionState};

pub struct ProvisioningExtension {
    pool: ConnPool,
}

impl ProvisioningExtension {
    pub fn new(pool: ConnPool) -> Self {
        Self { pool }
    }
}

impl Extension for ProvisioningExtension {
    fn manifest(&self) -> Manifest {
        Manifest {
            id: "convergio-provisioning".to_string(),
            description: "Node provisioning — sync config, keys, binary to remote peers"
                .to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
            kind: ModuleKind::Extension,
            provides: vec![Capability {
                name: "node-provisioning".to_string(),
                version: "1.0.0".to_string(),
                description: "Sync config/keys/binary to peers via rsync/SSH".to_string(),
            }],
            requires: vec![],
            agent_tools: vec![],
            required_roles: vec!["orchestrator".into(), "all".into()],
        }
    }

    fn routes(&self, _ctx: &AppContext) -> Option<axum::Router> {
        let state = Arc::new(ProvisionState {
            pool: self.pool.clone(),
        });
        Some(provision_routes(state))
    }

    fn migrations(&self) -> Vec<Migration> {
        vec![Migration {
            version: 1,
            description: "provisioning tables",
            up: "CREATE TABLE IF NOT EXISTS provision_runs (\
                    id INTEGER PRIMARY KEY,\
                    peer_name TEXT NOT NULL,\
                    ssh_target TEXT NOT NULL,\
                    status TEXT DEFAULT 'pending',\
                    items_total INTEGER DEFAULT 0,\
                    items_done INTEGER DEFAULT 0,\
                    started_at TEXT DEFAULT (datetime('now')),\
                    completed_at TEXT,\
                    error_message TEXT\
                );\
                CREATE INDEX IF NOT EXISTS idx_pr_peer \
                    ON provision_runs(peer_name);\
                CREATE INDEX IF NOT EXISTS idx_pr_status \
                    ON provision_runs(status);\
                CREATE TABLE IF NOT EXISTS provision_items (\
                    id INTEGER PRIMARY KEY,\
                    run_id INTEGER NOT NULL REFERENCES provision_runs(id),\
                    item_type TEXT NOT NULL,\
                    source_path TEXT,\
                    dest_path TEXT,\
                    status TEXT DEFAULT 'pending',\
                    bytes_transferred INTEGER DEFAULT 0,\
                    duration_ms INTEGER DEFAULT 0,\
                    error_message TEXT\
                );\
                CREATE INDEX IF NOT EXISTS idx_pi_run \
                    ON provision_items(run_id);",
        }]
    }

    fn health(&self) -> Health {
        match self.pool.get() {
            Ok(_) => Health::Ok,
            Err(e) => Health::Degraded {
                reason: format!("db: {e}"),
            },
        }
    }

    fn metrics(&self) -> Vec<Metric> {
        let run_count: f64 = self
            .pool
            .get()
            .ok()
            .and_then(|c| {
                c.query_row("SELECT COUNT(*) FROM provision_runs", [], |r| {
                    r.get::<_, i64>(0)
                })
                .ok()
            })
            .unwrap_or(0) as f64;
        vec![Metric {
            name: "provision_runs_total".to_string(),
            value: run_count,
            labels: vec![],
        }]
    }

    fn mcp_tools(&self) -> Vec<McpToolDef> {
        crate::mcp_defs::provisioning_tools()
    }
}