server_forge/
deployment.rs

1//! # Deployment Module
2//!
3//! This module provides functionality for deploying various applications and services
4//! on a Linux server. It supports deployment of web servers (Nginx, Apache), databases
5//! (MySQL, PostgreSQL), programming languages and runtimes (PHP, Node.js, Python),
6//! and configures them according to best practices.
7//!
8//! The module is designed to work across different Linux distributions by leveraging
9//! the appropriate package manager for each system.
10
11use crate::config::Config;
12use crate::distro::{get_package_manager, PackageManager};
13use crate::rollback::RollbackManager;
14use crate::utils::run_command;
15use log::info;
16use std::error::Error;
17
18/// Deploys all applications specified in the configuration.
19///
20/// This function iterates through the list of applications specified in the configuration
21/// and deploys each one. It creates a snapshot before deployment for potential rollback.
22///
23/// # Arguments
24///
25/// * `config` - A reference to the `Config` struct containing deployment information
26/// * `rollback` - A reference to the `RollbackManager` for creating snapshots
27///
28/// # Returns
29///
30/// Returns `Ok(())` if all applications are deployed successfully, or an error if any deployment fails.
31pub fn deploy_applications(
32    config: &Config,
33    rollback: &RollbackManager,
34) -> Result<(), Box<dyn Error>> {
35    info!("Deploying applications...");
36
37    let snapshot = rollback.create_snapshot()?;
38
39    for app in &config.deployed_apps {
40        deploy_app(app, &config.server_role)?;
41    }
42
43    rollback.commit_snapshot(snapshot)?;
44
45    info!("Application deployment completed");
46    Ok(())
47}
48
49/// Deploys a single application based on its type and the server role.
50///
51/// # Arguments
52///
53/// * `app` - A string slice representing the application to deploy
54/// * `server_role` - A string slice representing the role of the server (e.g., "web", "database")
55///
56/// # Returns
57///
58/// Returns `Ok(())` if the application is deployed successfully, or an error if deployment fails.
59pub fn deploy_app(app: &str, server_role: &str) -> Result<(), Box<dyn Error>> {
60    match app {
61        "nginx" => deploy_nginx()?,
62        "apache" => deploy_apache()?,
63        "mysql" => deploy_mysql()?,
64        "postgresql" => deploy_postgresql()?,
65        "php" => deploy_php(server_role)?,
66        "nodejs" => deploy_nodejs()?,
67        "python" => deploy_python()?,
68        _ => return Err(format!("Unsupported application: {}", app).into()),
69    }
70    Ok(())
71}
72
73/// Deploys and configures the Nginx web server.
74///
75/// This function installs Nginx using the appropriate package manager,
76/// starts the Nginx service, and enables it to start on boot.
77///
78/// # Returns
79///
80/// Returns `Ok(())` if Nginx is deployed successfully, or an error if deployment fails.
81pub fn deploy_nginx() -> Result<(), Box<dyn Error>> {
82    let package_manager = get_package_manager()?;
83
84    match package_manager {
85        PackageManager::Apt => run_command("apt", &["install", "-y", "nginx"])?,
86        PackageManager::Yum => run_command("yum", &["install", "-y", "nginx"])?,
87        PackageManager::Dnf => run_command("dnf", &["install", "-y", "nginx"])?,
88    }
89
90    run_command("systemctl", &["start", "nginx"])?;
91    run_command("systemctl", &["enable", "nginx"])?;
92
93    Ok(())
94}
95
96/// Deploys and configures the Apache web server.
97///
98/// This function installs Apache (httpd) using the appropriate package manager,
99/// starts the Apache service, and enables it to start on boot.
100///
101/// # Returns
102///
103/// Returns `Ok(())` if Apache is deployed successfully, or an error if deployment fails.
104pub fn deploy_apache() -> Result<(), Box<dyn Error>> {
105    let package_manager = get_package_manager()?;
106
107    match package_manager {
108        PackageManager::Apt => run_command("apt", &["install", "-y", "apache2"])?,
109        PackageManager::Yum => run_command("yum", &["install", "-y", "httpd"])?,
110        PackageManager::Dnf => run_command("dnf", &["install", "-y", "httpd"])?,
111    }
112
113    if let Err(_) = run_command("systemctl", &["start", "apache2"]) {
114        run_command("systemctl", &["start", "httpd"])?;
115    }
116    if let Err(_) = run_command("systemctl", &["enable", "apache2"]) {
117        run_command("systemctl", &["enable", "httpd"])?;
118    }
119
120    Ok(())
121}
122
123/// Deploys and configures the MySQL database server.
124///
125/// This function installs MySQL using the appropriate package manager,
126/// starts the MySQL service, enables it to start on boot, and runs the
127/// mysql_secure_installation script to set up basic security measures.
128///
129/// # Returns
130///
131/// Returns `Ok(())` if MySQL is deployed successfully, or an error if deployment fails.
132pub fn deploy_mysql() -> Result<(), Box<dyn Error>> {
133    let package_manager = get_package_manager()?;
134
135    match package_manager {
136        PackageManager::Apt => run_command("apt", &["install", "-y", "mysql-server"])?,
137        PackageManager::Yum => run_command("yum", &["install", "-y", "mysql-server"])?,
138        PackageManager::Dnf => run_command("dnf", &["install", "-y", "mysql-server"])?,
139    }
140
141    run_command("systemctl", &["start", "mysql"])?;
142    run_command("systemctl", &["enable", "mysql"])?;
143
144    // Secure MySQL installation
145    run_command("mysql_secure_installation", &[])?;
146
147    Ok(())
148}
149
150/// Deploys and configures the PostgreSQL database server.
151///
152/// This function installs PostgreSQL using the appropriate package manager,
153/// initializes the database if necessary (for CentOS/Fedora), starts the
154/// PostgreSQL service, and enables it to start on boot.
155///
156/// # Returns
157///
158/// Returns `Ok(())` if PostgreSQL is deployed successfully, or an error if deployment fails.
159pub fn deploy_postgresql() -> Result<(), Box<dyn Error>> {
160    let package_manager = get_package_manager()?;
161
162    match package_manager {
163        PackageManager::Apt => run_command(
164            "apt",
165            &["install", "-y", "postgresql", "postgresql-contrib"],
166        )?,
167        PackageManager::Yum => run_command(
168            "yum",
169            &["install", "-y", "postgresql-server", "postgresql-contrib"],
170        )?,
171        PackageManager::Dnf => run_command(
172            "dnf",
173            &["install", "-y", "postgresql-server", "postgresql-contrib"],
174        )?,
175    }
176
177    // Initialize the database (for CentOS/Fedora)
178    if package_manager != PackageManager::Apt {
179        run_command("postgresql-setup", &["--initdb"])?;
180    }
181
182    run_command("systemctl", &["start", "postgresql"])?;
183    run_command("systemctl", &["enable", "postgresql"])?;
184
185    Ok(())
186}
187
188/// Deploys and configures PHP.
189///
190/// This function installs PHP and related packages using the appropriate package manager.
191/// It also installs additional modules based on the server role (e.g., libapache2-mod-php for web servers).
192///
193/// # Arguments
194///
195/// * `server_role` - A string slice representing the role of the server (e.g., "web")
196///
197/// # Returns
198///
199/// Returns `Ok(())` if PHP is deployed successfully, or an error if deployment fails.
200pub fn deploy_php(server_role: &str) -> Result<(), Box<dyn Error>> {
201    let package_manager = get_package_manager()?;
202
203    match package_manager {
204        PackageManager::Apt => {
205            run_command("apt", &["install", "-y", "php", "php-fpm", "php-mysql"])?;
206            if server_role == "web" {
207                run_command("apt", &["install", "-y", "libapache2-mod-php"])?;
208            }
209        }
210        PackageManager::Yum | PackageManager::Dnf => {
211            let install_cmd = if package_manager == PackageManager::Yum {
212                "yum"
213            } else {
214                "dnf"
215            };
216            run_command(
217                install_cmd,
218                &["install", "-y", "php", "php-fpm", "php-mysqlnd"],
219            )?;
220            if server_role == "web" {
221                run_command(install_cmd, &["install", "-y", "php-apache"])?;
222            }
223        }
224    }
225
226    run_command("systemctl", &["start", "php-fpm"])?;
227    run_command("systemctl", &["enable", "php-fpm"])?;
228
229    Ok(())
230}
231
232/// Deploys and configures Node.js.
233///
234/// This function installs Node.js using NVM (Node Version Manager), installs the latest LTS version,
235/// and sets it as the default. It also installs the PM2 process manager for running Node.js applications.
236///
237/// # Returns
238///
239/// Returns `Ok(())` if Node.js is deployed successfully, or an error if deployment fails.
240pub fn deploy_nodejs() -> Result<(), Box<dyn Error>> {
241    // Install Node.js using NVM (Node Version Manager)
242    run_command(
243        "curl",
244        &[
245            "-o-",
246            "https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh",
247            "|",
248            "bash",
249        ],
250    )?;
251    run_command("source", &["~/.nvm/nvm.sh"])?;
252    run_command("nvm", &["install", "node"])?;
253    run_command("nvm", &["use", "node"])?;
254
255    // Install PM2 process manager
256    run_command("npm", &["install", "-g", "pm2"])?;
257
258    Ok(())
259}
260
261/// Deploys and configures Python.
262///
263/// This function installs Python 3 and pip using the appropriate package manager.
264/// It also installs virtualenv for creating isolated Python environments.
265///
266/// # Returns
267///
268/// Returns `Ok(())` if Python is deployed successfully, or an error if deployment fails.
269pub fn deploy_python() -> Result<(), Box<dyn Error>> {
270    let package_manager = get_package_manager()?;
271
272    match package_manager {
273        PackageManager::Apt => run_command(
274            "apt",
275            &["install", "-y", "python3", "python3-pip", "python3-venv"],
276        )?,
277        PackageManager::Yum => run_command("yum", &["install", "-y", "python3", "python3-pip"])?,
278        PackageManager::Dnf => run_command("dnf", &["install", "-y", "python3", "python3-pip"])?,
279    }
280
281    // Install virtualenv
282    run_command("pip3", &["install", "virtualenv"])?;
283
284    Ok(())
285}
286
287pub fn setup_web_server_config(app: &str) -> Result<(), Box<dyn Error>> {
288    match app {
289        "nginx" => setup_nginx_config()?,
290        "apache" => setup_apache_config()?,
291        _ => return Err(format!("Unsupported web server: {}", app).into()),
292    }
293    Ok(())
294}
295
296fn setup_nginx_config() -> Result<(), Box<dyn Error>> {
297    let nginx_config = r#"
298server {
299    listen 80 default_server;
300    listen [::]:80 default_server;
301    root /var/www/html;
302    index index.html index.htm index.nginx-debian.html;
303    server_name _;
304    location / {
305        try_files $uri $uri/ =404;
306    }
307}
308"#;
309    std::fs::write("/etc/nginx/sites-available/default", nginx_config)?;
310    run_command("systemctl", &["reload", "nginx"])?;
311    Ok(())
312}
313
314fn setup_apache_config() -> Result<(), Box<dyn Error>> {
315    let apache_config = r#"
316<VirtualHost *:80>
317    ServerAdmin webmaster@localhost
318    DocumentRoot /var/www/html
319    ErrorLog ${APACHE_LOG_DIR}/error.log
320    CustomLog ${APACHE_LOG_DIR}/access.log combined
321</VirtualHost>
322"#;
323    std::fs::write(
324        "/etc/apache2/sites-available/000-default.conf",
325        apache_config,
326    )?;
327    if let Err(_) = run_command("systemctl", &["reload", "apache2"]) {
328        run_command("systemctl", &["reload", "httpd"])?;
329    }
330    Ok(())
331}
332
333pub fn setup_database(db: &str) -> Result<(), Box<dyn Error>> {
334    match db {
335        "mysql" => setup_mysql()?,
336        "postgresql" => setup_postgresql()?,
337        _ => return Err(format!("Unsupported database: {}", db).into()),
338    }
339    Ok(())
340}
341
342fn setup_mysql() -> Result<(), Box<dyn Error>> {
343    // Generate a secure random password
344    let password = generate_secure_password();
345
346    // Set root password and remove anonymous users
347    run_command(
348        "mysql",
349        &[
350            "-e",
351            &format!(
352                "ALTER USER 'root'@'localhost' IDENTIFIED BY '{}';",
353                password
354            ),
355        ],
356    )?;
357    run_command("mysql", &["-e", "DELETE FROM mysql.user WHERE User='';"])?;
358    run_command("mysql", &["-e", "FLUSH PRIVILEGES;"])?;
359
360    // Save the password securely (this is a placeholder - in a real-world scenario,
361    // you'd want to use a more secure method to store this password)
362    std::fs::write("/root/.mysql_root_password", &password)?;
363
364    Ok(())
365}
366
367fn setup_postgresql() -> Result<(), Box<dyn Error>> {
368    // Generate a secure random password
369    let password = generate_secure_password();
370
371    // Set postgres user password
372    run_command(
373        "sudo",
374        &[
375            "-u",
376            "postgres",
377            "psql",
378            "-c",
379            &format!("ALTER USER postgres PASSWORD '{}';", password),
380        ],
381    )?;
382
383    // Save the password securely
384    // you'd want to use a more secure method to store this password)
385    std::fs::write("/root/.postgres_password", &password)?;
386
387    Ok(())
388}
389
390/// Generates a secure random password.
391///
392/// This function creates a random password of 20 characters, including uppercase and lowercase
393/// letters, numbers, and special characters.
394///
395/// # Returns
396///
397/// Returns a `String` containing the generated password.
398fn generate_secure_password() -> String {
399    use rand::Rng;
400    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
401                            abcdefghijklmnopqrstuvwxyz\
402                            0123456789)(*&^%$#@!~";
403    const PASSWORD_LEN: usize = 20;
404    let mut rng = rand::thread_rng();
405
406    let password: String = (0..PASSWORD_LEN)
407        .map(|_| {
408            let idx = rng.gen_range(0..CHARSET.len());
409            CHARSET[idx] as char
410        })
411        .collect();
412
413    password
414}
415
416/// Creates a sample web application based on the specified application type.
417///
418/// This function creates a basic "Hello, World!" application for PHP, Node.js, or Python,
419/// demonstrating how to set up a simple web server for each technology.
420///
421/// # Arguments
422///
423/// * `app_type` - A string slice representing the type of application to create ("php", "nodejs", or "python")
424///
425/// # Returns
426///
427/// Returns `Ok(())` if the sample application is created successfully, or an error if creation fails.
428// Helper function to create a simple web application
429fn create_sample_web_app(app_type: &str) -> Result<(), Box<dyn Error>> {
430    match app_type {
431        "php" => {
432            let php_content = r#"
433<?php
434echo "Hello, World! This is a sample PHP application.";
435?>
436"#;
437            std::fs::write("/var/www/html/index.php", php_content)?;
438        }
439        "nodejs" => {
440            let node_content = r#"
441const http = require('http');
442const server = http.createServer((req, res) => {
443  res.statusCode = 200;
444  res.setHeader('Content-Type', 'text/plain');
445  res.end('Hello, World! This is a sample Node.js application.');
446});
447server.listen(3000, '127.0.0.1', () => {
448  console.log('Server running on http://127.0.0.1:3000/');
449});
450"#;
451            std::fs::write("/root/app.js", node_content)?;
452            run_command("pm2", &["start", "/root/app.js"])?;
453        }
454        "python" => {
455            let python_content = r#"
456from flask import Flask
457app = Flask(__name__)
458
459@app.route('/')
460def hello_world():
461    return 'Hello, World! This is a sample Python Flask application.'
462
463if __name__ == '__main__':
464    app.run(host='0.0.0.0', port=5000)
465"#;
466            std::fs::write("/root/app.py", python_content)?;
467            run_command("pip3", &["install", "flask"])?;
468            run_command("python3", &["/root/app.py", "&"])?;
469        }
470        _ => return Err(format!("Unsupported application type: {}", app_type).into()),
471    }
472    Ok(())
473}
474
475/// Sets up firewall rules based on the configuration.
476///
477/// This function configures the firewall (ufw for Ubuntu, firewalld for CentOS/Fedora)
478/// with basic rules for SSH, HTTP, and HTTPS, as well as any custom rules specified in the configuration.
479///
480/// # Arguments
481///
482/// * `config` - A reference to the `Config` struct containing firewall configuration
483///
484/// # Returns
485///
486/// Returns `Ok(())` if firewall rules are set up successfully, or an error if setup fails.
487fn setup_firewall_rules(config: &Config) -> Result<(), Box<dyn Error>> {
488    let package_manager = get_package_manager()?;
489
490    match package_manager {
491        PackageManager::Apt => {
492            run_command("ufw", &["allow", "OpenSSH"])?;
493            run_command("ufw", &["allow", "80/tcp"])?;
494            run_command("ufw", &["allow", "443/tcp"])?;
495            for rule in &config.custom_firewall_rules {
496                run_command("ufw", &["allow", rule])?;
497            }
498            run_command("ufw", &["enable"])?;
499        }
500        PackageManager::Yum | PackageManager::Dnf => {
501            run_command("firewall-cmd", &["--permanent", "--add-service=ssh"])?;
502            run_command("firewall-cmd", &["--permanent", "--add-service=http"])?;
503            run_command("firewall-cmd", &["--permanent", "--add-service=https"])?;
504            for rule in &config.custom_firewall_rules {
505                run_command("firewall-cmd", &["--permanent", "--add-port=", rule])?;
506            }
507            run_command("firewall-cmd", &["--reload"])?;
508        }
509    }
510    Ok(())
511}