diff options
| author | diogo464 <[email protected]> | 2025-05-28 20:46:55 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-05-28 20:46:55 +0100 |
| commit | 05f7444ecd2e3405b0e595042166ec5f8a3395ab (patch) | |
| tree | 2b28bca5dab0f2ec79c41893732b5d6866725f56 | |
init
| -rw-r--r-- | .cursor/rules/project.mdc | 24 | ||||
| -rw-r--r-- | .gitignore | 49 | ||||
| -rw-r--r-- | Containerfile | 37 | ||||
| -rw-r--r-- | README.md | 184 | ||||
| -rw-r--r-- | app.py | 154 | ||||
| -rwxr-xr-x | build.sh | 146 | ||||
| -rw-r--r-- | index.html | 116 | ||||
| -rw-r--r-- | logo.png | bin | 0 -> 1602427 bytes | |||
| -rw-r--r-- | requirements.txt | 2 | ||||
| -rw-r--r-- | script.js | 156 |
10 files changed, 868 insertions, 0 deletions
diff --git a/.cursor/rules/project.mdc b/.cursor/rules/project.mdc new file mode 100644 index 0000000..2d0eae9 --- /dev/null +++ b/.cursor/rules/project.mdc | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | --- | ||
| 2 | description: | ||
| 3 | globs: | ||
| 4 | alwaysApply: true | ||
| 5 | --- | ||
| 6 | bonsai is a python command line tool used to generate realistic network latency graphs using a yaml config. | ||
| 7 | |||
| 8 | The tool is called with | ||
| 9 | ``` | ||
| 10 | python -m main --net_config <network-config>.yaml --output_dir <output_dir> | ||
| 11 | ``` | ||
| 12 | The network config is a yaml file whose contents don't really matter and the output directory must exist and will be populated with two files `edges.csv` and `nodes.csv`. | ||
| 13 | |||
| 14 | The goal of this project, `bonsai-web` is to create a simple web interface for bonsai since it requires a specific version of python and quite a few dependencies. | ||
| 15 | You can assume that the bonsai source code is under the directory `/usr/src/bonsai` so you use subprocess in that directory to execute the command above. | ||
| 16 | The web page we are trying to create is simple and will contain the following components. | ||
| 17 | + single html page using picocss | ||
| 18 | + single javascript file for client side logic | ||
| 19 | + simple flask backend | ||
| 20 | |||
| 21 | The html page should have a header with the name bonsai and an image logo. The logo will be present at `logo.png`. The page should have some description text at the top, followed by a text box for the user to provide a configuration and then a button to generate the output files. When the user clicks that button a POST request with the contents of the config should be made to `/` and the server should execute bonsai and return a json object where the keys are the names of the generated files and the values associated with those keys will be the contents of the files. | ||
| 22 | The page should then display, bellow the button, the list of files with a button to download and one to view. The javascript file should contain the code for all this logic. | ||
| 23 | |||
| 24 | There should also be a `Containerfile` that creates a container image for this project. Make sure to use python version 3.9 as the base image and leave some space for me to write the code required to setup the bonsai project, you just need to assume that it will be present at the above location. \ No newline at end of file | ||
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77e4615 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | # Python | ||
| 2 | __pycache__/ | ||
| 3 | *.py[cod] | ||
| 4 | *$py.class | ||
| 5 | *.so | ||
| 6 | .Python | ||
| 7 | build/ | ||
| 8 | develop-eggs/ | ||
| 9 | dist/ | ||
| 10 | downloads/ | ||
| 11 | eggs/ | ||
| 12 | .eggs/ | ||
| 13 | lib/ | ||
| 14 | lib64/ | ||
| 15 | parts/ | ||
| 16 | sdist/ | ||
| 17 | var/ | ||
| 18 | wheels/ | ||
| 19 | *.egg-info/ | ||
| 20 | .installed.cfg | ||
| 21 | *.egg | ||
| 22 | MANIFEST | ||
| 23 | |||
| 24 | # Virtual Environment | ||
| 25 | venv/ | ||
| 26 | env/ | ||
| 27 | ENV/ | ||
| 28 | |||
| 29 | # IDE | ||
| 30 | .vscode/ | ||
| 31 | .idea/ | ||
| 32 | *.swp | ||
| 33 | *.swo | ||
| 34 | |||
| 35 | # OS | ||
| 36 | .DS_Store | ||
| 37 | Thumbs.db | ||
| 38 | |||
| 39 | # Flask | ||
| 40 | instance/ | ||
| 41 | .webassets-cache | ||
| 42 | |||
| 43 | # Environment variables | ||
| 44 | .env | ||
| 45 | .env.local | ||
| 46 | |||
| 47 | # Container artifacts | ||
| 48 | .buildah/ | ||
| 49 | .containerignore \ No newline at end of file | ||
diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..c8ff201 --- /dev/null +++ b/Containerfile | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | FROM python:3.9-slim | ||
| 2 | |||
| 3 | # Set working directory | ||
| 4 | WORKDIR /app | ||
| 5 | |||
| 6 | # Install system dependencies | ||
| 7 | RUN apt-get update && apt-get install -y \ | ||
| 8 | git \ | ||
| 9 | && rm -rf /var/lib/apt/lists/* | ||
| 10 | |||
| 11 | # Copy requirements and install Python dependencies | ||
| 12 | COPY requirements.txt . | ||
| 13 | RUN pip install --no-cache-dir -r requirements.txt | ||
| 14 | |||
| 15 | # === BONSAI SETUP SECTION === | ||
| 16 | WORKDIR /usr/src/bonsai | ||
| 17 | RUN git clone https://codelab.fct.unl.pt/di/computer-systems/bonsai . && \ | ||
| 18 | pip install --no-cache-dir 'torch~=2.0.1' && \ | ||
| 19 | pip install --no-cache-dir -r requirements.txt | ||
| 20 | # === END BONSAI SETUP SECTION === | ||
| 21 | |||
| 22 | # Copy application files | ||
| 23 | COPY app.py . | ||
| 24 | COPY index.html . | ||
| 25 | COPY script.js . | ||
| 26 | COPY logo.png . | ||
| 27 | |||
| 28 | # Expose the port | ||
| 29 | EXPOSE 5000 | ||
| 30 | |||
| 31 | # Set environment variables | ||
| 32 | ENV FLASK_APP=app.py | ||
| 33 | ENV FLASK_ENV=production | ||
| 34 | ENV BONSAI_TEST_MODE=false | ||
| 35 | |||
| 36 | # Run the application | ||
| 37 | CMD ["python", "app.py"] \ No newline at end of file | ||
diff --git a/README.md b/README.md new file mode 100644 index 0000000..1467530 --- /dev/null +++ b/README.md | |||
| @@ -0,0 +1,184 @@ | |||
| 1 | # Bonsai Web | ||
| 2 | |||
| 3 | A simple web interface for the Bonsai network latency graph generator. | ||
| 4 | |||
| 5 | ## Features | ||
| 6 | |||
| 7 | - Clean, modern UI using Pico CSS | ||
| 8 | - YAML configuration input | ||
| 9 | - Real-time file generation | ||
| 10 | - File viewing and downloading capabilities | ||
| 11 | - Containerized deployment support | ||
| 12 | |||
| 13 | ## Local Development | ||
| 14 | |||
| 15 | ### Prerequisites | ||
| 16 | |||
| 17 | - Python 3.9+ | ||
| 18 | - Flask | ||
| 19 | |||
| 20 | ### Installation | ||
| 21 | |||
| 22 | 1. Clone this repository | ||
| 23 | 2. Install dependencies: | ||
| 24 | ```bash | ||
| 25 | pip install -r requirements.txt | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ### Running Locally | ||
| 29 | |||
| 30 | ```bash | ||
| 31 | python app.py | ||
| 32 | ``` | ||
| 33 | |||
| 34 | The application will be available at `http://localhost:5000` | ||
| 35 | |||
| 36 | ### Environment Configuration | ||
| 37 | |||
| 38 | The application behavior can be controlled using the `BONSAI_TEST_MODE` environment variable: | ||
| 39 | |||
| 40 | - **Test Mode** (default for local development): `BONSAI_TEST_MODE=true` | ||
| 41 | - If Bonsai fails or is not available, dummy files are generated for testing | ||
| 42 | - Useful for development and testing the web interface | ||
| 43 | |||
| 44 | - **Production Mode**: `BONSAI_TEST_MODE=false` | ||
| 45 | - If Bonsai fails, proper error messages are returned to the user | ||
| 46 | - No dummy files are generated | ||
| 47 | |||
| 48 | ```bash | ||
| 49 | # Run in production mode | ||
| 50 | BONSAI_TEST_MODE=false python app.py | ||
| 51 | |||
| 52 | # Run in test mode (default) | ||
| 53 | BONSAI_TEST_MODE=true python app.py | ||
| 54 | # or simply | ||
| 55 | python app.py | ||
| 56 | ``` | ||
| 57 | |||
| 58 | ### Development Mode | ||
| 59 | |||
| 60 | For development, the application includes fallback dummy data generation when `BONSAI_TEST_MODE=true` and the actual Bonsai tool is not available. This allows you to test the web interface without having the full Bonsai installation. | ||
| 61 | |||
| 62 | **Error Handling**: When Bonsai fails in production mode, detailed error messages are displayed to the user in red text, including: | ||
| 63 | - Execution failures with return codes and stderr output | ||
| 64 | - Timeout errors (30-second limit) | ||
| 65 | - Missing Bonsai installation errors | ||
| 66 | |||
| 67 | ## Container Deployment | ||
| 68 | |||
| 69 | ### Building the Container | ||
| 70 | |||
| 71 | #### Local Build (for testing) | ||
| 72 | ```bash | ||
| 73 | ./build.sh | ||
| 74 | ``` | ||
| 75 | |||
| 76 | #### Production Build and Push | ||
| 77 | ```bash | ||
| 78 | PUSH=1 ./build.sh | ||
| 79 | ``` | ||
| 80 | |||
| 81 | The build script will: | ||
| 82 | - Always build the container image with appropriate tags | ||
| 83 | - Only push to `git.d464.sh/diogo464/bonsai-web` when `PUSH=1` is set | ||
| 84 | - Use git tags for versioning (or 'latest' if no tag) | ||
| 85 | - Check registry authentication only when pushing | ||
| 86 | |||
| 87 | #### Manual Build Commands | ||
| 88 | |||
| 89 | ```bash | ||
| 90 | # Local build | ||
| 91 | podman build -t bonsai-web -f Containerfile . | ||
| 92 | |||
| 93 | # Production build and push | ||
| 94 | podman build -t git.d464.sh/diogo464/bonsai-web:latest -f Containerfile . | ||
| 95 | podman push git.d464.sh/diogo464/bonsai-web:latest | ||
| 96 | ``` | ||
| 97 | |||
| 98 | ### Running the Container | ||
| 99 | |||
| 100 | #### From Local Build | ||
| 101 | ```bash | ||
| 102 | podman run -p 5000:5000 bonsai-web:local | ||
| 103 | ``` | ||
| 104 | |||
| 105 | #### From Registry | ||
| 106 | ```bash | ||
| 107 | podman run -p 5000:5000 git.d464.sh/diogo464/bonsai-web:latest | ||
| 108 | ``` | ||
| 109 | |||
| 110 | ### Registry Authentication | ||
| 111 | |||
| 112 | Before pushing to the registry, authenticate with: | ||
| 113 | ```bash | ||
| 114 | podman login git.d464.sh | ||
| 115 | ``` | ||
| 116 | |||
| 117 | ### Bonsai Integration | ||
| 118 | |||
| 119 | To integrate with the actual Bonsai tool, modify the `Containerfile` in the "BONSAI SETUP SECTION" to: | ||
| 120 | |||
| 121 | 1. Clone or copy the Bonsai source code to `/usr/src/bonsai` | ||
| 122 | 2. Install Bonsai's dependencies | ||
| 123 | 3. Ensure the Bonsai tool is properly configured | ||
| 124 | |||
| 125 | Example: | ||
| 126 | ```dockerfile | ||
| 127 | # In the BONSAI SETUP SECTION of Containerfile | ||
| 128 | RUN git clone https://github.com/your-org/bonsai.git /usr/src/bonsai | ||
| 129 | WORKDIR /usr/src/bonsai | ||
| 130 | RUN pip install -r requirements.txt | ||
| 131 | WORKDIR /app | ||
| 132 | ``` | ||
| 133 | |||
| 134 | ## Usage | ||
| 135 | |||
| 136 | 1. Open the web application in your browser | ||
| 137 | 2. Enter your network configuration in YAML format in the text area | ||
| 138 | 3. Click "Generate Network Files" | ||
| 139 | 4. View or download the generated `edges.csv` and `nodes.csv` files | ||
| 140 | |||
| 141 | ### Example Configuration | ||
| 142 | |||
| 143 | ```yaml | ||
| 144 | network: | ||
| 145 | nodes: 10 | ||
| 146 | density: 0.3 | ||
| 147 | latency_range: [1, 100] | ||
| 148 | ``` | ||
| 149 | |||
| 150 | ## API | ||
| 151 | |||
| 152 | ### POST / | ||
| 153 | |||
| 154 | Generates network files from a YAML configuration. | ||
| 155 | |||
| 156 | **Request Body:** | ||
| 157 | ```json | ||
| 158 | { | ||
| 159 | "config": "network:\n nodes: 10\n density: 0.3" | ||
| 160 | } | ||
| 161 | ``` | ||
| 162 | |||
| 163 | **Response:** | ||
| 164 | ```json | ||
| 165 | { | ||
| 166 | "edges.csv": "source,target,weight\nnode1,node2,0.5\n...", | ||
| 167 | "nodes.csv": "id,label,x,y\nnode1,Node 1,0,0\n..." | ||
| 168 | } | ||
| 169 | ``` | ||
| 170 | |||
| 171 | ## Project Structure | ||
| 172 | |||
| 173 | ``` | ||
| 174 | . | ||
| 175 | ├── app.py # Flask backend | ||
| 176 | ├── index.html # Main web page | ||
| 177 | ├── script.js # Client-side JavaScript | ||
| 178 | ├── logo.png # Bonsai logo | ||
| 179 | ├── requirements.txt # Python dependencies | ||
| 180 | ├── Containerfile # Container build instructions | ||
| 181 | ├── build.sh # Container build script | ||
| 182 | ├── .gitignore # Git ignore patterns | ||
| 183 | └── README.md # This file | ||
| 184 | ``` \ No newline at end of file | ||
| @@ -0,0 +1,154 @@ | |||
| 1 | import os | ||
| 2 | import subprocess | ||
| 3 | import tempfile | ||
| 4 | import json | ||
| 5 | from flask import Flask, request, jsonify, send_from_directory | ||
| 6 | |||
| 7 | app = Flask(__name__) | ||
| 8 | |||
| 9 | # Check if we're in test environment (defaults to True for local development) | ||
| 10 | TEST_ENVIRONMENT = os.getenv("BONSAI_TEST_MODE", "true").lower() == "true" | ||
| 11 | |||
| 12 | |||
| 13 | @app.route("/") | ||
| 14 | def index(): | ||
| 15 | """Serve the main HTML page.""" | ||
| 16 | return send_from_directory(".", "index.html") | ||
| 17 | |||
| 18 | |||
| 19 | @app.route("/logo.png") | ||
| 20 | def logo(): | ||
| 21 | """Serve the logo image.""" | ||
| 22 | return send_from_directory(".", "logo.png") | ||
| 23 | |||
| 24 | |||
| 25 | @app.route("/script.js") | ||
| 26 | def script(): | ||
| 27 | """Serve the JavaScript file.""" | ||
| 28 | return send_from_directory(".", "script.js") | ||
| 29 | |||
| 30 | |||
| 31 | @app.route("/", methods=["POST"]) | ||
| 32 | def generate_files(): | ||
| 33 | """Handle configuration submission and execute bonsai.""" | ||
| 34 | try: | ||
| 35 | # Get the configuration from the request | ||
| 36 | data = request.get_json() | ||
| 37 | if not data or "config" not in data: | ||
| 38 | return jsonify({"error": "No configuration provided"}), 400 | ||
| 39 | |||
| 40 | config_content = data["config"] | ||
| 41 | |||
| 42 | # Create temporary directory for output | ||
| 43 | with tempfile.TemporaryDirectory() as temp_dir: | ||
| 44 | # Write config to temporary file | ||
| 45 | config_file = os.path.join(temp_dir, "config.yaml") | ||
| 46 | with open(config_file, "w") as f: | ||
| 47 | f.write(config_content) | ||
| 48 | |||
| 49 | # Create output directory | ||
| 50 | output_dir = os.path.join(temp_dir, "output") | ||
| 51 | os.makedirs(output_dir, exist_ok=True) | ||
| 52 | |||
| 53 | # Execute bonsai command | ||
| 54 | cmd = [ | ||
| 55 | "python", | ||
| 56 | "-m", | ||
| 57 | "main", | ||
| 58 | "--net_config", | ||
| 59 | config_file, | ||
| 60 | "--output_dir", | ||
| 61 | output_dir, | ||
| 62 | ] | ||
| 63 | |||
| 64 | # Determine bonsai directory | ||
| 65 | bonsai_dir = "/usr/src/bonsai" if os.path.exists("/usr/src/bonsai") else "." | ||
| 66 | |||
| 67 | try: | ||
| 68 | result = subprocess.run( | ||
| 69 | cmd, cwd=bonsai_dir, capture_output=True, text=True, timeout=30 | ||
| 70 | ) | ||
| 71 | |||
| 72 | if result.returncode != 0: | ||
| 73 | error_msg = ( | ||
| 74 | f"Bonsai execution failed with return code {result.returncode}" | ||
| 75 | ) | ||
| 76 | if result.stderr: | ||
| 77 | error_msg += f": {result.stderr.strip()}" | ||
| 78 | |||
| 79 | # Only create dummy files in test environment | ||
| 80 | if TEST_ENVIRONMENT: | ||
| 81 | print( | ||
| 82 | f"WARNING: {error_msg}. Creating dummy files for testing." | ||
| 83 | ) | ||
| 84 | edges_content = ( | ||
| 85 | "source,target,weight\nnode1,node2,0.5\nnode2,node3,0.3\n" | ||
| 86 | ) | ||
| 87 | nodes_content = "id,label,x,y\nnode1,Node 1,0,0\nnode2,Node 2,1,1\nnode3,Node 3,2,0\n" | ||
| 88 | |||
| 89 | with open(os.path.join(output_dir, "edges.csv"), "w") as f: | ||
| 90 | f.write(edges_content) | ||
| 91 | with open(os.path.join(output_dir, "nodes.csv"), "w") as f: | ||
| 92 | f.write(nodes_content) | ||
| 93 | else: | ||
| 94 | return jsonify({"error": error_msg}), 500 | ||
| 95 | |||
| 96 | except subprocess.TimeoutExpired: | ||
| 97 | error_msg = "Bonsai execution timed out after 30 seconds" | ||
| 98 | if TEST_ENVIRONMENT: | ||
| 99 | print(f"WARNING: {error_msg}. Creating dummy files for testing.") | ||
| 100 | edges_content = ( | ||
| 101 | "source,target,weight\nnode1,node2,0.5\nnode2,node3,0.3\n" | ||
| 102 | ) | ||
| 103 | nodes_content = "id,label,x,y\nnode1,Node 1,0,0\nnode2,Node 2,1,1\nnode3,Node 3,2,0\n" | ||
| 104 | |||
| 105 | with open(os.path.join(output_dir, "edges.csv"), "w") as f: | ||
| 106 | f.write(edges_content) | ||
| 107 | with open(os.path.join(output_dir, "nodes.csv"), "w") as f: | ||
| 108 | f.write(nodes_content) | ||
| 109 | else: | ||
| 110 | return jsonify({"error": error_msg}), 500 | ||
| 111 | |||
| 112 | except FileNotFoundError: | ||
| 113 | error_msg = "Bonsai tool not found. Please ensure Bonsai is installed and available." | ||
| 114 | if TEST_ENVIRONMENT: | ||
| 115 | print(f"WARNING: {error_msg}. Creating dummy files for testing.") | ||
| 116 | edges_content = ( | ||
| 117 | "source,target,weight\nnode1,node2,0.5\nnode2,node3,0.3\n" | ||
| 118 | ) | ||
| 119 | nodes_content = "id,label,x,y\nnode1,Node 1,0,0\nnode2,Node 2,1,1\nnode3,Node 3,2,0\n" | ||
| 120 | |||
| 121 | with open(os.path.join(output_dir, "edges.csv"), "w") as f: | ||
| 122 | f.write(edges_content) | ||
| 123 | with open(os.path.join(output_dir, "nodes.csv"), "w") as f: | ||
| 124 | f.write(nodes_content) | ||
| 125 | else: | ||
| 126 | return jsonify({"error": error_msg}), 500 | ||
| 127 | |||
| 128 | # Read generated files and return their contents | ||
| 129 | files = {} | ||
| 130 | for filename in ["edges.csv", "nodes.csv"]: | ||
| 131 | filepath = os.path.join(output_dir, filename) | ||
| 132 | if os.path.exists(filepath): | ||
| 133 | with open(filepath, "r") as f: | ||
| 134 | files[filename] = f.read() | ||
| 135 | |||
| 136 | if not files: | ||
| 137 | return ( | ||
| 138 | jsonify({"error": "No output files were generated by Bonsai"}), | ||
| 139 | 500, | ||
| 140 | ) | ||
| 141 | |||
| 142 | # Files are automatically cleaned up when temp_dir context exits | ||
| 143 | return jsonify(files) | ||
| 144 | |||
| 145 | except Exception as e: | ||
| 146 | return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 | ||
| 147 | |||
| 148 | |||
| 149 | if __name__ == "__main__": | ||
| 150 | if TEST_ENVIRONMENT: | ||
| 151 | print("Running in TEST mode - dummy files will be generated if Bonsai fails") | ||
| 152 | else: | ||
| 153 | print("Running in PRODUCTION mode - errors will be returned if Bonsai fails") | ||
| 154 | app.run(debug=True, host="0.0.0.0", port=5000) | ||
diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..df3d28a --- /dev/null +++ b/build.sh | |||
| @@ -0,0 +1,146 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | |||
| 3 | # Container registry configuration | ||
| 4 | REGISTRY="git.d464.sh" | ||
| 5 | NAMESPACE="diogo464" | ||
| 6 | IMAGE_NAME="bonsai-web" | ||
| 7 | FULL_IMAGE_NAME="${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" | ||
| 8 | |||
| 9 | # Get version from git tag or use 'latest' | ||
| 10 | VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "latest") | ||
| 11 | |||
| 12 | # Check if we should push to registry | ||
| 13 | SHOULD_PUSH=${PUSH:-0} | ||
| 14 | |||
| 15 | # Colors for output | ||
| 16 | RED='\033[0;31m' | ||
| 17 | GREEN='\033[0;32m' | ||
| 18 | YELLOW='\033[1;33m' | ||
| 19 | BLUE='\033[0;34m' | ||
| 20 | NC='\033[0m' # No Color | ||
| 21 | |||
| 22 | # Logging functions | ||
| 23 | log_info() { | ||
| 24 | echo -e "${BLUE}[INFO]${NC} $1" | ||
| 25 | } | ||
| 26 | |||
| 27 | log_success() { | ||
| 28 | echo -e "${GREEN}[SUCCESS]${NC} $1" | ||
| 29 | } | ||
| 30 | |||
| 31 | log_warning() { | ||
| 32 | echo -e "${YELLOW}[WARNING]${NC} $1" | ||
| 33 | } | ||
| 34 | |||
| 35 | log_error() { | ||
| 36 | echo -e "${RED}[ERROR]${NC} $1" | ||
| 37 | } | ||
| 38 | |||
| 39 | # Error handler | ||
| 40 | handle_error() { | ||
| 41 | log_error "Script failed at line $1" | ||
| 42 | exit 1 | ||
| 43 | } | ||
| 44 | |||
| 45 | trap 'handle_error $LINENO' ERR | ||
| 46 | set -e | ||
| 47 | |||
| 48 | if [[ "$SHOULD_PUSH" == "1" ]]; then | ||
| 49 | log_info "Starting container build and push process..." | ||
| 50 | log_info "Registry: ${REGISTRY}" | ||
| 51 | log_info "Image: ${FULL_IMAGE_NAME}" | ||
| 52 | else | ||
| 53 | log_info "Starting container build process (local only)..." | ||
| 54 | log_info "Image: ${IMAGE_NAME}" | ||
| 55 | fi | ||
| 56 | log_info "Version: ${VERSION}" | ||
| 57 | |||
| 58 | # Check if podman is available | ||
| 59 | if ! command -v podman &> /dev/null; then | ||
| 60 | log_error "podman is not installed or not in PATH" | ||
| 61 | exit 1 | ||
| 62 | fi | ||
| 63 | |||
| 64 | # Check if we're in the right directory | ||
| 65 | if [[ ! -f "Containerfile" ]]; then | ||
| 66 | log_error "Containerfile not found. Please run this script from the project root." | ||
| 67 | exit 1 | ||
| 68 | fi | ||
| 69 | |||
| 70 | # Check if we're logged into the registry (only if pushing) | ||
| 71 | if [[ "$SHOULD_PUSH" == "1" ]]; then | ||
| 72 | log_info "Checking registry authentication..." | ||
| 73 | if ! podman login --get-login "${REGISTRY}" &> /dev/null; then | ||
| 74 | log_warning "Not logged into ${REGISTRY}. Please login first:" | ||
| 75 | echo "podman login ${REGISTRY}" | ||
| 76 | exit 1 | ||
| 77 | fi | ||
| 78 | fi | ||
| 79 | |||
| 80 | # Build the container image | ||
| 81 | log_info "Building container image..." | ||
| 82 | if [[ "$SHOULD_PUSH" == "1" ]]; then | ||
| 83 | # Build with registry tags for pushing | ||
| 84 | podman build \ | ||
| 85 | -f Containerfile \ | ||
| 86 | -t "${FULL_IMAGE_NAME}:${VERSION}" \ | ||
| 87 | -t "${FULL_IMAGE_NAME}:latest" \ | ||
| 88 | -t "${IMAGE_NAME}:local" \ | ||
| 89 | . | ||
| 90 | else | ||
| 91 | # Build with local tags only | ||
| 92 | podman build \ | ||
| 93 | -f Containerfile \ | ||
| 94 | -t "${IMAGE_NAME}:${VERSION}" \ | ||
| 95 | -t "${IMAGE_NAME}:latest" \ | ||
| 96 | -t "${IMAGE_NAME}:local" \ | ||
| 97 | . | ||
| 98 | fi | ||
| 99 | |||
| 100 | log_success "Container image built successfully" | ||
| 101 | |||
| 102 | if [[ "$SHOULD_PUSH" == "1" ]]; then | ||
| 103 | # Push the versioned tag | ||
| 104 | log_info "Pushing ${FULL_IMAGE_NAME}:${VERSION}..." | ||
| 105 | podman push "${FULL_IMAGE_NAME}:${VERSION}" | ||
| 106 | log_success "Pushed ${FULL_IMAGE_NAME}:${VERSION}" | ||
| 107 | |||
| 108 | # Push the latest tag (only if version is not 'latest') | ||
| 109 | if [[ "${VERSION}" != "latest" ]]; then | ||
| 110 | log_info "Pushing ${FULL_IMAGE_NAME}:latest..." | ||
| 111 | podman push "${FULL_IMAGE_NAME}:latest" | ||
| 112 | log_success "Pushed ${FULL_IMAGE_NAME}:latest" | ||
| 113 | fi | ||
| 114 | fi | ||
| 115 | |||
| 116 | # Display final information | ||
| 117 | echo | ||
| 118 | if [[ "$SHOULD_PUSH" == "1" ]]; then | ||
| 119 | log_success "Build and push completed successfully!" | ||
| 120 | echo | ||
| 121 | echo "Image details:" | ||
| 122 | echo " Registry: ${REGISTRY}" | ||
| 123 | echo " Full name: ${FULL_IMAGE_NAME}" | ||
| 124 | echo " Version: ${VERSION}" | ||
| 125 | echo | ||
| 126 | echo "To run the container:" | ||
| 127 | echo " podman run -p 5000:5000 ${FULL_IMAGE_NAME}:${VERSION}" | ||
| 128 | echo | ||
| 129 | echo "To pull on another machine:" | ||
| 130 | echo " podman pull ${FULL_IMAGE_NAME}:${VERSION}" | ||
| 131 | else | ||
| 132 | log_success "Local build completed!" | ||
| 133 | echo | ||
| 134 | echo "Image details:" | ||
| 135 | echo " Name: ${IMAGE_NAME}" | ||
| 136 | echo " Version: ${VERSION}" | ||
| 137 | echo | ||
| 138 | echo "To run the container:" | ||
| 139 | echo " podman run -p 5000:5000 ${IMAGE_NAME}:${VERSION}" | ||
| 140 | echo | ||
| 141 | echo "To test the container:" | ||
| 142 | echo " podman run --rm -p 5000:5000 ${IMAGE_NAME}:${VERSION}" | ||
| 143 | echo | ||
| 144 | echo "To push to registry:" | ||
| 145 | echo " PUSH=1 $0" | ||
| 146 | fi \ No newline at end of file | ||
diff --git a/index.html b/index.html new file mode 100644 index 0000000..fff715a --- /dev/null +++ b/index.html | |||
| @@ -0,0 +1,116 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | |||
| 4 | <head> | ||
| 5 | <meta charset="UTF-8"> | ||
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 7 | <title>Bonsai - Network Latency Graph Generator</title> | ||
| 8 | <link rel="stylesheet" href="https://unpkg.com/@picocss/[email protected]/css/pico.min.css"> | ||
| 9 | <style> | ||
| 10 | .logo-container { | ||
| 11 | display: flex; | ||
| 12 | align-items: center; | ||
| 13 | gap: 1rem; | ||
| 14 | margin-bottom: 2rem; | ||
| 15 | } | ||
| 16 | |||
| 17 | .logo { | ||
| 18 | height: 60px; | ||
| 19 | width: auto; | ||
| 20 | } | ||
| 21 | |||
| 22 | .file-item { | ||
| 23 | margin-bottom: 1rem; | ||
| 24 | padding: 1rem; | ||
| 25 | border: 1px solid var(--border-color); | ||
| 26 | border-radius: var(--border-radius); | ||
| 27 | } | ||
| 28 | |||
| 29 | .file-buttons { | ||
| 30 | display: flex; | ||
| 31 | gap: 0.5rem; | ||
| 32 | margin-top: 0.5rem; | ||
| 33 | } | ||
| 34 | |||
| 35 | .file-content { | ||
| 36 | margin-top: 1rem; | ||
| 37 | padding: 1rem; | ||
| 38 | background-color: var(--code-background-color); | ||
| 39 | border-radius: var(--border-radius); | ||
| 40 | white-space: pre; | ||
| 41 | font-family: monospace; | ||
| 42 | font-size: 0.9rem; | ||
| 43 | max-height: 300px; | ||
| 44 | overflow-y: auto; | ||
| 45 | display: none; | ||
| 46 | } | ||
| 47 | |||
| 48 | .error { | ||
| 49 | color: var(--color-red); | ||
| 50 | background-color: var(--color-red-background); | ||
| 51 | padding: 1rem; | ||
| 52 | border-radius: var(--border-radius); | ||
| 53 | margin-top: 1rem; | ||
| 54 | } | ||
| 55 | |||
| 56 | .loading { | ||
| 57 | text-align: center; | ||
| 58 | margin-top: 1rem; | ||
| 59 | } | ||
| 60 | </style> | ||
| 61 | </head> | ||
| 62 | |||
| 63 | <body> | ||
| 64 | <main class="container"> | ||
| 65 | <header> | ||
| 66 | <div class="logo-container"> | ||
| 67 | <img src="logo.png" alt="Bonsai Logo" class="logo"> | ||
| 68 | <h1>Bonsai</h1> | ||
| 69 | </div> | ||
| 70 | </header> | ||
| 71 | |||
| 72 | <section> | ||
| 73 | <p> | ||
| 74 | Bonsai is a powerful tool for generating realistic network latency graphs. | ||
| 75 | This web interface provides an easy way to use the | ||
| 76 | <a href="https://codelab.fct.unl.pt/di/computer-systems/bonsai" target="_blank">Bonsai project</a> | ||
| 77 | without needing to install its dependencies locally. | ||
| 78 | Simply provide your network configuration in YAML format below, and click | ||
| 79 | "Generate" to create your network topology files. | ||
| 80 | </p> | ||
| 81 | |||
| 82 | <p> | ||
| 83 | The tool will generate two CSV files: <code>edges.csv</code> containing | ||
| 84 | the network connections and weights, and <code>nodes.csv</code> containing | ||
| 85 | the node information and positions. | ||
| 86 | </p> | ||
| 87 | </section> | ||
| 88 | |||
| 89 | <section> | ||
| 90 | <label for="config">Network Configuration (YAML):</label> | ||
| 91 | <textarea id="config" name="config" rows="10">nodes: 15 | ||
| 92 | continents: | ||
| 93 | EU: 5 | ||
| 94 | AS: 5 | ||
| 95 | countries: | ||
| 96 | DE: 2 | ||
| 97 | FR: 2 | ||
| 98 | CN: 2 | ||
| 99 | JP: 2</textarea> | ||
| 100 | |||
| 101 | <button id="generateBtn" onclick="generateFiles()">Generate Network Files</button> | ||
| 102 | |||
| 103 | <div id="loading" class="loading" style="display: none;"> | ||
| 104 | <p aria-busy="true">Generating network files...</p> | ||
| 105 | </div> | ||
| 106 | |||
| 107 | <div id="error" class="error" style="display: none;"></div> | ||
| 108 | |||
| 109 | <div id="results" style="margin-top: 2rem;"></div> | ||
| 110 | </section> | ||
| 111 | </main> | ||
| 112 | |||
| 113 | <script src="script.js"></script> | ||
| 114 | </body> | ||
| 115 | |||
| 116 | </html> \ No newline at end of file | ||
diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..fb6e55d --- /dev/null +++ b/logo.png | |||
| Binary files differ | |||
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c4988fb --- /dev/null +++ b/requirements.txt | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | Flask==2.3.3 | ||
| 2 | Werkzeug==2.3.7 \ No newline at end of file | ||
diff --git a/script.js b/script.js new file mode 100644 index 0000000..5bc60b0 --- /dev/null +++ b/script.js | |||
| @@ -0,0 +1,156 @@ | |||
| 1 | let generatedFiles = {}; | ||
| 2 | |||
| 3 | async function generateFiles() { | ||
| 4 | const configTextarea = document.getElementById('config'); | ||
| 5 | const generateBtn = document.getElementById('generateBtn'); | ||
| 6 | const loadingDiv = document.getElementById('loading'); | ||
| 7 | const errorDiv = document.getElementById('error'); | ||
| 8 | const resultsDiv = document.getElementById('results'); | ||
| 9 | |||
| 10 | const config = configTextarea.value.trim(); | ||
| 11 | |||
| 12 | if (!config) { | ||
| 13 | showError('Please enter a configuration before generating files.'); | ||
| 14 | return; | ||
| 15 | } | ||
| 16 | |||
| 17 | // Reset UI state | ||
| 18 | hideError(); | ||
| 19 | hideResults(); | ||
| 20 | showLoading(); | ||
| 21 | generateBtn.disabled = true; | ||
| 22 | |||
| 23 | try { | ||
| 24 | const response = await fetch('/', { | ||
| 25 | method: 'POST', | ||
| 26 | headers: { | ||
| 27 | 'Content-Type': 'application/json', | ||
| 28 | }, | ||
| 29 | body: JSON.stringify({ config: config }) | ||
| 30 | }); | ||
| 31 | |||
| 32 | const data = await response.json(); | ||
| 33 | |||
| 34 | if (!response.ok) { | ||
| 35 | throw new Error(data.error || 'Failed to generate files'); | ||
| 36 | } | ||
| 37 | |||
| 38 | // Store the generated files | ||
| 39 | generatedFiles = data; | ||
| 40 | |||
| 41 | // Display the results | ||
| 42 | displayResults(data); | ||
| 43 | |||
| 44 | } catch (error) { | ||
| 45 | console.error('Error generating files:', error); | ||
| 46 | showError('Error generating files: ' + error.message); | ||
| 47 | } finally { | ||
| 48 | hideLoading(); | ||
| 49 | generateBtn.disabled = false; | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | function displayResults(files) { | ||
| 54 | const resultsDiv = document.getElementById('results'); | ||
| 55 | |||
| 56 | if (Object.keys(files).length === 0) { | ||
| 57 | showError('No files were generated.'); | ||
| 58 | return; | ||
| 59 | } | ||
| 60 | |||
| 61 | let html = '<h3>Generated Files</h3>'; | ||
| 62 | |||
| 63 | for (const [filename, content] of Object.entries(files)) { | ||
| 64 | html += ` | ||
| 65 | <div class="file-item"> | ||
| 66 | <h4>${filename}</h4> | ||
| 67 | <p>Size: ${content.length} characters</p> | ||
| 68 | <div class="file-buttons"> | ||
| 69 | <button onclick="toggleFileContent('${filename}')" id="view-${filename}"> | ||
| 70 | View Content | ||
| 71 | </button> | ||
| 72 | <button onclick="downloadFile('${filename}')" class="secondary"> | ||
| 73 | Download | ||
| 74 | </button> | ||
| 75 | </div> | ||
| 76 | <div id="content-${filename}" class="file-content">${escapeHtml(content)}</div> | ||
| 77 | </div> | ||
| 78 | `; | ||
| 79 | } | ||
| 80 | |||
| 81 | resultsDiv.innerHTML = html; | ||
| 82 | resultsDiv.style.display = 'block'; | ||
| 83 | } | ||
| 84 | |||
| 85 | function toggleFileContent(filename) { | ||
| 86 | const contentDiv = document.getElementById(`content-${filename}`); | ||
| 87 | const viewBtn = document.getElementById(`view-${filename}`); | ||
| 88 | |||
| 89 | if (contentDiv.style.display === 'none' || contentDiv.style.display === '') { | ||
| 90 | contentDiv.style.display = 'block'; | ||
| 91 | viewBtn.textContent = 'Hide Content'; | ||
| 92 | } else { | ||
| 93 | contentDiv.style.display = 'none'; | ||
| 94 | viewBtn.textContent = 'View Content'; | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | function downloadFile(filename) { | ||
| 99 | if (!generatedFiles[filename]) { | ||
| 100 | showError('File not found: ' + filename); | ||
| 101 | return; | ||
| 102 | } | ||
| 103 | |||
| 104 | const content = generatedFiles[filename]; | ||
| 105 | const blob = new Blob([content], { type: 'text/csv' }); | ||
| 106 | const url = window.URL.createObjectURL(blob); | ||
| 107 | |||
| 108 | const a = document.createElement('a'); | ||
| 109 | a.href = url; | ||
| 110 | a.download = filename; | ||
| 111 | document.body.appendChild(a); | ||
| 112 | a.click(); | ||
| 113 | |||
| 114 | // Clean up | ||
| 115 | window.URL.revokeObjectURL(url); | ||
| 116 | document.body.removeChild(a); | ||
| 117 | } | ||
| 118 | |||
| 119 | function showError(message) { | ||
| 120 | const errorDiv = document.getElementById('error'); | ||
| 121 | errorDiv.textContent = message; | ||
| 122 | errorDiv.style.display = 'block'; | ||
| 123 | } | ||
| 124 | |||
| 125 | function hideError() { | ||
| 126 | const errorDiv = document.getElementById('error'); | ||
| 127 | errorDiv.style.display = 'none'; | ||
| 128 | } | ||
| 129 | |||
| 130 | function showLoading() { | ||
| 131 | const loadingDiv = document.getElementById('loading'); | ||
| 132 | loadingDiv.style.display = 'block'; | ||
| 133 | } | ||
| 134 | |||
| 135 | function hideLoading() { | ||
| 136 | const loadingDiv = document.getElementById('loading'); | ||
| 137 | loadingDiv.style.display = 'none'; | ||
| 138 | } | ||
| 139 | |||
| 140 | function hideResults() { | ||
| 141 | const resultsDiv = document.getElementById('results'); | ||
| 142 | resultsDiv.style.display = 'none'; | ||
| 143 | } | ||
| 144 | |||
| 145 | function escapeHtml(text) { | ||
| 146 | const div = document.createElement('div'); | ||
| 147 | div.textContent = text; | ||
| 148 | return div.innerHTML; | ||
| 149 | } | ||
| 150 | |||
| 151 | // Add Enter key support for the textarea (Ctrl+Enter to generate) | ||
| 152 | document.getElementById('config').addEventListener('keydown', function (event) { | ||
| 153 | if (event.ctrlKey && event.key === 'Enter') { | ||
| 154 | generateFiles(); | ||
| 155 | } | ||
| 156 | }); \ No newline at end of file | ||
