├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── api.py ├── config.yaml ├── lws.py ├── requirements.txt └── ui.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [fabriziosalmi] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.yaml 2 | *.log 3 | *.tar.gz 4 | wget-log* 5 | flask 6 | flask-cors -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 fab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LWS - Linux Web Services CLI 2 | 3 | **Version:** 1.1.0 4 | 5 | LWS is a command-line interface (CLI) tool designed to simplify the management of LXC containers on Proxmox VE hosts. It provides a convenient way to perform common tasks such as creating, starting, stopping, terminating, and managing LXC instances, as well as interacting with Proxmox hosts themselves. 6 | 7 | Recently, LWS has been expanded to include: 8 | 9 | * **A RESTful API:** Provides programmatic access to LWS functionality over HTTP. 10 | * **A Simple Web UI:** Offers a basic graphical interface to interact with the API. 11 | * **Swagger Documentation:** Interactive API documentation generated via Swagger UI. 12 | 13 | [![asciicast](https://asciinema.org/a/8rE7H67VjQ15HQ9KtsJVMRR4O.svg)](https://asciinema.org/a/8rE7H67VjQ15HQ9KtsJVMRR4O) 14 | 15 | ## Table of Contents 16 | - [Introduction](#introduction) 17 | - [Features](#features) 18 | - [Getting Started](#getting-started) 19 | - [Usage](#usage) 20 | - [Proxmox Host Management](#proxmox-host-management) 21 | - [LXC Container Management](#lxc-container-management) 22 | - [Docker Management](#docker-management) 23 | - [Container Backups & Restores](#container-backups--restores) 24 | - [Monitoring & Reporting](#monitoring--reporting) 25 | - [Security Tools](#security-tools) 26 | - [Managing Scaling Thresholds and Triggers](#managing-scaling-thresholds-and-triggers) 27 | - [API Server](#api-server) 28 | - [Web UI](#web-ui) 29 | - [Swagger Documentation](#swagger-documentation) 30 | - [Security Considerations](#security-considerations) 31 | - [Best Practices](#best-practices) 32 | - [Contributing](#contributing) 33 | - [Roadmap](#roadmap) 34 | - [License](#license) 35 | - [Acknowledgements](#acknowledgements) 36 | 37 | ## Introduction 38 | 39 | **lws** (Linux Web Services) is an open-source CLI tool designed to help developers and system administrators manage Proxmox environments, LXC containers, and Docker services with a unified, AWS-like interface. It simplifies complex operations, reducing them to single commands that can be executed locally or remotely. 40 | 41 | ## Features 42 | 43 | ### General 44 | 45 | - **Unified Interface**: Manage Proxmox hosts, LXC containers, and Docker services with a single tool 46 | - **Remote Operations**: Execute commands locally or remotely via SSH 47 | - **Error Handling**: Robust error detection and reporting 48 | - **Logging**: Comprehensive logging with both text and JSON formats 49 | - **Command Alias Support**: Use short aliases for common commands 50 | 51 | ### Proxmox Management 52 | 53 | - **Host Monitoring**: Monitor CPU, memory, and disk usage of Proxmox hosts 54 | - **Cluster Operations**: Manage Proxmox clusters (start, stop, restart) 55 | - **Template Management**: Upload, create, and delete LXC templates 56 | - **Firewall Rules**: Define and manage security groups and firewall rules 57 | - **Host Backups**: Create and manage backups of Proxmox configurations 58 | 59 | ### LXC Container Management 60 | 61 | - **Container Operations**: Create, start, stop, reboot, and destroy containers 62 | - **Resource Scaling**: Dynamically adjust CPU, memory, and storage resources 63 | - **Snapshot Management**: Create, list, and restore container snapshots 64 | - **Network Configuration**: Configure network settings for containers 65 | - **Volume Management**: Attach and detach storage volumes to containers 66 | - **Container Migration**: Migrate containers between Proxmox hosts 67 | - **Clone Containers**: Create identical copies of existing containers 68 | - **Command Execution**: Run arbitrary commands within containers 69 | - **Network Testing**: Test network connectivity from containers 70 | - **Backup & Restore**: Create and restore container backups 71 | 72 | ### Docker Management 73 | 74 | - **Installation**: Install Docker and Docker Compose within LXC containers 75 | - **Container Operations**: Run, stop, and manage Docker containers 76 | - **Application Deployment**: Deploy applications using Docker Compose 77 | - **Log Access**: View logs from Docker containers 78 | - **Container Listing**: List running Docker containers 79 | - **Application Updates**: Update applications with new images 80 | 81 | ### Security Tools 82 | 83 | - **Security Scanning**: Perform security audits on containers 84 | - **Network Discovery**: Discover reachable hosts in container networks 85 | - **Health Checks**: Perform health checks with automatic issue detection 86 | - **Monitoring**: Monitor real-time resource usage with thresholds 87 | 88 | ### Resource Reporting 89 | 90 | - **Advanced Container Reports**: Generate comprehensive reports on container status and resources 91 | - **Resource Monitoring**: Real-time monitoring of container CPU, memory, and disk usage 92 | - **Scaling Recommendations**: Get intelligent scaling suggestions based on usage patterns 93 | 94 | ## Getting Started 95 | 96 | ### Prerequisites 97 | 98 | - Python 3.6 or higher 99 | - Proxmox Virtual Environment 6.x or higher 100 | - SSH access to Proxmox hosts 101 | - The following Python packages: 102 | - click 103 | - pyyaml 104 | - requests 105 | - tqdm 106 | 107 | ### Installation 108 | 109 | 1. Clone the repository: 110 | ```bash 111 | git clone https://github.com/fabriziosalmi/lws.git 112 | cd lws 113 | ``` 114 | 115 | 2. Install required Python packages: 116 | ```bash 117 | pip install -r requirements.txt 118 | ``` 119 | 120 | 3. Create a configuration file `config.yaml` with your Proxmox server details: 121 | ```yaml 122 | regions: 123 | eu-south-1: 124 | availability_zones: 125 | az1: 126 | host: proxmox1.example.com 127 | user: root 128 | ssh_password: your_password 129 | az2: 130 | host: proxmox2.example.com 131 | user: root 132 | ssh_password: your_password 133 | 134 | instance_sizes: 135 | small: 136 | memory: 512 137 | cpulimit: 1 138 | storage: local:8 139 | medium: 140 | memory: 1024 141 | cpulimit: 2 142 | storage: local:16 143 | large: 144 | memory: 2048 145 | cpulimit: 4 146 | storage: local:32 147 | 148 | default_storage: local 149 | default_network: vmbr0 150 | use_local_only: false 151 | ``` 152 | 153 | 4. Make the script executable and create an alias: 154 | ```bash 155 | chmod +x lws.py && alias lws='python3 /path/to/lws.py' 156 | ``` 157 | 158 | 5. Verify the installation: 159 | ```bash 160 | lws --version 161 | ``` 162 | 163 | ## Usage 164 | 165 | ### Proxmox Host Management 166 | 167 | #### List Proxmox Hosts 168 | ```bash 169 | lws px list 170 | ``` 171 | 172 | #### Check Host Status 173 | ```bash 174 | lws px status --region eu-south-1 --az az1 175 | ``` 176 | 177 | #### List Clusters 178 | ```bash 179 | lws px clusters --region eu-south-1 --az az1 180 | ``` 181 | 182 | #### Backup Proxmox Configuration 183 | ```bash 184 | lws px backup /path/to/backup/directory --region eu-south-1 --az az1 185 | ``` 186 | 187 | #### Start/Stop/Restart Cluster Services 188 | ```bash 189 | lws px cluster-start --region eu-south-1 --az az1 190 | lws px cluster-stop --region eu-south-1 --az az1 191 | lws px cluster-restart --region eu-south-1 --az az1 192 | ``` 193 | 194 | #### Execute Command on Proxmox Host 195 | ```bash 196 | lws px exec "free -m" --region eu-south-1 --az az1 197 | ``` 198 | 199 | #### Upload Template to Proxmox 200 | ```bash 201 | lws px upload ./ubuntu-20.04-template.tar.gz ubuntu-20.04 --region eu-south-1 --az az1 202 | ``` 203 | 204 | ### LXC Container Management 205 | 206 | #### Create and Start LXC Container 207 | ```bash 208 | lws lxc run --image-id local:vztmpl/ubuntu-20.04-standard_20.04-1_amd64.tar.gz --size medium --count 3 --hostname web-server 209 | ``` 210 | 211 | #### Start/Stop/Reboot Container 212 | ```bash 213 | lws lxc start 100 101 102 214 | lws lxc stop 100 101 102 215 | lws lxc reboot 100 216 | ``` 217 | 218 | #### Show Container Details 219 | ```bash 220 | lws lxc show 100 221 | ``` 222 | 223 | #### Show All Containers 224 | ```bash 225 | lws lxc show 226 | ``` 227 | 228 | #### Scale Container Resources 229 | ```bash 230 | lws lxc scale 100 --memory 2048 --cpulimit 2 --storage-size 32G 231 | ``` 232 | 233 | #### Monitor Container Resources 234 | ```bash 235 | lws lxc status 100 236 | ``` 237 | 238 | #### Execute Command in Container 239 | ```bash 240 | lws lxc exec 100 "apt update && apt upgrade -y" 241 | ``` 242 | 243 | #### Create and Manage Snapshots 244 | ```bash 245 | lws lxc snapshot-add 100 snap1 246 | lws lxc snapshots 100 247 | lws lxc snapshot-rm 100 snap1 248 | ``` 249 | 250 | #### Check Network Connectivity 251 | ```bash 252 | lws lxc net 100 tcp 80 253 | ``` 254 | 255 | #### Show Container Info and IP 256 | ```bash 257 | lws lxc show-info 100 258 | lws lxc show-public-ip 100 259 | ``` 260 | 261 | #### Clone Container 262 | ```bash 263 | lws lxc clone 100 101 --full 264 | ``` 265 | 266 | #### Scale Recommendations 267 | ```bash 268 | lws lxc scale-check 100 269 | ``` 270 | 271 | #### Manage Container Services 272 | ```bash 273 | lws lxc service status nginx 100 274 | lws lxc service restart nginx 100 275 | ``` 276 | 277 | #### Advanced Health Check 278 | ```bash 279 | lws lxc health-check 100 --fix 280 | ``` 281 | 282 | #### Resource Monitoring 283 | ```bash 284 | lws lxc resources 100 --interval 5 --count 10 285 | ``` 286 | 287 | #### Generate Container Report 288 | ```bash 289 | lws lxc report 100 --output json --file container_report.json 290 | ``` 291 | 292 | ### Container Backups & Restores 293 | 294 | #### Create Container Backup 295 | ```bash 296 | lws lxc backup-create 100 --download 297 | ``` 298 | 299 | #### Restore Container from Backup 300 | ```bash 301 | lws lxc backup-restore 100 --backup-file backup-100-20230915-123456.tar.gz 302 | ``` 303 | 304 | ### Docker Management 305 | 306 | #### Install Docker on Container 307 | ```bash 308 | lws app setup 100 309 | ``` 310 | 311 | #### Run Docker Container 312 | ```bash 313 | lws app run 100 -d -p 80:80 nginx 314 | ``` 315 | 316 | #### Deploy with Docker Compose 317 | ```bash 318 | lws app deploy install 100 --compose_file docker-compose.yml --auto_start 319 | ``` 320 | 321 | #### Update Docker Compose Application 322 | ```bash 323 | lws app update 100 docker-compose.yml 324 | ``` 325 | 326 | #### View Docker Logs 327 | ```bash 328 | lws app logs 100 nginx --follow 329 | ``` 330 | 331 | #### List Docker Containers 332 | ```bash 333 | lws app list 100 334 | ``` 335 | 336 | #### Remove Docker 337 | ```bash 338 | lws app remove 100 --purge 339 | ``` 340 | 341 | ### Security Tools 342 | 343 | #### Perform Security Scan 344 | ```bash 345 | lws sec scan 100 --scan-type full 346 | ``` 347 | 348 | #### Network Discovery 349 | ```bash 350 | lws sec discovery 100 351 | ``` 352 | 353 | ### Managing Scaling Thresholds and Triggers 354 | 355 | Scaling thresholds and triggers allow **lws** to automatically adjust resources (CPU, memory, storage) for LXC containers based on defined conditions met on both the Proxmox host and the LXC container. This feature ensures optimal performance while preventing resource exhaustion. 356 | 357 | #### Example Scaling Configuration 358 | ```yaml 359 | scaling: 360 | host_cpu: 361 | high_threshold: 0.80 362 | low_threshold: 0.20 363 | check_interval_seconds: 60 364 | 365 | host_memory: 366 | high_threshold: 0.85 367 | low_threshold: 0.30 368 | check_interval_seconds: 60 369 | 370 | lxc_cpu: 371 | min_threshold: 0.30 372 | max_threshold: 0.80 373 | step: 1 374 | scale_up_multiplier: 1.5 375 | scale_down_multiplier: 0.5 376 | 377 | lxc_memory: 378 | min_threshold: 0.40 379 | max_threshold: 0.70 380 | step_mb: 256 381 | scale_up_multiplier: 1.25 382 | scale_down_multiplier: 0.75 383 | 384 | limits: 385 | min_cpu_cores: 1 386 | max_cpu_cores: 4 387 | min_memory_mb: 512 388 | max_memory_mb: 8192 389 | min_storage_gb: 10 390 | max_storage_gb: 500 391 | 392 | notifications: 393 | notify_user: true 394 | dry_run: true 395 | ``` 396 | 397 | > [!TIP] 398 | > Use `notify_user: true` to get immediate feedback on scaling adjustments, which is especially useful in dynamic environments. 399 | 400 | > [!WARNING] 401 | > Be cautious when setting the `dry_run` option to `false`, as real scaling adjustments will be applied. Ensure your thresholds and multipliers are well-tested before applying them in production. 402 | 403 | ### API Server 404 | 405 | The API server provides HTTP access to LWS functions. 406 | 407 | 1. **Ensure `config.yaml` is configured**, especially `api_key`, `api.host`, and `api.port`. 408 | 2. **Run the API server:** 409 | ```bash 410 | python3 api.py 411 | ``` 412 | The server will start, typically listening on `0.0.0.0:8080` (or as configured). 413 | 414 | 3. **Interact with the API:** Use tools like `curl`, Postman, or the provided Web UI. Remember to include the API key in the `X-API-Key` header for protected endpoints. 415 | 416 | ```bash 417 | # Example: Health Check (no API key needed) 418 | curl http://localhost:8080/api/v1/health 419 | 420 | # Example: List LXC Instances (requires API key) 421 | curl -H "X-API-Key: your-secure-api-key" http://localhost:8080/api/v1/lxc/instances 422 | ``` 423 | 424 | ### Web UI 425 | 426 | A simple web interface is provided to interact with the API. 427 | 428 | 1. **Ensure the API server (`api.py`) is running.** 429 | 2. **Access the UI:** 430 | * Navigate your browser to the root URL of the running API server (e.g., `http://localhost:8080/`). The API server serves `ui.html` directly. 431 | * Alternatively, open the `ui.html` file directly in your browser (`file:///.../ui.html`). **Note:** Direct file access might cause CORS issues when making API calls, depending on your browser and the API's `allowed_origins` configuration in `config.yaml`. Serving the UI via the API is recommended. 432 | 3. **Enter your API Key** in the input field and use the buttons to trigger API calls. Responses will be displayed in a formatted view. 433 | 434 | ### Swagger Documentation 435 | 436 | Interactive API documentation is available via Swagger UI. 437 | 438 | 1. **Ensure the API server (`api.py`) is running.** 439 | 2. **Access Swagger UI:** Navigate your browser to `/api/v1/docs` relative to the API server's URL (e.g., `http://localhost:8080/api/v1/docs`). 440 | 3. You can explore the available endpoints, view parameters, and even try API calls directly from the Swagger interface (remember to authorize using your API key via the "Authorize" button). 441 | 442 | ## Security Considerations 443 | 444 | - **Secure Storage of Credentials**: Consider using environment variables or a secure key store instead of plaintext passwords in configuration files. 445 | - **Restricted Access**: Limit access to the configuration file containing sensitive credentials. 446 | - **Regular Security Scans**: Run `lws sec scan` regularly on your containers to detect security issues. 447 | - **Firewall Rules**: Use the security group functionality to restrict network access to containers. 448 | - **Update Regularly**: Keep your container images and software up to date. 449 | 450 | ## Best Practices 451 | 452 | - **Resource Planning**: Use `lws lxc scale-check` to get recommendations on optimal resource allocation. 453 | - **Regular Backups**: Create regular backups with `lws lxc backup-create` to prevent data loss. 454 | - **Monitoring**: Use `lws lxc resources` to monitor resource usage patterns. 455 | - **Health Checks**: Run `lws lxc health-check` periodically to detect and fix issues. 456 | - **Documentation**: Generate reports with `lws lxc report` for documentation and auditing purposes. 457 | 458 | ## Contributing 459 | 460 | **lws** is an open-source project developed for fun and learning. Contributions are welcome! Feel free to submit issues, feature requests, or pull requests. 461 | 462 | ### How to Contribute 463 | 464 | 1. **Fork the Repository** 465 | 2. **Create a Branch** 466 | ```bash 467 | git checkout -b feature-branch 468 | ``` 469 | 3. **Make Changes** 470 | 4. **Submit a Pull Request** 471 | 472 | > [!TIP] 473 | > Include clear commit messages and documentation with your pull requests to make the review process smoother. 474 | 475 | ## Roadmap 476 | 477 | **lws** continues to evolve. Planned features and improvements include: 478 | 479 | - **Multi-Factor Authentication**: Support for MFA in SSH connections. 480 | - **Web Interface**: A simple web dashboard for visual management. 481 | - **Configuration Versioning**: Track changes to container configurations. 482 | - **Integration with CI/CD Pipelines**: Make lws part of your deployment workflows. 483 | - **Kubernetes Support**: Expand management capabilities to Kubernetes clusters. 484 | - **More Security Tools**: Additional security scanning and threat detection tools. 485 | 486 | ## License 487 | 488 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. 489 | 490 | ## Acknowledgements 491 | 492 | - The Proxmox team for their excellent virtualization platform 493 | - The Click developers for the wonderful CLI framework 494 | - All contributors who have helped improve this tool 495 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | API wrapper for LWS (Linux Web Services) 5 | This module provides a REST API interface to the functionality available in lws.py 6 | """ 7 | 8 | import os 9 | import time 10 | import json 11 | import subprocess 12 | import yaml 13 | import logging 14 | import sys 15 | from functools import wraps 16 | import shlex # Import shlex for safe command splitting 17 | 18 | # Import Flask and related extensions 19 | from flask import Flask, request, jsonify, Response, abort, send_from_directory, url_for # Added url_for 20 | from flask_cors import CORS 21 | from werkzeug.exceptions import HTTPException 22 | # Import Swagger UI 23 | from flask_swagger_ui import get_swaggerui_blueprint 24 | 25 | # Create Flask application 26 | app = Flask(__name__) 27 | # --- Configuration Loading --- 28 | def load_api_config(): 29 | """Loads configuration from config.yaml.""" 30 | try: 31 | config_path = os.path.join(os.path.dirname(__file__), 'config.yaml') 32 | if not os.path.exists(config_path): 33 | logging.error(f"Configuration file not found at {config_path}") 34 | return None 35 | with open(config_path, 'r') as file: 36 | config = yaml.safe_load(file) 37 | return config 38 | except Exception as e: 39 | logging.error(f"Error loading configuration: {e}") 40 | return None 41 | 42 | config = load_api_config() 43 | if not config: 44 | logging.critical("Failed to load configuration. Exiting.") 45 | sys.exit(1) 46 | 47 | API_KEY = config.get('api_key', None) 48 | API_CONFIG = config.get('api', {}) 49 | LWS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), 'lws.py') # Path to lws.py 50 | 51 | # --- CORS Configuration --- 52 | allowed_origins = API_CONFIG.get('allowed_origins', '*') # Default to allow all if not specified 53 | # If allowing specific origins, consider adding 'null' for file:// access during development 54 | # Example: allowed_origins = ["http://localhost:8000", "null"] 55 | CORS(app, origins=allowed_origins) # Apply CORS settings 56 | 57 | if not API_KEY: 58 | logging.warning("API key is not set in config.yaml. API will be insecure.") 59 | 60 | # Configure logging 61 | log_level_str = API_CONFIG.get('log_level', 'INFO').upper() 62 | log_level = getattr(logging, log_level_str, logging.INFO) 63 | 64 | logging.basicConfig( 65 | level=log_level, 66 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 67 | handlers=[ 68 | logging.FileHandler("api.log"), 69 | logging.StreamHandler(sys.stdout) 70 | ] 71 | ) 72 | logging.info("API starting up...") 73 | logging.info(f"Log level set to {log_level_str}") 74 | 75 | # --- Authentication --- 76 | def require_api_key(f): 77 | @wraps(f) 78 | def decorated_function(*args, **kwargs): 79 | if API_KEY: # Only enforce if API_KEY is set 80 | provided_key = request.headers.get('X-API-Key') 81 | if not provided_key or provided_key != API_KEY: 82 | logging.warning(f"Unauthorized access attempt from {request.remote_addr}") 83 | abort(401, description="Unauthorized: Invalid or missing API key.") 84 | return f(*args, **kwargs) 85 | return decorated_function 86 | 87 | # --- Error Handling --- 88 | @app.errorhandler(HTTPException) 89 | def handle_exception(e): 90 | """Return JSON instead of HTML for HTTP errors.""" 91 | response = e.get_response() 92 | response.data = json.dumps({ 93 | "code": e.code, 94 | "name": e.name, 95 | "description": e.description, 96 | }) 97 | response.content_type = "application/json" 98 | logging.error(f"HTTP Error {e.code} {e.name}: {e.description} for {request.url}") 99 | return response 100 | 101 | @app.errorhandler(404) 102 | def not_found(error): 103 | return jsonify({"error": "Not Found", "message": "The requested URL was not found on the server."}), 404 104 | 105 | @app.errorhandler(Exception) 106 | def handle_generic_exception(e): 107 | """Handle unexpected errors.""" 108 | logging.exception(f"An unexpected error occurred: {e}") 109 | return jsonify({"error": "Internal Server Error", "message": str(e)}), 500 110 | 111 | 112 | # --- Helper Function to Run lws.py Commands --- 113 | def run_lws_command(command_parts, data=None): 114 | """ 115 | Executes an lws.py command using subprocess. 116 | 117 | Args: 118 | command_parts (list): A list containing the command and its subcommands/arguments 119 | (e.g., ['lxc', 'run', '--image-id', 'ubuntu-22.04']). 120 | data (dict, optional): Data from the request body (for POST/PUT). 121 | 122 | Returns: 123 | tuple: (output, error, return_code) 124 | """ 125 | base_cmd = [sys.executable, LWS_SCRIPT_PATH] # Use sys.executable to ensure correct python interpreter 126 | full_cmd = base_cmd + command_parts 127 | 128 | # Add options from query parameters and JSON body 129 | options = {} 130 | if request.args: 131 | options.update(request.args.to_dict()) 132 | if data: 133 | options.update(data) 134 | 135 | # Append options as command line arguments 136 | for key, value in options.items(): 137 | # Handle boolean flags (like --confirm, --purge, --fix) 138 | if isinstance(value, bool): 139 | if value: 140 | full_cmd.append(f"--{key.replace('_', '-')}") 141 | elif value is not None: # Append only if value is not None 142 | full_cmd.append(f"--{key.replace('_', '-')}") 143 | full_cmd.append(str(value)) # Ensure value is string 144 | 145 | # Handle positional arguments if needed (e.g., instance_ids, command for exec) 146 | # This part needs careful mapping based on specific commands. 147 | # For simplicity, many commands take IDs/names in the path or specific options. 148 | # Commands like 'exec' or 'run_docker' might need special handling for their command arguments. 149 | 150 | logging.info(f"Executing command: {' '.join(shlex.quote(str(c)) for c in full_cmd)}") # Log the command safely 151 | 152 | try: 153 | # Use Popen for potentially long-running commands or streaming output if needed later 154 | process = subprocess.Popen(full_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 155 | stdout, stderr = process.communicate(timeout=300) # 5 minute timeout 156 | return_code = process.returncode 157 | 158 | logging.debug(f"Command stdout: {stdout.strip()}") 159 | if return_code != 0: 160 | logging.error(f"Command stderr: {stderr.strip()}") 161 | logging.error(f"Command return code: {return_code}") 162 | 163 | return stdout, stderr, return_code 164 | 165 | except subprocess.TimeoutExpired: 166 | logging.error(f"Command timed out: {' '.join(shlex.quote(str(c)) for c in full_cmd)}") 167 | return None, "Command execution timed out after 300 seconds.", 124 # Timeout return code 168 | except Exception as e: 169 | logging.exception(f"Error executing command: {e}") 170 | return None, f"Internal error executing command: {str(e)}", 1 171 | 172 | def format_response(stdout, stderr, return_code): 173 | """Formats the command output into a JSON response.""" 174 | if return_code == 0: 175 | try: 176 | # Try to parse stdout as JSON if it looks like it 177 | if stdout and stdout.strip().startswith(("{", "[")): 178 | return jsonify(json.loads(stdout)), 200 179 | else: 180 | # Otherwise return plain text output 181 | return jsonify({"output": stdout.strip()}), 200 182 | except json.JSONDecodeError: 183 | # If JSON parsing fails, return as plain text 184 | return jsonify({"output": stdout.strip()}), 200 185 | else: 186 | # Return error details 187 | return jsonify({ 188 | "error": "Command execution failed", 189 | "details": stderr.strip() if stderr else "No error details provided.", 190 | "output": stdout.strip() if stdout else "", 191 | "return_code": return_code 192 | }), 500 # Use 500 for internal/execution errors 193 | 194 | # --- Swagger UI Configuration --- 195 | SWAGGER_URL = '/api/v1/docs' # URL for exposing Swagger UI (without trailing '/') 196 | API_URL = '/api/v1/swagger.json' # URL for your OpenAPI spec 197 | 198 | # Call factory function to create our blueprint 199 | swaggerui_blueprint = get_swaggerui_blueprint( 200 | SWAGGER_URL, 201 | API_URL, 202 | config={ # Swagger UI config overrides 203 | 'app_name': "LWS API" 204 | } 205 | ) 206 | 207 | # Register blueprint at URL 208 | app.register_blueprint(swaggerui_blueprint) 209 | 210 | # --- Minimal OpenAPI Spec --- 211 | @app.route('/api/v1/swagger.json') 212 | def swagger_spec(): 213 | """Serves the OpenAPI specification (dynamically generated skeleton).""" 214 | # NOTE: This dynamically generates path skeletons but lacks details. 215 | # Manual completion or using libraries like apispec/flasgger is recommended for full documentation. 216 | spec = { 217 | "openapi": "3.0.0", 218 | "info": { 219 | "title": "LWS API", 220 | "version": "1.0.0", 221 | "description": "API for managing Linux Web Services (LXC on Proxmox)" 222 | }, 223 | "servers": [ 224 | # Determine server URL dynamically if needed, or keep relative 225 | {"url": request.host_url.rstrip('/') + url_for('serve_ui').rstrip('/') + '/api/v1'} 226 | # Or simply use relative: {"url": "/api/v1"} 227 | ], 228 | "paths": {}, # Initialize empty paths 229 | "components": { 230 | "securitySchemes": { 231 | "ApiKeyAuth": { 232 | "type": "apiKey", 233 | "in": "header", 234 | "name": "X-API-Key" 235 | } 236 | } 237 | }, 238 | "security": [ # Default security for most endpoints 239 | {"ApiKeyAuth": []} 240 | ] 241 | } 242 | 243 | # Dynamically populate paths from Flask routes 244 | ignore_methods = {"HEAD", "OPTIONS"} 245 | # Exclude static routes and swagger routes 246 | excluded_endpoints = ['static', 'swagger_ui.show', 'swagger_spec'] 247 | 248 | for rule in app.url_map.iter_rules(): 249 | if rule.endpoint in excluded_endpoints: 250 | continue 251 | 252 | # Convert path parameters from to {name} 253 | path = rule.rule.replace('<', '{').replace('>', '}').replace('string:', '').replace('int:', '') 254 | 255 | # Ensure path starts relative to /api/v1 if needed, or adjust server URL 256 | # For simplicity, assuming paths are correctly prefixed or server URL handles it. 257 | 258 | if path not in spec['paths']: 259 | spec['paths'][path] = {} 260 | 261 | methods = [m for m in rule.methods if m not in ignore_methods] 262 | for method in methods: 263 | # Basic placeholder structure - NEEDS MANUAL COMPLETION 264 | spec['paths'][path][method.lower()] = { 265 | "summary": f"Placeholder for {rule.endpoint}", 266 | "description": f"TODO: Describe the {method} operation for {path}", 267 | "tags": [rule.endpoint.split('.')[0] if '.' in rule.endpoint else 'default'], # Basic tagging 268 | "parameters": [ 269 | # TODO: Add path parameters like {instance_id} here manually or via introspection 270 | # Example: {"name": "instance_id", "in": "path", "required": True, "schema": {"type": "string"}} 271 | ], 272 | # TODO: Add requestBody for POST/PUT manually 273 | # TODO: Add detailed responses manually 274 | "responses": { 275 | "200": {"description": "TODO: Describe success response"}, 276 | "400": {"description": "TODO: Describe bad request response"}, 277 | "401": {"description": "Unauthorized (if require_api_key is used)"}, 278 | "404": {"description": "TODO: Describe not found response"}, 279 | "500": {"description": "Command execution failed or Internal Server Error"} 280 | } 281 | } 282 | # Apply default security if not the health check 283 | if rule.endpoint != 'health_check': 284 | spec['paths'][path][method.lower()]['security'] = [{"ApiKeyAuth": []}] 285 | 286 | 287 | return jsonify(spec) 288 | 289 | 290 | # --- API Endpoints --- 291 | 292 | # --- Serve UI --- 293 | @app.route('/', methods=['GET']) 294 | def serve_ui(): 295 | """Serves the ui.html file.""" 296 | # Assumes ui.html is in the same directory as api.py 297 | return send_from_directory(os.path.dirname(__file__), 'ui.html') 298 | 299 | # --- Health Check --- 300 | @app.route('/api/v1/health', methods=['GET']) 301 | def health_check(): 302 | """Basic health check endpoint.""" 303 | return jsonify({"status": "ok"}), 200 304 | 305 | # --- Configuration Management (`conf`) --- 306 | @app.route('/api/v1/conf', methods=['GET']) 307 | @require_api_key 308 | def conf_show(): 309 | """Show current configuration (masked).""" 310 | stdout, stderr, rc = run_lws_command(['conf', 'show']) 311 | return format_response(stdout, stderr, rc) 312 | 313 | @app.route('/api/v1/conf/validate', methods=['POST']) 314 | @require_api_key 315 | def conf_validate(): 316 | """Validate the current configuration.""" 317 | stdout, stderr, rc = run_lws_command(['conf', 'validate']) 318 | return format_response(stdout, stderr, rc) 319 | 320 | @app.route('/api/v1/conf/backup', methods=['POST']) 321 | @require_api_key 322 | def conf_backup(): 323 | """Backup the current configuration.""" 324 | data = request.get_json() 325 | if not data or 'destination_path' not in data: 326 | return jsonify({"error": "Missing 'destination_path' in request body"}), 400 327 | 328 | cmd_parts = ['conf', 'backup', data['destination_path']] 329 | # Add optional flags from data 330 | if data.get('timestamp'): 331 | cmd_parts.append('--timestamp') 332 | if data.get('compress'): 333 | cmd_parts.append('--compress') 334 | 335 | stdout, stderr, rc = run_lws_command(cmd_parts) 336 | return format_response(stdout, stderr, rc) 337 | 338 | # --- Proxmox Host Management (`px`) --- 339 | @app.route('/api/v1/px/hosts', methods=['GET']) 340 | @require_api_key 341 | def px_list_hosts(): 342 | """List all available Proxmox hosts.""" 343 | cmd_parts = ['px', 'list'] 344 | if 'region' in request.args: 345 | cmd_parts.extend(['--region', request.args['region']]) 346 | stdout, stderr, rc = run_lws_command(cmd_parts) 347 | # Special handling for list output if needed (e.g., parse lines) 348 | return format_response(stdout, stderr, rc) 349 | 350 | @app.route('/api/v1/px/reboot', methods=['POST']) 351 | @require_api_key 352 | def px_reboot(): 353 | """Reboot a Proxmox host.""" 354 | data = request.get_json() 355 | if not data or not data.get('confirm'): 356 | return jsonify({"error": "Confirmation required. Set 'confirm': true in the request body."}), 400 357 | 358 | cmd_parts = ['px', 'reboot', '--confirm'] 359 | if 'region' in data: cmd_parts.extend(['--region', data['region']]) 360 | if 'az' in data: cmd_parts.extend(['--az', data['az']]) 361 | 362 | stdout, stderr, rc = run_lws_command(cmd_parts) 363 | return format_response(stdout, stderr, rc) 364 | 365 | @app.route('/api/v1/px/upload', methods=['POST']) 366 | @require_api_key 367 | def px_upload_template(): 368 | """Upload template to Proxmox host.""" 369 | data = request.get_json() 370 | if not data or 'local_path' not in data: 371 | return jsonify({"error": "Missing 'local_path' in request body"}), 400 372 | 373 | cmd_parts = ['px', 'upload', data['local_path']] 374 | if 'remote_template_name' in data: cmd_parts.append(data['remote_template_name']) 375 | 376 | # Pass other options via run_lws_command's data handling 377 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 378 | return format_response(stdout, stderr, rc) 379 | 380 | @app.route('/api/v1/px/status', methods=['GET']) 381 | @require_api_key 382 | def px_status(): 383 | """Monitor resource usage of a Proxmox host.""" 384 | cmd_parts = ['px', 'status'] 385 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 386 | return format_response(stdout, stderr, rc) 387 | 388 | @app.route('/api/v1/px/clusters', methods=['GET']) 389 | @require_api_key 390 | def px_list_clusters(): 391 | """List all clusters in the Proxmox environment.""" 392 | cmd_parts = ['px', 'clusters'] 393 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 394 | return format_response(stdout, stderr, rc) 395 | 396 | @app.route('/api/v1/px/update', methods=['POST']) 397 | @require_api_key 398 | def px_update_hosts(): 399 | """Update all Proxmox hosts.""" 400 | # Note: lws.py px update doesn't take region/az, it seems to run locally? 401 | # Clarify if this should target specific hosts or run where API runs. 402 | # Assuming it runs where the API runs for now. 403 | stdout, stderr, rc = run_lws_command(['px', 'update']) 404 | return format_response(stdout, stderr, rc) 405 | 406 | @app.route('/api/v1/px/cluster/start', methods=['POST']) 407 | @require_api_key 408 | def px_start_cluster(): 409 | """Start cluster services on a Proxmox host.""" 410 | data = request.get_json() or {} 411 | cmd_parts = ['px', 'cluster-start'] 412 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 413 | return format_response(stdout, stderr, rc) 414 | 415 | @app.route('/api/v1/px/cluster/stop', methods=['POST']) 416 | @require_api_key 417 | def px_stop_cluster(): 418 | """Stop cluster services on a Proxmox host.""" 419 | data = request.get_json() or {} 420 | cmd_parts = ['px', 'cluster-stop'] 421 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 422 | return format_response(stdout, stderr, rc) 423 | 424 | @app.route('/api/v1/px/cluster/restart', methods=['POST']) 425 | @require_api_key 426 | def px_restart_cluster(): 427 | """Restart cluster services on a Proxmox host.""" 428 | data = request.get_json() or {} 429 | cmd_parts = ['px', 'cluster-restart'] 430 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 431 | return format_response(stdout, stderr, rc) 432 | 433 | @app.route('/api/v1/px/backup-lxc', methods=['POST']) 434 | @require_api_key 435 | def px_backup_lxc(): 436 | """Create a backup of a specific LXC container via vzdump.""" 437 | data = request.get_json() 438 | if not data or 'vmid' not in data or 'storage' not in data: 439 | return jsonify({"error": "Missing 'vmid' or 'storage' in request body"}), 400 440 | 441 | cmd_parts = ['px', 'backup-lxc', data['vmid']] 442 | stdout, stderr, rc = run_lws_command(cmd_parts, data) # Pass remaining options 443 | return format_response(stdout, stderr, rc) 444 | 445 | @app.route('/api/v1/px/image', methods=['POST']) 446 | @require_api_key 447 | def px_image_add(): 448 | """Create a template image from an LXC container.""" 449 | data = request.get_json() 450 | if not data or 'instance_id' not in data or 'template_name' not in data: 451 | return jsonify({"error": "Missing 'instance_id' or 'template_name' in request body"}), 400 452 | 453 | cmd_parts = ['px', 'image-add', data['instance_id'], data['template_name']] 454 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 455 | return format_response(stdout, stderr, rc) 456 | 457 | @app.route('/api/v1/px/image/', methods=['DELETE']) 458 | @require_api_key 459 | def px_image_rm(template_name): 460 | """Delete a template image from Proxmox host.""" 461 | data = request.args.to_dict() # Get region/az from query params 462 | cmd_parts = ['px', 'image-rm', template_name] 463 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 464 | return format_response(stdout, stderr, rc) 465 | 466 | @app.route('/api/v1/px/security-groups', methods=['POST']) 467 | @require_api_key 468 | def px_security_group_add(): 469 | """Create a security group.""" 470 | data = request.get_json() 471 | if not data or 'group_name' not in data: 472 | return jsonify({"error": "Missing 'group_name' in request body"}), 400 473 | 474 | cmd_parts = ['px', 'security-group-add', data['group_name']] 475 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 476 | return format_response(stdout, stderr, rc) 477 | 478 | @app.route('/api/v1/px/security-groups/', methods=['DELETE']) 479 | @require_api_key 480 | def px_security_group_rm(group_name): 481 | """Delete a security group.""" 482 | data = request.args.to_dict() # Get region/az from query params 483 | cmd_parts = ['px', 'security-group-rm', group_name] 484 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 485 | return format_response(stdout, stderr, rc) 486 | 487 | @app.route('/api/v1/px/security-groups//rules', methods=['POST']) 488 | @require_api_key 489 | def px_security_group_rule_add(group_name): 490 | """Add a rule to a security group.""" 491 | data = request.get_json() 492 | if not data or 'direction' not in data: 493 | return jsonify({"error": "Missing 'direction' in request body"}), 400 494 | 495 | cmd_parts = ['px', 'security-group-rule-add', group_name] 496 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 497 | return format_response(stdout, stderr, rc) 498 | 499 | @app.route('/api/v1/px/security-groups//rules', methods=['DELETE']) 500 | @require_api_key 501 | def px_security_group_rule_rm(group_name): 502 | """Remove a rule from a security group.""" 503 | # Rules are complex to identify uniquely via URL path. 504 | # We pass all rule details in the JSON body for the command to handle. 505 | data = request.get_json() 506 | if not data or 'direction' not in data: 507 | return jsonify({"error": "Missing rule details ('direction', etc.) in request body"}), 400 508 | 509 | cmd_parts = ['px', 'security-group-rule-rm', group_name] 510 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 511 | return format_response(stdout, stderr, rc) 512 | 513 | @app.route('/api/v1/px/security-groups/attach', methods=['POST']) 514 | @require_api_key 515 | def px_security_group_attach(): 516 | """Attach security group to an LXC container.""" 517 | data = request.get_json() 518 | if not data or 'group_name' not in data or 'vmid' not in data: 519 | return jsonify({"error": "Missing 'group_name' or 'vmid' in request body"}), 400 520 | 521 | cmd_parts = ['px', 'security-group-attach', data['group_name'], data['vmid']] 522 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 523 | return format_response(stdout, stderr, rc) 524 | 525 | @app.route('/api/v1/px/security-groups/detach', methods=['POST']) 526 | @require_api_key 527 | def px_security_group_detach(): 528 | """Detach security group from an LXC container.""" 529 | data = request.get_json() 530 | if not data or 'group_name' not in data or 'vmid' not in data: 531 | return jsonify({"error": "Missing 'group_name' or 'vmid' in request body"}), 400 532 | 533 | cmd_parts = ['px', 'security-group-detach', data['group_name'], data['vmid']] 534 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 535 | return format_response(stdout, stderr, rc) 536 | 537 | @app.route('/api/v1/px/templates', methods=['GET']) 538 | @require_api_key 539 | def px_list_templates(): 540 | """List all available templates.""" 541 | cmd_parts = ['px', 'templates'] 542 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 543 | return format_response(stdout, stderr, rc) 544 | 545 | @app.route('/api/v1/px/security-groups', methods=['GET']) 546 | @require_api_key 547 | def px_list_security_groups(): 548 | """List all security groups and their rules.""" 549 | cmd_parts = ['px', 'security-groups'] 550 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 551 | return format_response(stdout, stderr, rc) 552 | 553 | @app.route('/api/v1/px/exec', methods=['POST']) 554 | @require_api_key 555 | def px_exec(): 556 | """Execute an arbitrary command on a Proxmox host.""" 557 | data = request.get_json() 558 | if not data or 'command' not in data: 559 | return jsonify({"error": "Missing 'command' in request body"}), 400 560 | 561 | # The command itself might have multiple parts, handle as list or string 562 | cmd_to_exec = data['command'] 563 | if isinstance(cmd_to_exec, str): 564 | cmd_to_exec_parts = shlex.split(cmd_to_exec) # Split safely 565 | elif isinstance(cmd_to_exec, list): 566 | cmd_to_exec_parts = cmd_to_exec 567 | else: 568 | return jsonify({"error": "'command' must be a string or a list of strings"}), 400 569 | 570 | cmd_parts = ['px', 'exec'] + cmd_to_exec_parts 571 | 572 | # Pass region/az from data if present 573 | exec_data = {k: v for k, v in data.items() if k in ['region', 'az']} 574 | 575 | stdout, stderr, rc = run_lws_command(cmd_parts, exec_data) 576 | return format_response(stdout, stderr, rc) 577 | 578 | @app.route('/api/v1/px/backup', methods=['POST']) 579 | @require_api_key 580 | def px_backup_host_config(): 581 | """Backup configurations from a Proxmox host.""" 582 | data = request.get_json() 583 | if not data or 'backup_dir' not in data: 584 | return jsonify({"error": "Missing 'backup_dir' in request body"}), 400 585 | 586 | cmd_parts = ['px', 'backup', data['backup_dir']] 587 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 588 | return format_response(stdout, stderr, rc) 589 | 590 | 591 | # --- LXC Container Management (`lxc`) --- 592 | @app.route('/api/v1/lxc/instances', methods=['POST']) 593 | @require_api_key 594 | def lxc_run_instance(): 595 | """Create and start LXC containers.""" 596 | data = request.get_json() 597 | if not data or 'image_id' not in data: 598 | return jsonify({"error": "Missing 'image_id' in request body"}), 400 599 | 600 | cmd_parts = ['lxc', 'run'] 601 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 602 | return format_response(stdout, stderr, rc) 603 | 604 | @app.route('/api/v1/lxc/instances/stop', methods=['POST']) 605 | @require_api_key 606 | def lxc_stop_instances(): 607 | """Stop running LXC containers.""" 608 | data = request.get_json() 609 | if not data or 'instance_ids' not in data or not isinstance(data['instance_ids'], list): 610 | return jsonify({"error": "Missing 'instance_ids' (list) in request body"}), 400 611 | 612 | cmd_parts = ['lxc', 'stop'] + data['instance_ids'] 613 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 614 | return format_response(stdout, stderr, rc) 615 | 616 | @app.route('/api/v1/lxc/instances/terminate', methods=['POST']) # Using POST for multiple IDs 617 | @require_api_key 618 | def lxc_terminate_instances(): 619 | """Terminate (destroy) LXC containers.""" 620 | data = request.get_json() 621 | if not data or 'instance_ids' not in data or not isinstance(data['instance_ids'], list): 622 | return jsonify({"error": "Missing 'instance_ids' (list) in request body"}), 400 623 | 624 | cmd_parts = ['lxc', 'terminate'] + data['instance_ids'] 625 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 626 | return format_response(stdout, stderr, rc) 627 | 628 | @app.route('/api/v1/lxc/instances', methods=['GET']) 629 | @require_api_key 630 | def lxc_list_instances(): 631 | """List all LXC containers.""" 632 | cmd_parts = ['lxc', 'show'] # 'lxc show' without IDs lists all 633 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 634 | return format_response(stdout, stderr, rc) 635 | 636 | @app.route('/api/v1/lxc/instances/', methods=['GET']) 637 | @require_api_key 638 | def lxc_describe_instance(instance_id): 639 | """Describe a specific LXC container.""" 640 | cmd_parts = ['lxc', 'show', instance_id] 641 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 642 | return format_response(stdout, stderr, rc) 643 | 644 | @app.route('/api/v1/lxc/instances/scale', methods=['POST']) # Using POST for multiple IDs 645 | @require_api_key 646 | def lxc_scale_instances(): 647 | """Scale resources for LXC containers.""" 648 | data = request.get_json() 649 | if not data or 'instance_ids' not in data or not isinstance(data['instance_ids'], list): 650 | return jsonify({"error": "Missing 'instance_ids' (list) in request body"}), 400 651 | if not any(k in data for k in ['memory', 'cpulimit', 'cpucores', 'storage_size', 'net_limit', 'disk_read_limit', 'disk_write_limit']): 652 | return jsonify({"error": "Missing scaling parameters (memory, cpulimit, etc.)"}), 400 653 | 654 | cmd_parts = ['lxc', 'scale'] + data['instance_ids'] 655 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 656 | return format_response(stdout, stderr, rc) 657 | 658 | @app.route('/api/v1/lxc/instances//snapshots', methods=['POST']) 659 | @require_api_key 660 | def lxc_snapshot_add(instance_id): 661 | """Create a snapshot of an LXC container.""" 662 | data = request.get_json() 663 | if not data or 'snapshot_name' not in data: 664 | return jsonify({"error": "Missing 'snapshot_name' in request body"}), 400 665 | 666 | cmd_parts = ['lxc', 'snapshot-add', instance_id, data['snapshot_name']] 667 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 668 | return format_response(stdout, stderr, rc) 669 | 670 | @app.route('/api/v1/lxc/instances//snapshots/', methods=['DELETE']) 671 | @require_api_key 672 | def lxc_snapshot_rm(instance_id, snapshot_name): 673 | """Delete a snapshot of an LXC container.""" 674 | data = request.args.to_dict() # region/az from query 675 | cmd_parts = ['lxc', 'snapshot-rm', instance_id, snapshot_name] 676 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 677 | return format_response(stdout, stderr, rc) 678 | 679 | @app.route('/api/v1/lxc/instances//snapshots', methods=['GET']) 680 | @require_api_key 681 | def lxc_list_snapshots(instance_id): 682 | """List all snapshots of an LXC container.""" 683 | cmd_parts = ['lxc', 'snapshots', instance_id] 684 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 685 | return format_response(stdout, stderr, rc) 686 | 687 | @app.route('/api/v1/lxc/instances/start', methods=['POST']) 688 | @require_api_key 689 | def lxc_start_instances(): 690 | """Start stopped LXC containers.""" 691 | data = request.get_json() 692 | if not data or 'instance_ids' not in data or not isinstance(data['instance_ids'], list): 693 | return jsonify({"error": "Missing 'instance_ids' (list) in request body"}), 400 694 | 695 | cmd_parts = ['lxc', 'start'] + data['instance_ids'] 696 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 697 | return format_response(stdout, stderr, rc) 698 | 699 | @app.route('/api/v1/lxc/instances/reboot', methods=['POST']) 700 | @require_api_key 701 | def lxc_reboot_instances(): 702 | """Reboot running LXC containers.""" 703 | data = request.get_json() 704 | if not data or 'instance_ids' not in data or not isinstance(data['instance_ids'], list): 705 | return jsonify({"error": "Missing 'instance_ids' (list) in request body"}), 400 706 | 707 | cmd_parts = ['lxc', 'reboot'] + data['instance_ids'] 708 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 709 | return format_response(stdout, stderr, rc) 710 | 711 | @app.route('/api/v1/lxc/instances//volumes/attach', methods=['POST']) 712 | @require_api_key 713 | def lxc_volume_attach(instance_id): 714 | """Attach a storage volume to an LXC container.""" 715 | data = request.get_json() 716 | if not data or 'volume_name' not in data or 'volume_size' not in data or 'mount_point' not in data: 717 | return jsonify({"error": "Missing 'volume_name', 'volume_size', or 'mount_point' in request body"}), 400 718 | 719 | cmd_parts = ['lxc', 'volume-attach', instance_id, data['volume_name'], data['volume_size']] 720 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 721 | return format_response(stdout, stderr, rc) 722 | 723 | @app.route('/api/v1/lxc/instances//volumes/detach', methods=['POST']) 724 | @require_api_key 725 | def lxc_volume_detach(instance_id): 726 | """Detach a storage volume from an LXC container.""" 727 | data = request.get_json() 728 | if not data or 'volume_name' not in data: 729 | return jsonify({"error": "Missing 'volume_name' in request body"}), 400 730 | 731 | cmd_parts = ['lxc', 'volume-detach', instance_id, data['volume_name']] 732 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 733 | return format_response(stdout, stderr, rc) 734 | 735 | @app.route('/api/v1/lxc/instances/status', methods=['POST']) # POST for multiple IDs 736 | @require_api_key 737 | def lxc_monitor_instances(): 738 | """Monitor resources of LXC containers.""" 739 | data = request.get_json() 740 | if not data or 'instance_ids' not in data or not isinstance(data['instance_ids'], list): 741 | return jsonify({"error": "Missing 'instance_ids' (list) in request body"}), 400 742 | 743 | cmd_parts = ['lxc', 'status'] + data['instance_ids'] 744 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 745 | return format_response(stdout, stderr, rc) 746 | 747 | @app.route('/api/v1/lxc/instances//service', methods=['POST']) 748 | @require_api_key 749 | def lxc_service(instance_id): 750 | """Manage a service within an LXC container.""" 751 | data = request.get_json() 752 | if not data or 'action' not in data or 'service_name' not in data: 753 | return jsonify({"error": "Missing 'action' or 'service_name' in request body"}), 400 754 | 755 | valid_actions = ['status', 'start', 'stop', 'restart', 'reload', 'enable'] 756 | if data['action'] not in valid_actions: 757 | return jsonify({"error": f"Invalid action. Must be one of: {valid_actions}"}), 400 758 | 759 | cmd_parts = ['lxc', 'service', data['action'], data['service_name'], instance_id] 760 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 761 | return format_response(stdout, stderr, rc) 762 | 763 | @app.route('/api/v1/lxc/instances//migrate', methods=['POST']) 764 | @require_api_key 765 | def lxc_migrate(instance_id): 766 | """Migrate LXC container between hosts.""" 767 | data = request.get_json() 768 | if not data or 'target_host' not in data: 769 | return jsonify({"error": "Missing 'target_host' in request body"}), 400 770 | 771 | cmd_parts = ['lxc', 'migrate', instance_id] 772 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 773 | return format_response(stdout, stderr, rc) 774 | 775 | @app.route('/api/v1/lxc/instances//storage', methods=['GET']) 776 | @require_api_key 777 | def lxc_show_storage(instance_id): 778 | """List storage details for an LXC container.""" 779 | cmd_parts = ['lxc', 'show-storage', instance_id] 780 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 781 | return format_response(stdout, stderr, rc) 782 | 783 | @app.route('/api/v1/lxc/instances//scale-check', methods=['GET']) 784 | @require_api_key 785 | def lxc_scale_check(instance_id): 786 | """Suggest scaling adjustments for an LXC container.""" 787 | cmd_parts = ['lxc', 'scale-check', instance_id] 788 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 789 | return format_response(stdout, stderr, rc) 790 | 791 | @app.route('/api/v1/lxc/instances/clone', methods=['POST']) 792 | @require_api_key 793 | def lxc_clone(): 794 | """Clone an LXC container.""" 795 | data = request.get_json() 796 | if not data or 'source_instance_id' not in data or 'target_instance_id' not in data: 797 | return jsonify({"error": "Missing 'source_instance_id' or 'target_instance_id' in request body"}), 400 798 | 799 | cmd_parts = ['lxc', 'clone', data['source_instance_id'], data['target_instance_id']] 800 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 801 | return format_response(stdout, stderr, rc) 802 | 803 | @app.route('/api/v1/lxc/instances//exec', methods=['POST']) 804 | @require_api_key 805 | def lxc_exec(instance_id): 806 | """Execute an arbitrary command in an LXC container.""" 807 | data = request.get_json() 808 | if not data or 'command' not in data: 809 | return jsonify({"error": "Missing 'command' in request body"}), 400 810 | 811 | # The command itself might have multiple parts 812 | cmd_to_exec = data['command'] 813 | if isinstance(cmd_to_exec, str): 814 | cmd_to_exec_parts = shlex.split(cmd_to_exec) # Split safely 815 | elif isinstance(cmd_to_exec, list): 816 | cmd_to_exec_parts = cmd_to_exec 817 | else: 818 | return jsonify({"error": "'command' must be a string or a list of strings"}), 400 819 | 820 | # The lws command expects the command parts after the instance ID 821 | cmd_parts = ['lxc', 'exec', instance_id] + cmd_to_exec_parts 822 | 823 | # Pass region/az from data if present 824 | exec_data = {k: v for k, v in data.items() if k in ['region', 'az']} 825 | 826 | stdout, stderr, rc = run_lws_command(cmd_parts, exec_data) 827 | return format_response(stdout, stderr, rc) 828 | 829 | @app.route('/api/v1/lxc/instances//net-check', methods=['GET']) 830 | @require_api_key 831 | def lxc_net_check(instance_id): 832 | """Perform simple network checks on an LXC container.""" 833 | args = request.args.to_dict() 834 | if 'protocol' not in args or 'port' not in args: 835 | return jsonify({"error": "Missing 'protocol' or 'port' query parameters"}), 400 836 | 837 | cmd_parts = ['lxc', 'net', instance_id, args['protocol'], args['port']] 838 | stdout, stderr, rc = run_lws_command(cmd_parts, args) 839 | return format_response(stdout, stderr, rc) 840 | 841 | @app.route('/api/v1/lxc/instances//info', methods=['GET']) 842 | @require_api_key 843 | def lxc_show_info(instance_id): 844 | """Retrieve IP address, hostname, DNS, and name for an LXC container.""" 845 | cmd_parts = ['lxc', 'show-info', instance_id] 846 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 847 | return format_response(stdout, stderr, rc) 848 | 849 | @app.route('/api/v1/lxc/instances//public-ip', methods=['GET']) 850 | @require_api_key 851 | def lxc_show_public_ip(instance_id): 852 | """Retrieve the public IP address of an LXC container.""" 853 | cmd_parts = ['lxc', 'show-public-ip', instance_id] 854 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 855 | return format_response(stdout, stderr, rc) 856 | 857 | @app.route('/api/v1/lxc/instances//health-check', methods=['GET']) 858 | @require_api_key 859 | def lxc_health_check(instance_id): 860 | """Perform health check on an LXC container.""" 861 | cmd_parts = ['lxc', 'health-check', instance_id] 862 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 863 | return format_response(stdout, stderr, rc) 864 | 865 | @app.route('/api/v1/lxc/instances//restore', methods=['POST']) 866 | @require_api_key 867 | def lxc_backup_restore(instance_id): 868 | """Restore an LXC container from a backup file.""" 869 | data = request.get_json() 870 | if not data or 'backup_file' not in data: 871 | return jsonify({"error": "Missing 'backup_file' in request body"}), 400 872 | 873 | cmd_parts = ['lxc', 'backup-restore', instance_id] 874 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 875 | return format_response(stdout, stderr, rc) 876 | 877 | @app.route('/api/v1/lxc/instances//backup', methods=['POST']) 878 | @require_api_key 879 | def lxc_backup_create(instance_id): 880 | """Create a backup of an LXC container.""" 881 | data = request.get_json() or {} # Allow empty body, use defaults 882 | cmd_parts = ['lxc', 'backup-create', instance_id] 883 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 884 | return format_response(stdout, stderr, rc) 885 | 886 | @app.route('/api/v1/lxc/instances//resources', methods=['GET']) 887 | @require_api_key 888 | def lxc_monitor_resources(instance_id): 889 | """Monitor real-time resource usage of an LXC container.""" 890 | cmd_parts = ['lxc', 'resources', instance_id] 891 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 892 | return format_response(stdout, stderr, rc) 893 | 894 | @app.route('/api/v1/lxc/instances//report', methods=['GET']) 895 | @require_api_key 896 | def lxc_generate_report(instance_id): 897 | """Generate a comprehensive report about an LXC container.""" 898 | cmd_parts = ['lxc', 'report', instance_id] 899 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 900 | # The command might output JSON directly if --output json is used 901 | return format_response(stdout, stderr, rc) 902 | 903 | 904 | # --- Docker Management (`app`) --- 905 | @app.route('/api/v1/lxc/instances//app/setup', methods=['POST']) 906 | @require_api_key 907 | def app_setup(instance_id): 908 | """Install Docker and Compose on an LXC container.""" 909 | data = request.get_json() or {} 910 | package_name = data.get('package_name', 'docker') # Default from lws.py 911 | cmd_parts = ['app', 'setup', instance_id, package_name] 912 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 913 | return format_response(stdout, stderr, rc) 914 | 915 | @app.route('/api/v1/lxc/instances//app/run', methods=['POST']) 916 | @require_api_key 917 | def app_run_docker(instance_id): 918 | """Execute docker run inside an LXC container.""" 919 | data = request.get_json() 920 | if not data or 'docker_command' not in data: 921 | return jsonify({"error": "Missing 'docker_command' (string or list) in request body"}), 400 922 | 923 | docker_cmd_parts = data['docker_command'] 924 | if isinstance(docker_cmd_parts, str): 925 | docker_cmd_parts = shlex.split(docker_cmd_parts) 926 | elif not isinstance(docker_cmd_parts, list): 927 | return jsonify({"error": "'docker_command' must be a string or list"}), 400 928 | 929 | # lws app run -- 930 | cmd_parts = ['app', 'run', instance_id] + docker_cmd_parts 931 | 932 | # Pass region/az from data if present 933 | run_data = {k: v for k, v in data.items() if k in ['region', 'az']} 934 | 935 | stdout, stderr, rc = run_lws_command(cmd_parts, run_data) 936 | return format_response(stdout, stderr, rc) 937 | 938 | @app.route('/api/v1/lxc/instances//app/deploy', methods=['POST']) 939 | @require_api_key 940 | def app_deploy_compose(instance_id): 941 | """Manage apps with Compose on LXC containers.""" 942 | data = request.get_json() 943 | if not data or 'action' not in data or 'compose_file' not in data: 944 | return jsonify({"error": "Missing 'action' or 'compose_file' in request body"}), 400 945 | 946 | valid_actions = ['install', 'uninstall', 'start', 'stop', 'restart', 'status'] 947 | if data['action'] not in valid_actions: 948 | return jsonify({"error": f"Invalid action. Must be one of: {valid_actions}"}), 400 949 | 950 | cmd_parts = ['app', 'deploy', data['action'], instance_id] 951 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 952 | return format_response(stdout, stderr, rc) 953 | 954 | @app.route('/api/v1/lxc/instances//app/update', methods=['POST']) 955 | @require_api_key 956 | def app_update_compose(instance_id): 957 | """Update app within an LXC container via Compose.""" 958 | data = request.get_json() 959 | if not data or 'compose_file' not in data: 960 | return jsonify({"error": "Missing 'compose_file' in request body"}), 400 961 | 962 | cmd_parts = ['app', 'update', instance_id] 963 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 964 | return format_response(stdout, stderr, rc) 965 | 966 | @app.route('/api/v1/lxc/instances//app/logs/', methods=['GET']) 967 | @require_api_key 968 | def app_logs(instance_id, container_name_or_id): 969 | """Fetch Docker logs from an LXC container.""" 970 | cmd_parts = ['app', 'logs', instance_id, container_name_or_id] 971 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 972 | return format_response(stdout, stderr, rc) 973 | 974 | @app.route('/api/v1/lxc/instances//app/containers', methods=['GET']) 975 | @require_api_key 976 | def app_list_containers(instance_id): 977 | """List Docker containers in an LXC container.""" 978 | cmd_parts = ['app', 'list', instance_id] 979 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 980 | return format_response(stdout, stderr, rc) 981 | 982 | @app.route('/api/v1/lxc/instances/app/remove', methods=['POST']) # POST for multiple IDs 983 | @require_api_key 984 | def app_remove(instance_ids): 985 | """Uninstall Docker and Compose from LXC containers.""" 986 | data = request.get_json() 987 | if not data or 'instance_ids' not in data or not isinstance(data['instance_ids'], list): 988 | return jsonify({"error": "Missing 'instance_ids' (list) in request body"}), 400 989 | 990 | cmd_parts = ['app', 'remove'] + data['instance_ids'] 991 | stdout, stderr, rc = run_lws_command(cmd_parts, data) 992 | return format_response(stdout, stderr, rc) 993 | 994 | 995 | # --- Security (`sec`) --- 996 | @app.route('/api/v1/sec/discovery', methods=['GET']) 997 | @require_api_key 998 | def sec_discovery(): 999 | """Discover reachable hosts.""" 1000 | args = request.args.to_dict() 1001 | cmd_parts = ['sec', 'discovery'] 1002 | if 'lxc_id' in args: 1003 | cmd_parts.append(args['lxc_id']) # lxc_id is positional if present 1004 | 1005 | stdout, stderr, rc = run_lws_command(cmd_parts, args) 1006 | return format_response(stdout, stderr, rc) 1007 | 1008 | @app.route('/api/v1/lxc/instances//sec/scan', methods=['GET']) 1009 | @require_api_key 1010 | def sec_scan(instance_id): 1011 | """Perform a security scan on an LXC container.""" 1012 | cmd_parts = ['sec', 'scan', instance_id] 1013 | stdout, stderr, rc = run_lws_command(cmd_parts, request.args.to_dict()) 1014 | return format_response(stdout, stderr, rc) 1015 | 1016 | 1017 | # --- Main Execution --- 1018 | if __name__ == '__main__': 1019 | host = API_CONFIG.get('host', '127.0.0.1') 1020 | port = API_CONFIG.get('port', 8080) 1021 | debug = API_CONFIG.get('debug', False) 1022 | logging.info(f"Starting Flask server on {host}:{port} (Debug: {debug})") 1023 | app.run(host=host, port=port, debug=debug) 1024 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | use_local_only: false 2 | start_vmid: 10000 3 | default_storage: local-lvm 4 | default_network: vmbr0 5 | minimum_resources: 6 | cores: 1 7 | memory_mb: 512 8 | 9 | # API configuration 10 | api_key: "my-secure-api-key" # Replace with a strong, random API key 11 | api: 12 | host: "0.0.0.0" # Listen on all interfaces 13 | port: 8080 # Default port for the API server 14 | debug: false # Set to true for development only 15 | log_level: "INFO" # Log level (DEBUG, INFO, WARNING, ERROR) 16 | allowed_origins: # Example: Allow localhost server and file:// origin 17 | - "http://localhost:8080" # If serving ui.html via python http.server 18 | - "null" # For file:// access (use with caution) 19 | 20 | regions: 21 | eu-south-1: 22 | availability_zones: 23 | az1: 24 | host: proxmox1.public-fqdn.com # example: public FQDN (access must be secured) 25 | user: root 26 | ssh_password: password 27 | az2: 28 | host: 172.23.0.2 # example: VPN address 29 | user: root 30 | ssh_password: password 31 | az3: 32 | host: proxmox3.local # example: local network 33 | user: root 34 | ssh_password: password 35 | az4: 36 | host: 192.168.0.4 # example: LAN address 37 | user: root 38 | ssh_password: password 39 | 40 | eu-central-1: 41 | availability_zones: 42 | pve-rhine: 43 | host: pve-rhine.mydomain.com 44 | user: root 45 | ssh_password: password 46 | pve-alps: 47 | host: pve-alps.mydomain.com 48 | user: root 49 | ssh_password: password 50 | 51 | scaling: 52 | host_cpu: 53 | max_threshold: 80 # Maximum percentage of host CPU usage before considering a decrease 54 | min_threshold: 30 # Minimum percentage of host CPU usage before considering an increase 55 | step: 1 # Base increment or decrement of CPU cores on the host 56 | scale_up_multiplier: 1.5 # Multiplier applied to step size when scaling up 57 | scale_down_multiplier: 0.5 # Multiplier applied to step size when scaling down 58 | 59 | lxc_cpu: 60 | max_threshold: 80 # Maximum percentage of LXC CPU usage before considering a decrease 61 | min_threshold: 30 # Minimum percentage of LXC CPU usage before considering an increase 62 | step: 1 # Base increment or decrement of CPU cores in the LXC 63 | scale_up_multiplier: 1.5 # Multiplier applied to step size when scaling up 64 | scale_down_multiplier: 0.5 # Multiplier applied to step size when scaling down 65 | 66 | host_memory: 67 | max_threshold: 70 # Percentage of total memory on the host before considering a decrease 68 | min_threshold: 40 # Percentage of total memory on the host before considering an increase 69 | step_mb: 256 # Base amount of memory in MB to increase or decrease 70 | scale_up_multiplier: 1.25 # Multiplier applied to step size when scaling up 71 | scale_down_multiplier: 0.75 # Multiplier applied to step size when scaling down 72 | 73 | lxc_memory: 74 | max_threshold: 70 # Maximum percentage of LXC memory usage before considering a decrease 75 | min_threshold: 40 # Minimum percentage of LXC memory usage before considering an increase 76 | step_mb: 256 # Base amount of memory in MB to increase or decrease 77 | scale_up_multiplier: 1.25 # Multiplier applied to step size when scaling up 78 | scale_down_multiplier: 0.75 # Multiplier applied to step size when scaling down 79 | 80 | host_storage: 81 | max_threshold: 85 # Maximum percentage of storage usage on the host before considering a decrease 82 | min_threshold: 50 # Minimum percentage of storage usage on the host before considering an increase 83 | step_gb: 10 # Base increment or decrement of storage in GB 84 | scale_up_multiplier: 1.5 # Multiplier applied to step size when scaling up 85 | scale_down_multiplier: 0.5 # Multiplier applied to step size when scaling down 86 | 87 | lxc_storage: 88 | max_threshold: 85 # Maximum percentage of storage usage in the LXC before considering a decrease 89 | min_threshold: 50 # Minimum percentage of storage usage in the LXC before considering an increase 90 | step_gb: 10 # Base increment or decrement of storage in GB 91 | scale_up_multiplier: 1.5 # Multiplier applied to step size when scaling up 92 | scale_down_multiplier: 0.5 # Multiplier applied to step size when scaling down 93 | 94 | limits: 95 | min_memory_mb: 512 # Minimum allowed memory for any LXC container 96 | max_memory_mb: 32768 # Maximum allowed memory for any LXC container 97 | min_cpu_cores: 1 # Minimum allowed CPU cores for any LXC container 98 | max_cpu_cores: 16 # Maximum allowed CPU cores for any LXC container 99 | min_storage_gb: 10 # Minimum allowed storage for any LXC container 100 | max_storage_gb: 1024 # Maximum allowed storage for any LXC container 101 | 102 | general: 103 | scaling_interval: 5 # Interval in minutes to check for resource adjustments 104 | notify_user: true # Notify user via CLI output when scaling adjustments are made 105 | dry_run: false # If true, simulate scaling adjustments without applying changes 106 | scaling_log_level: DEBUG # Log level for scaling operations (DEBUG, INFO, WARN, ERROR) 107 | use_custom_scaling_algorithms: false # Enable if custom scaling algorithms are implemented 108 | 109 | security: 110 | discovery: 111 | proxmox_timeout: 2 112 | lxc_timeout: 2 113 | discovery_methods: ['ping'] 114 | max_parallel_workers: 10 # Maximum number of parallel workers 115 | 116 | instance_sizes: 117 | # Generic 118 | micro: 119 | memory: 512 120 | cpulimit: 1 121 | storage: local-lvm:4 122 | small: 123 | memory: 1024 124 | cpulimit: 1 125 | storage: local-lvm:8 126 | mid: 127 | memory: 2048 128 | cpulimit: 2 129 | storage: local-lvm:16 130 | large: 131 | memory: 4096 132 | cpulimit: 2 133 | storage: local-lvm:32 134 | x-large: 135 | memory: 8192 136 | cpulimit: 4 137 | storage: local-lvm:64 138 | xx-large: 139 | memory: 16384 140 | cpulimit: 8 141 | storage: local-lvm:128 142 | 143 | # Open-Source Self-Hosted Applications with Abstract Roles 144 | lws-minio: 145 | memory: 4096 # 4 GB 146 | cpulimit: 2 # 2 vCPUs 147 | storage: local-lvm:50 # 50 GB of storage 148 | # Example: MinIO for object storage. 149 | 150 | lws-postgres: 151 | memory: 4096 # 4 GB 152 | cpulimit: 2 # 2 vCPUs 153 | storage: local-lvm:40 # 40 GB of storage 154 | # Example: PostgreSQL for relational database. 155 | 156 | lws-mysql: 157 | memory: 4096 # 4 GB 158 | cpulimit: 2 # 2 vCPUs 159 | storage: local-lvm:40 # 40 GB of storage 160 | # Example: MySQL for relational database. 161 | 162 | lws-nosql: 163 | memory: 8192 # 8 GB 164 | cpulimit: 4 # 4 vCPUs 165 | storage: local-lvm:50 # 50 GB of storage 166 | # Example: MongoDB for NoSQL database. 167 | 168 | lws-cdn: 169 | memory: 1024 # 1 GB 170 | cpulimit: 1 # 1 vCPU 171 | storage: local-lvm:10 # 10 GB of storage 172 | # Example: Caddy for reverse proxy and CDN. 173 | 174 | lws-metrics-monitoring: 175 | memory: 2048 # 2 GB 176 | cpulimit: 1 # 1 vCPU 177 | storage: local-lvm:20 # 20 GB of storage 178 | # Example: Prometheus for metrics and monitoring. 179 | 180 | lws-metrics-visualization: 181 | memory: 2048 # 2 GB 182 | cpulimit: 1 # 1 vCPU 183 | storage: local-lvm:20 # 20 GB of storage 184 | # Example: Grafana for data visualization. 185 | 186 | lws-mq: 187 | memory: 2048 # 2 GB 188 | cpulimit: 1 # 1 vCPU 189 | storage: local-lvm:20 # 20 GB of storage 190 | # Example: Apache ActiveMQ for messaging queues. 191 | 192 | lws-firewall: 193 | memory: 4096 # 4 GB 194 | cpulimit: 2 # 2 vCPUs 195 | storage: local-lvm:20 # 20 GB of storage 196 | # Example: OPNsense for firewall and routing. 197 | 198 | lws-search-analytics: 199 | memory: 8192 # 8 GB 200 | cpulimit: 4 # 4 vCPUs 201 | storage: local-lvm:50 # 50 GB of storage 202 | # Example: OpenSearch for search and analytics. 203 | 204 | lws-serverless: 205 | memory: 2048 # 2 GB 206 | cpulimit: 2 # 2 vCPUs 207 | storage: local-lvm:20 # 20 GB of storage 208 | # Example: OpenFaaS for serverless functions. 209 | 210 | lws-email: 211 | memory: 4096 # 4 GB 212 | cpulimit: 2 # 2 vCPUs 213 | storage: local-lvm:40 # 40 GB of storage 214 | # Example: Mailcow for email management. 215 | 216 | lws-machine-learning: 217 | memory: 8192 # 8 GB 218 | cpulimit: 4 # 4 vCPUs 219 | storage: local-lvm:50 # 50 GB of storage 220 | # Example: Hugging Face Transformers for machine learning models. 221 | 222 | lws-identity-management: 223 | memory: 8192 # 8 GB 224 | cpulimit: 4 # 4 vCPUs 225 | storage: local-lvm:50 # 50 GB of storage 226 | # Example: Keycloak for identity and access management. 227 | 228 | lws-file-storage: 229 | memory: 4096 # 4 GB 230 | cpulimit: 2 # 2 vCPUs 231 | storage: local-lvm:50 # 50 GB of storage 232 | # Example: Nextcloud for file storage and collaboration. 233 | 234 | lws-data-warehouse: 235 | memory: 16384 # 16 GB 236 | cpulimit: 4 # 4 vCPUs 237 | storage: local-lvm:100 # 100 GB of storage 238 | # Example: ClickHouse for data warehousing. 239 | 240 | lws-messaging-broker: 241 | memory: 4096 # 4 GB 242 | cpulimit: 2 # 2 vCPUs 243 | storage: local-lvm:40 # 40 GB of storage 244 | # Example: RabbitMQ for messaging broker. 245 | 246 | lws-code-server: 247 | memory: 2048 # 2 GB 248 | cpulimit: 2 # 2 vCPUs 249 | storage: local-lvm:20 # 20 GB of storage 250 | # Example: Coder or code-server for online development environment. 251 | 252 | lws-log-aggregation: 253 | memory: 8192 # 8 GB 254 | cpulimit: 4 # 4 vCPUs 255 | storage: local-lvm:50 # 50 GB of storage 256 | # Example: Loki for log aggregation. 257 | 258 | lws-container-registry: 259 | memory: 4096 # 4 GB 260 | cpulimit: 2 # 2 vCPUs 261 | storage: local-lvm:50 # 50 GB of storage 262 | # Example: Harbor for container registry. 263 | 264 | lws-web: 265 | memory: 2048 # 2 GB 266 | cpulimit: 2 # 2 vCPUs 267 | storage: local-lvm:20 # 20 GB of storage 268 | # Example: Nginx for web server. 269 | 270 | lws-load-balancer: 271 | memory: 2048 # 2 GB 272 | cpulimit: 2 # 2 vCPUs 273 | storage: local-lvm:20 # 20 GB of storage 274 | # Example: HAProxy for load balancing. 275 | 276 | lws-redis: 277 | memory: 2048 # 2 GB 278 | cpulimit: 1 # 1 vCPU 279 | storage: local-lvm:10 # 10 GB of storage 280 | # Example: Redis for in-memory caching. 281 | 282 | lws-vpn: 283 | memory: 2048 # 2 GB 284 | cpulimit: 1 # 1 vCPU 285 | storage: local-lvm:10 # 10 GB of storage 286 | # Example: OpenVPN for VPN server. 287 | 288 | lws-backup-system: 289 | memory: 4096 # 4 GB 290 | cpulimit: 2 # 2 vCPUs 291 | storage: local-lvm:50 # 50 GB of storage 292 | # Example: Restic or Bacula for backup solutions. 293 | 294 | lws-static-site-generator: 295 | memory: 2048 # 2 GB 296 | cpulimit: 1 # 1 vCPU 297 | storage: local-lvm:10 # 10 GB of storage 298 | # Example: Hugo for static site generation. 299 | 300 | lws-dns: 301 | memory: 1024 # 1 GB 302 | cpulimit: 1 # 1 vCPU 303 | storage: local-lvm:10 # 10 GB of storage 304 | # Example: PowerDNS for DNS management. 305 | 306 | # General Purpose Instances: balance of compute, memory, and networking resources. 307 | t2-pico: 308 | memory: 512 # 1 GB 309 | cpulimit: 1 # 1 vCPU 310 | storage: local-lvm:8 # 8 GB of storage 311 | t2-micro: 312 | memory: 1024 # 1 GB 313 | cpulimit: 1 # 1 vCPU 314 | storage: local-lvm:8 # 8 GB of storage 315 | t2-small: 316 | memory: 2048 # 2 GB 317 | cpulimit: 1 # 1 vCPU 318 | storage: local-lvm:20 # 20 GB of storage 319 | t2-medium: 320 | memory: 4096 # 4 GB 321 | cpulimit: 2 # 2 vCPUs 322 | storage: local-lvm:40 # 40 GB of storage 323 | m5-large: 324 | memory: 8192 # 8 GB 325 | cpulimit: 2 # 2 vCPUs 326 | storage: local-lvm:50 # 50 GB of storage 327 | m5-xlarge: 328 | memory: 16384 # 16 GB 329 | cpulimit: 4 # 4 vCPUs 330 | storage: local-lvm:100 # 100 GB of storage 331 | m5-2xlarge: 332 | memory: 32768 # 32 GB 333 | cpulimit: 8 # 8 vCPUs 334 | storage: local-lvm:200 # 200 GB of storage 335 | 336 | # Compute Optimized Instances: applications that benefit from high-performance processors. 337 | c5-large: 338 | memory: 4096 # 4 GB 339 | cpulimit: 2 # 2 vCPUs 340 | storage: local-lvm:50 # 50 GB of storage 341 | c5-xlarge: 342 | memory: 8192 # 8 GB 343 | cpulimit: 4 # 4 vCPUs 344 | storage: local-lvm:100 # 100 GB of storage 345 | c5-2xlarge: 346 | memory: 16384 # 16 GB 347 | cpulimit: 8 # 8 vCPUs 348 | storage: local-lvm:200 # 200 GB of storage 349 | c5-4xlarge: 350 | memory: 32768 # 32 GB 351 | cpulimit: 16 # 16 vCPUs 352 | storage: local-lvm:400 # 400 GB of storage 353 | 354 | # Memory Optimized Instances: memory-intensive applications like databases. 355 | r5-large: 356 | memory: 16384 # 16 GB 357 | cpulimit: 2 # 2 vCPUs 358 | storage: local-lvm:100 # 100 GB of storage 359 | r5-xlarge: 360 | memory: 32768 # 32 GB 361 | cpulimit: 4 # 4 vCPUs 362 | storage: local-lvm:200 # 200 GB of storage 363 | r5-2xlarge: 364 | memory: 65536 # 64 GB 365 | cpulimit: 8 # 8 vCPUs 366 | storage: local-lvm:400 # 400 GB of storage 367 | x1e-xlarge: 368 | memory: 65536 # 64 GB 369 | cpulimit: 4 # 4 vCPUs 370 | storage: local-lvm:200 # 200 GB of storage 371 | x1e-2xlarge: 372 | memory: 131072 # 128 GB 373 | cpulimit: 8 # 8 vCPUs 374 | storage: local-lvm:400 # 400 GB of storage 375 | x1e-4xlarge: 376 | memory: 262144 # 256 GB 377 | cpulimit: 16 # 16 vCPUs 378 | storage: local-lvm:800 # 800 GB of storage 379 | 380 | # Storage Optimized Instances: high, sequential read and write access to very large datasets on local storage. 381 | i3-large: 382 | memory: 15360 # 15 GB 383 | cpulimit: 2 # 2 vCPUs 384 | storage: local-lvm:500 # 500 GB of storage 385 | i3-xlarge: 386 | memory: 30720 # 30 GB 387 | cpulimit: 4 # 4 vCPUs 388 | storage: local-lvm:1000 # 1 TB of storage 389 | i3-2xlarge: 390 | memory: 61440 # 60 GB 391 | cpulimit: 8 # 8 vCPUs 392 | storage: local-lvm:2000 # 2 TB of storage 393 | i3-4xlarge: 394 | memory: 122880 # 120 GB 395 | cpulimit: 16 # 16 vCPUs 396 | storage: local-lvm:4000 # 4 TB of storage 397 | 398 | # GPU Instances: machine learning, graphics processing, or general-purpose GPU computing. 399 | p3-large: 400 | memory: 15360 # 15 GB 401 | cpulimit: 2 # 2 vCPUs 402 | storage: local-lvm:100 # 100 GB of storage 403 | p3-xlarge: 404 | memory: 30720 # 30 GB 405 | cpulimit: 4 # 4 vCPUs 406 | storage: local-lvm:200 # 200 GB of storage 407 | p3-2xlarge: 408 | memory: 61440 # 60 GB 409 | cpulimit: 8 # 8 vCPUs 410 | storage: local-lvm:400 # 400 GB of storage 411 | p3-8xlarge: 412 | memory: 245760 # 240 GB 413 | cpulimit: 32 # 32 vCPUs 414 | storage: local-lvm:1600 # 1.6 TB of storage 415 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click>=8.1.7,<9.0.0 2 | requests>=2.31.0,<3.0.0 3 | pyyaml>=6.0.1,<7.0.0 4 | flask-swagger-ui>=5.11,<6.0.0 5 | flask>=3.0.2,<4.0.0 6 | flask-cors>=4.0.0,<5.0.0 7 | tqdm>=4.66.2,<5.0.0 # For progress bars in lws.py 8 | -------------------------------------------------------------------------------- /ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | LWS API Interface 16 | 129 | 130 | 131 |
132 |

LWS API Interface

133 | 134 |
135 | 136 | 137 |
138 | 139 |
140 | 141 | 142 | 143 | 144 |
145 |
146 | 147 | 148 | 149 | 150 |
151 | 152 | 153 |

Response / Output

154 |
155 |
156 | Loading... 157 |
158 |
API responses will appear here...
159 |
160 |
161 | 162 | 261 | 262 | 263 | --------------------------------------------------------------------------------