initial commit
This commit is contained in:
parent
4cc1853061
commit
a1f1376aaa
31
.gitignore
vendored
31
.gitignore
vendored
@ -1,22 +1,17 @@
|
||||
# ---> Rust
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
# Rust 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
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
# Env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# 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
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
# Terraform drop/output directories
|
||||
terraform_drop/
|
||||
|
||||
# OS/editor junk
|
||||
.DS_Store
|
||||
*.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