diff --git a/seeds/postgres/dev_seed.sql b/seeds/postgres/dev_seed.sql new file mode 100644 index 0000000..35ec47c --- /dev/null +++ b/seeds/postgres/dev_seed.sql @@ -0,0 +1,63 @@ +-- dev_seed.sql (PostgreSQL) — development test data +-- +-- Running this script is idempotent: existing rows are left untouched +-- and missing rows are inserted. Safe to run multiple times. +-- +-- Load with: cargo run --features ssr --bin seed + +-- ── Networks ────────────────────────────────────────────────────────────────── + +INSERT INTO networks (cidr) VALUES + ('192.168.1.0/24'), + ('192.168.10.0/24'), + ('10.0.0.0/8'), + ('172.16.0.0/16') +ON CONFLICT (cidr) DO NOTHING; + +-- ── Hosts ───────────────────────────────────────────────────────────────────── +-- Hosts have no UNIQUE constraint, so we guard with WHERE NOT EXISTS. + +-- LAN — 192.168.1.0/24 +INSERT INTO hosts (name, ip, network_id) +SELECT name, ip, (SELECT id FROM networks WHERE cidr = '192.168.1.0/24') +FROM (VALUES + ('gateway', '192.168.1.1'), + ('workstation-01', '192.168.1.10'), + ('workstation-02', '192.168.1.11'), + ('workstation-03', '192.168.1.12'), + ('nas-01', '192.168.1.20'), + ('printer-01', '192.168.1.50') +) AS t(name, ip) +WHERE NOT EXISTS (SELECT 1 FROM hosts WHERE hosts.name = t.name AND hosts.ip = t.ip); + +-- DMZ — 192.168.10.0/24 +INSERT INTO hosts (name, ip, network_id) +SELECT name, ip, (SELECT id FROM networks WHERE cidr = '192.168.10.0/24') +FROM (VALUES + ('web-server-01', '192.168.10.10'), + ('web-server-02', '192.168.10.11'), + ('db-server-01', '192.168.10.20'), + ('mail-server-01', '192.168.10.30') +) AS t(name, ip) +WHERE NOT EXISTS (SELECT 1 FROM hosts WHERE hosts.name = t.name AND hosts.ip = t.ip); + +-- Corporate backbone — 10.0.0.0/8 +INSERT INTO hosts (name, ip, network_id) +SELECT name, ip, (SELECT id FROM networks WHERE cidr = '10.0.0.0/8') +FROM (VALUES + ('core-switch-01', '10.0.0.1'), + ('monitoring-01', '10.0.1.10'), + ('backup-server-01', '10.0.1.20'), + ('log-server-01', '10.0.1.30') +) AS t(name, ip) +WHERE NOT EXISTS (SELECT 1 FROM hosts WHERE hosts.name = t.name AND hosts.ip = t.ip); + +-- VPN — 172.16.0.0/16 +INSERT INTO hosts (name, ip, network_id) +SELECT name, ip, (SELECT id FROM networks WHERE cidr = '172.16.0.0/16') +FROM (VALUES + ('vpn-gateway-01', '172.16.0.1'), + ('vpn-client-01', '172.16.1.10'), + ('vpn-client-02', '172.16.1.11') +) AS t(name, ip) +WHERE NOT EXISTS (SELECT 1 FROM hosts WHERE hosts.name = t.name AND hosts.ip = t.ip); diff --git a/seeds/sqlite/dev_seed.sql b/seeds/sqlite/dev_seed.sql new file mode 100644 index 0000000..0e1e4d6 --- /dev/null +++ b/seeds/sqlite/dev_seed.sql @@ -0,0 +1,43 @@ +-- dev_seed.sql (SQLite) — development test data +-- +-- Running this script is idempotent: existing rows are left untouched +-- and missing rows are inserted. Safe to run multiple times. +-- +-- Load with: cargo run --features ssr --bin seed + +-- ── Networks ────────────────────────────────────────────────────────────────── +-- INSERT OR IGNORE relies on the UNIQUE constraint on cidr. + +INSERT OR IGNORE INTO networks (cidr) VALUES ('192.168.1.0/24'); +INSERT OR IGNORE INTO networks (cidr) VALUES ('192.168.10.0/24'); +INSERT OR IGNORE INTO networks (cidr) VALUES ('10.0.0.0/8'); +INSERT OR IGNORE INTO networks (cidr) VALUES ('172.16.0.0/16'); + +-- ── Hosts ───────────────────────────────────────────────────────────────────── +-- Hosts have no UNIQUE constraint, so we guard each insert with WHERE NOT EXISTS. +-- Network IDs are resolved by subquery on cidr for portability. + +-- LAN — 192.168.1.0/24 +INSERT INTO hosts (name, ip, network_id) SELECT 'gateway', '192.168.1.1', id FROM networks WHERE cidr = '192.168.1.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'gateway' AND ip = '192.168.1.1'); +INSERT INTO hosts (name, ip, network_id) SELECT 'workstation-01', '192.168.1.10', id FROM networks WHERE cidr = '192.168.1.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'workstation-01' AND ip = '192.168.1.10'); +INSERT INTO hosts (name, ip, network_id) SELECT 'workstation-02', '192.168.1.11', id FROM networks WHERE cidr = '192.168.1.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'workstation-02' AND ip = '192.168.1.11'); +INSERT INTO hosts (name, ip, network_id) SELECT 'workstation-03', '192.168.1.12', id FROM networks WHERE cidr = '192.168.1.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'workstation-03' AND ip = '192.168.1.12'); +INSERT INTO hosts (name, ip, network_id) SELECT 'nas-01', '192.168.1.20', id FROM networks WHERE cidr = '192.168.1.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'nas-01' AND ip = '192.168.1.20'); +INSERT INTO hosts (name, ip, network_id) SELECT 'printer-01', '192.168.1.50', id FROM networks WHERE cidr = '192.168.1.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'printer-01' AND ip = '192.168.1.50'); + +-- DMZ — 192.168.10.0/24 +INSERT INTO hosts (name, ip, network_id) SELECT 'web-server-01', '192.168.10.10', id FROM networks WHERE cidr = '192.168.10.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'web-server-01' AND ip = '192.168.10.10'); +INSERT INTO hosts (name, ip, network_id) SELECT 'web-server-02', '192.168.10.11', id FROM networks WHERE cidr = '192.168.10.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'web-server-02' AND ip = '192.168.10.11'); +INSERT INTO hosts (name, ip, network_id) SELECT 'db-server-01', '192.168.10.20', id FROM networks WHERE cidr = '192.168.10.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'db-server-01' AND ip = '192.168.10.20'); +INSERT INTO hosts (name, ip, network_id) SELECT 'mail-server-01', '192.168.10.30', id FROM networks WHERE cidr = '192.168.10.0/24' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'mail-server-01' AND ip = '192.168.10.30'); + +-- Corporate backbone — 10.0.0.0/8 +INSERT INTO hosts (name, ip, network_id) SELECT 'core-switch-01', '10.0.0.1', id FROM networks WHERE cidr = '10.0.0.0/8' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'core-switch-01' AND ip = '10.0.0.1'); +INSERT INTO hosts (name, ip, network_id) SELECT 'monitoring-01', '10.0.1.10', id FROM networks WHERE cidr = '10.0.0.0/8' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'monitoring-01' AND ip = '10.0.1.10'); +INSERT INTO hosts (name, ip, network_id) SELECT 'backup-server-01', '10.0.1.20', id FROM networks WHERE cidr = '10.0.0.0/8' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'backup-server-01' AND ip = '10.0.1.20'); +INSERT INTO hosts (name, ip, network_id) SELECT 'log-server-01', '10.0.1.30', id FROM networks WHERE cidr = '10.0.0.0/8' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'log-server-01' AND ip = '10.0.1.30'); + +-- VPN — 172.16.0.0/16 +INSERT INTO hosts (name, ip, network_id) SELECT 'vpn-gateway-01', '172.16.0.1', id FROM networks WHERE cidr = '172.16.0.0/16' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'vpn-gateway-01' AND ip = '172.16.0.1'); +INSERT INTO hosts (name, ip, network_id) SELECT 'vpn-client-01', '172.16.1.10', id FROM networks WHERE cidr = '172.16.0.0/16' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'vpn-client-01' AND ip = '172.16.1.10'); +INSERT INTO hosts (name, ip, network_id) SELECT 'vpn-client-02', '172.16.1.11', id FROM networks WHERE cidr = '172.16.0.0/16' AND NOT EXISTS (SELECT 1 FROM hosts WHERE name = 'vpn-client-02' AND ip = '172.16.1.11'); diff --git a/src/bin/seed.rs b/src/bin/seed.rs new file mode 100644 index 0000000..5b9cf09 --- /dev/null +++ b/src/bin/seed.rs @@ -0,0 +1,89 @@ +// bin/seed.rs — Development seed loader +// +// Inserts a realistic set of networks and hosts into the database so the UI +// can be tested without manual data entry. +// +// The seed is idempotent: running it multiple times never duplicates rows. +// +// Usage: +// cargo run --features ssr --bin seed +// +// The DATABASE_URL is read from the .env file (or environment variable), +// exactly like the main server. + +#[tokio::main] +async fn main() { + use rust_ipam::server::{ + config::{AppConfig, DatabaseBackend}, + db::{create_pool, run_migrations}, + }; + + // Load .env so DATABASE_URL is available without exporting it manually. + // Errors are ignored: the variable may already be set in the environment. + let _ = dotenvy::dotenv(); + + tracing_subscriber::fmt() + .with_env_filter( + std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), + ) + .init(); + + let config = AppConfig::from_env() + .expect("Configuration error — check DATABASE_URL in your .env file"); + + tracing::info!("Connecting to {} ({})", config.backend, config.database_url); + + let pool = create_pool(&config) + .await + .expect("Failed to connect to database"); + + run_migrations(&pool, &config.backend) + .await + .expect("Migration failed"); + + // Pick the seed file that matches the active backend. + // `include_str!` embeds the SQL at compile time so the binary is self-contained. + let sql = match config.backend { + DatabaseBackend::Sqlite => include_str!("../../seeds/sqlite/dev_seed.sql"), + DatabaseBackend::Postgres => include_str!("../../seeds/postgres/dev_seed.sql"), + }; + + // Strip comment lines first, then split on ';'. + // sqlx does not support multiple statements in a single `query()` call. + // Without pre-stripping comments, a block like "-- section\nINSERT …" + // would start with "--" and get incorrectly discarded. + let sql_stripped: String = sql + .lines() + .filter(|line| !line.trim().starts_with("--")) + .collect::>() + .join("\n"); + + let statements: Vec = sql_stripped + .split(';') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + let total = statements.len(); + for (i, stmt) in statements.iter().enumerate() { + sqlx::query(stmt) + .execute(&pool) + .await + .unwrap_or_else(|e| panic!("Statement {}/{} failed: {}\nSQL: {}", i + 1, total, e, stmt)); + } + + tracing::info!("Seed complete — {} statement(s) executed.", total); + + // Count what was inserted so the operator can confirm at a glance. + let network_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM networks") + .fetch_one(&pool) + .await + .unwrap_or(0); + + let host_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM hosts") + .fetch_one(&pool) + .await + .unwrap_or(0); + + tracing::info!("Database now contains {} network(s) and {} host(s).", network_count, host_count); +}