initial commit
This commit is contained in:
parent
4cc1853061
commit
a1f1376aaa
31
.gitignore
vendored
31
.gitignore
vendored
@ -1,22 +1,17 @@
|
|||||||
# ---> Rust
|
# Rust target
|
||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# Env files
|
||||||
*.pdb
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# RustRover
|
# Terraform drop/output directories
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
terraform_drop/
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# OS/editor junk
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
.DS_Store
|
||||||
#.idea/
|
*.swp
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|||||||
3099
Cargo.lock
generated
Normal file
3099
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
Normal file
27
Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"crates/api",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Corenthin"]
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
axum = "0.7"
|
||||||
|
config = "0.14"
|
||||||
|
dotenvy = "0.15"
|
||||||
|
rand = { version = "0.8", features = ["std"] }
|
||||||
|
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
schemars = "0.8"
|
||||||
|
sqlx = { version = "0.7", features = ["runtime-tokio", "mysql", "chrono", "uuid"] }
|
||||||
|
thiserror = "1"
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "process"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||||
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
22
config/default.toml
Normal file
22
config/default.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[service]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 3000
|
||||||
|
|
||||||
|
[database]
|
||||||
|
url = "mysql://user:password@localhost:3306/apiprojetsolyti"
|
||||||
|
max_connections = 10
|
||||||
|
|
||||||
|
|
||||||
|
[guacamole]
|
||||||
|
api_endpoint = "https://guacamole.local/api"
|
||||||
|
username = ""
|
||||||
|
password = ""
|
||||||
|
|
||||||
|
[pfsense]
|
||||||
|
api_endpoint = "https://pfsense.local/api/v1"
|
||||||
|
api_key = ""
|
||||||
|
api_secret = ""
|
||||||
|
|
||||||
|
[terraform]
|
||||||
|
# Local drop directory where API writes generated tfvars files
|
||||||
|
drop_dir = "terraform_drop"
|
||||||
1
crates/api/.gitignore
vendored
Normal file
1
crates/api/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
22
crates/api/Cargo.toml
Normal file
22
crates/api/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Automation API for Guacamole and pfSense orchestration with Terraform tfvars generation"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
axum = { workspace = true }
|
||||||
|
config = { workspace = true }
|
||||||
|
dotenvy = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
schemars = { workspace = true }
|
||||||
|
sqlx = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
30
crates/api/src/bootstrap.rs
Normal file
30
crates/api/src/bootstrap.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
use crate::{config::AppConfig, routes};
|
||||||
|
|
||||||
|
pub async fn run() -> Result<()> {
|
||||||
|
init_tracing();
|
||||||
|
|
||||||
|
let cfg = AppConfig::load()?;
|
||||||
|
let router = routes::build_router(cfg.clone()).await?;
|
||||||
|
|
||||||
|
let addr: SocketAddr = format!("{}:{}", cfg.service.host, cfg.service.port).parse()?;
|
||||||
|
tracing::info!(%addr, "Starting HTTP server");
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
axum::serve(listener, router).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_tracing() {
|
||||||
|
let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
||||||
|
let fmt_layer = tracing_subscriber::fmt::layer();
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter_layer)
|
||||||
|
.with(fmt_layer)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
16
crates/api/src/config/database.rs
Normal file
16
crates/api/src/config/database.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct DatabaseConfig {
|
||||||
|
pub url: String,
|
||||||
|
pub max_connections: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DatabaseConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
url: "mysql://user:password@localhost:3306/apiprojetsolyti".to_string(),
|
||||||
|
max_connections: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
crates/api/src/config/mod.rs
Normal file
79
crates/api/src/config/mod.rs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
pub mod database;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub service: ServiceConfig,
|
||||||
|
pub database: database::DatabaseConfig,
|
||||||
|
pub guacamole: GuacamoleConfig,
|
||||||
|
pub pfsense: PfSenseConfig,
|
||||||
|
pub terraform: TerraformConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ServiceConfig {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct GuacamoleConfig {
|
||||||
|
pub api_endpoint: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct PfSenseConfig {
|
||||||
|
pub api_endpoint: String,
|
||||||
|
pub api_key: String,
|
||||||
|
pub api_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct TerraformConfig {
|
||||||
|
pub drop_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
service: ServiceConfig {
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
database: database::DatabaseConfig::default(),
|
||||||
|
guacamole: GuacamoleConfig {
|
||||||
|
api_endpoint: String::new(),
|
||||||
|
username: String::new(),
|
||||||
|
password: String::new(),
|
||||||
|
},
|
||||||
|
pfsense: PfSenseConfig {
|
||||||
|
api_endpoint: String::new(),
|
||||||
|
api_key: String::new(),
|
||||||
|
api_secret: String::new(),
|
||||||
|
},
|
||||||
|
terraform: TerraformConfig {
|
||||||
|
drop_dir: "terraform_drop".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
pub fn load() -> anyhow::Result<Self> {
|
||||||
|
let _ = dotenvy::dotenv();
|
||||||
|
|
||||||
|
let builder = config::Config::builder()
|
||||||
|
.add_source(config::File::with_name("config/default.toml").required(false))
|
||||||
|
.add_source(
|
||||||
|
config::Environment::with_prefix("APP")
|
||||||
|
.try_parsing(true)
|
||||||
|
.separator("__"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = builder.build()?;
|
||||||
|
cfg.try_deserialize().map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
crates/api/src/domain/deployments.rs
Normal file
18
crates/api/src/domain/deployments.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Deployment {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub user_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deployment {
|
||||||
|
pub fn new(name: impl Into<String>, user_count: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: name.into(),
|
||||||
|
user_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/api/src/domain/mod.rs
Normal file
5
crates/api/src/domain/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod deployments;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
pub use deployments::Deployment;
|
||||||
|
pub use users::{GuacamoleUser, VmAssignment};
|
||||||
26
crates/api/src/domain/users.rs
Normal file
26
crates/api/src/domain/users.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GuacamoleUser {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VmAssignment {
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub vm_id: Uuid,
|
||||||
|
pub ip_address: String,
|
||||||
|
pub mac_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuacamoleUser {
|
||||||
|
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
username: username.into(),
|
||||||
|
password: password.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
crates/api/src/infrastructure/database.rs
Normal file
16
crates/api/src/infrastructure/database.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use sqlx::mysql::MySqlPoolOptions;
|
||||||
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
|
use crate::config::database::DatabaseConfig;
|
||||||
|
|
||||||
|
pub type DatabasePool = MySqlPool;
|
||||||
|
|
||||||
|
pub async fn create_pool(config: &DatabaseConfig) -> Result<DatabasePool> {
|
||||||
|
let pool = MySqlPoolOptions::new()
|
||||||
|
.max_connections(config.max_connections)
|
||||||
|
.connect(&config.url)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
19
crates/api/src/infrastructure/guacamole_client.rs
Normal file
19
crates/api/src/infrastructure/guacamole_client.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::config::GuacamoleConfig;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GuacamoleClient {
|
||||||
|
config: GuacamoleConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuacamoleClient {
|
||||||
|
pub fn new(config: GuacamoleConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn health_check(&self) -> Result<()> {
|
||||||
|
tracing::debug!(endpoint = %self.config.api_endpoint, "Guacamole client health check placeholder");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
6
crates/api/src/infrastructure/mod.rs
Normal file
6
crates/api/src/infrastructure/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
pub mod database;
|
||||||
|
pub mod guacamole_client;
|
||||||
|
pub mod pf_sense_client;
|
||||||
|
pub mod terraform_executor;
|
||||||
|
|
||||||
|
pub use database::DatabasePool;
|
||||||
19
crates/api/src/infrastructure/pf_sense_client.rs
Normal file
19
crates/api/src/infrastructure/pf_sense_client.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::config::PfSenseConfig;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PfSenseClient {
|
||||||
|
config: PfSenseConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PfSenseClient {
|
||||||
|
pub fn new(config: PfSenseConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_mapping(&self) -> Result<()> {
|
||||||
|
tracing::debug!(endpoint = %self.config.api_endpoint, "pfSense client mapping placeholder");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
32
crates/api/src/infrastructure/terraform_executor.rs
Normal file
32
crates/api/src/infrastructure/terraform_executor.rs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct TerraformExecutor;
|
||||||
|
|
||||||
|
impl TerraformExecutor {
|
||||||
|
pub async fn apply(&self, working_dir: &str) -> Result<()> {
|
||||||
|
run_command("apply", working_dir).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn destroy(&self, working_dir: &str) -> Result<()> {
|
||||||
|
run_command("destroy", working_dir).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_command(subcommand: &str, working_dir: &str) -> Result<()> {
|
||||||
|
tracing::debug!(subcommand, working_dir, "Terraform command placeholder");
|
||||||
|
|
||||||
|
let status = Command::new("terraform")
|
||||||
|
.arg(subcommand)
|
||||||
|
.arg("-auto-approve")
|
||||||
|
.current_dir(working_dir)
|
||||||
|
.status()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Terraform command `{}` failed", subcommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
11
crates/api/src/main.rs
Normal file
11
crates/api/src/main.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mod bootstrap;
|
||||||
|
pub mod config;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
pub mod routes;
|
||||||
|
pub mod services;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
bootstrap::run().await
|
||||||
|
}
|
||||||
118
crates/api/src/routes/deployments.rs
Normal file
118
crates/api/src/routes/deployments.rs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{config::AppConfig, services::tfvars::TfvarsGenerator};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub config: AppConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(config: AppConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DeploymentRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub user_count: u32,
|
||||||
|
// VM characteristics
|
||||||
|
pub vm_template: String,
|
||||||
|
pub cpu: u32,
|
||||||
|
pub ram_mb: u32,
|
||||||
|
pub disk_gb: u32,
|
||||||
|
// Optional starting host offset within 192.168.143.0/24
|
||||||
|
pub ip_start_host: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DeploymentResponse {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub tfvars_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_deployment(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<DeploymentRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if let Err(msg) = validate_request(&payload) {
|
||||||
|
return (StatusCode::BAD_REQUEST, Json(ErrorResponse { error: msg })).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let tfvars = TfvarsGenerator::generate(&payload);
|
||||||
|
let drop_dir = &state.config.terraform.drop_dir;
|
||||||
|
let filename = format!("{}-{}.tfvars", payload.name.replace(' ', "_"), id);
|
||||||
|
let path = format!("{}/{}", drop_dir, filename);
|
||||||
|
|
||||||
|
if let Err(e) = write_tfvars(&path, &tfvars).await {
|
||||||
|
tracing::error!(error = %e, %path, "Failed to write tfvars file");
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: "Failed to write tfvars file".into(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(DeploymentResponse {
|
||||||
|
id,
|
||||||
|
tfvars_path: path,
|
||||||
|
})
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_tfvars(path: &str, content: &str) -> anyhow::Result<()> {
|
||||||
|
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||||
|
tokio::fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
tokio::fs::write(path, content).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_request(req: &DeploymentRequest) -> Result<(), String> {
|
||||||
|
if req.name.trim().is_empty() {
|
||||||
|
return Err("name must not be empty".into());
|
||||||
|
}
|
||||||
|
if req.user_count == 0 {
|
||||||
|
return Err("user_count must be >= 1".into());
|
||||||
|
}
|
||||||
|
if req.cpu == 0 {
|
||||||
|
return Err("cpu must be >= 1".into());
|
||||||
|
}
|
||||||
|
if req.ram_mb < 256 {
|
||||||
|
return Err("ram_mb must be >= 256".into());
|
||||||
|
}
|
||||||
|
if req.disk_gb < 10 {
|
||||||
|
return Err("disk_gb must be >= 10".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = req.ip_start_host.unwrap_or(50) as u32;
|
||||||
|
if start < 2 || start > 254 {
|
||||||
|
return Err("ip_start_host must be in [2, 254]".into());
|
||||||
|
}
|
||||||
|
let last = start + req.user_count.saturating_sub(1) as u32;
|
||||||
|
if last > 254 {
|
||||||
|
return Err("ip range exceeds 192.168.143.254".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn schema() -> impl IntoResponse {
|
||||||
|
let schema = schemars::schema_for!(DeploymentRequest);
|
||||||
|
Json(schema)
|
||||||
|
}
|
||||||
6
crates/api/src/routes/health.rs
Normal file
6
crates/api/src/routes/health.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use axum::{response::IntoResponse, Json};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub async fn healthz() -> impl IntoResponse {
|
||||||
|
Json(json!({"status": "ok"}))
|
||||||
|
}
|
||||||
22
crates/api/src/routes/mod.rs
Normal file
22
crates/api/src/routes/mod.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
|
||||||
|
pub mod deployments;
|
||||||
|
pub mod health;
|
||||||
|
|
||||||
|
pub async fn build_router(cfg: AppConfig) -> Result<Router> {
|
||||||
|
let app_state = deployments::AppState::new(cfg);
|
||||||
|
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/healthz", get(health::healthz))
|
||||||
|
.route("/deployments", post(deployments::create_deployment))
|
||||||
|
.route("/deployments/schema", get(deployments::schema))
|
||||||
|
.with_state(app_state);
|
||||||
|
|
||||||
|
Ok(router)
|
||||||
|
}
|
||||||
20
crates/api/src/services/credentials.rs
Normal file
20
crates/api/src/services/credentials.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
|
||||||
|
pub struct CredentialGenerator;
|
||||||
|
|
||||||
|
impl CredentialGenerator {
|
||||||
|
pub fn random_password(length: usize) -> Result<String> {
|
||||||
|
if length < 8 {
|
||||||
|
anyhow::bail!("password length too short");
|
||||||
|
}
|
||||||
|
|
||||||
|
let password: String = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(length)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
crates/api/src/services/guacamole.rs
Normal file
19
crates/api/src/services/guacamole.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::config::GuacamoleConfig;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GuacamoleService {
|
||||||
|
config: GuacamoleConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuacamoleService {
|
||||||
|
pub fn new(config: GuacamoleConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_users(&self) -> Result<()> {
|
||||||
|
tracing::debug!(endpoint = %self.config.api_endpoint, "Guacamole user creation placeholder");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
7
crates/api/src/services/mod.rs
Normal file
7
crates/api/src/services/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub mod credentials;
|
||||||
|
pub mod guacamole;
|
||||||
|
pub mod pf_sense;
|
||||||
|
pub mod terraform;
|
||||||
|
pub mod tfvars;
|
||||||
|
|
||||||
|
pub use credentials::CredentialGenerator;
|
||||||
19
crates/api/src/services/pf_sense.rs
Normal file
19
crates/api/src/services/pf_sense.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::config::PfSenseConfig;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PfSenseService {
|
||||||
|
config: PfSenseConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PfSenseService {
|
||||||
|
pub fn new(config: PfSenseConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_static_mapping(&self) -> Result<()> {
|
||||||
|
tracing::debug!(endpoint = %self.config.api_endpoint, "pfSense mapping placeholder");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
16
crates/api/src/services/terraform.rs
Normal file
16
crates/api/src/services/terraform.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct TerraformService;
|
||||||
|
|
||||||
|
impl TerraformService {
|
||||||
|
pub async fn apply(&self) -> Result<()> {
|
||||||
|
tracing::debug!("Terraform apply placeholder");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn destroy(&self) -> Result<()> {
|
||||||
|
tracing::debug!("Terraform destroy placeholder");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
52
crates/api/src/services/tfvars.rs
Normal file
52
crates/api/src/services/tfvars.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use crate::routes::deployments::DeploymentRequest;
|
||||||
|
|
||||||
|
pub struct TfvarsGenerator;
|
||||||
|
|
||||||
|
impl TfvarsGenerator {
|
||||||
|
pub fn generate(req: &DeploymentRequest) -> String {
|
||||||
|
// network is always 192.168.143.0/24 per requirements
|
||||||
|
let base_prefix = "192.168.143";
|
||||||
|
let start = req.ip_start_host.unwrap_or(50); // default to .50
|
||||||
|
|
||||||
|
let ips: Vec<String> = (0..req.user_count)
|
||||||
|
.map(|i| format!("{}.{}", base_prefix, start as u32 + i as u32))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
push_kv(&mut buf, "deployment_name", &req.name);
|
||||||
|
push_kv_num(&mut buf, "instance_count", req.user_count);
|
||||||
|
push_kv(&mut buf, "vm_template", &req.vm_template);
|
||||||
|
push_kv_num(&mut buf, "cpu", req.cpu);
|
||||||
|
push_kv_num(&mut buf, "ram_mb", req.ram_mb);
|
||||||
|
push_kv_num(&mut buf, "disk_gb", req.disk_gb);
|
||||||
|
push_kv(&mut buf, "network_cidr", "192.168.143.0/24");
|
||||||
|
push_list(&mut buf, "ips", &ips);
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_kv(buf: &mut String, key: &str, val: &str) {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
let _ = writeln!(buf, "{} = \"{}\"", key, escape(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_kv_num<T: std::fmt::Display>(buf: &mut String, key: &str, val: T) {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
let _ = writeln!(buf, "{} = {}", key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_list(buf: &mut String, key: &str, values: &[String]) {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
let inner = values
|
||||||
|
.iter()
|
||||||
|
.map(|v| format!("\"{}\"", escape(v)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let _ = writeln!(buf, "{} = [{}]", key, inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape(s: &str) -> String {
|
||||||
|
s.replace('"', "\\\"")
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user