update example

master
a2nr 2026-01-06 21:38:54 +07:00
parent 6535670cd3
commit fee6fdec2b
11 changed files with 449 additions and 28 deletions

View File

@ -1,8 +1,11 @@
FROM python:3.11-slim
# Install gcc compiler for C code compilation
# Create a non-root user for security
RUN useradd --create-home --shell /bin/bash app
# Install gcc compiler for C code compilation and other production dependencies
RUN apt-get update && \
apt-get install -y gcc build-essential && \
apt-get install -y gcc build-essential curl && \
rm -rf /var/lib/apt/lists/*
# Set working directory
@ -15,8 +18,12 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Change ownership to the app user
RUN chown -R app:app /app
USER app
# Expose port 5000
EXPOSE 5000
# Run the application with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "1", "app:app"]
# Run the application with Gunicorn in production mode using config file
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"]

185
README.md
View File

@ -70,6 +70,38 @@ The system uses Markdown files for content management. There are two main types
The content directory is located at the parent directory level, outside of the `elemes/` directory. This allows for easy linking and sharing of content between different instances of the LMS.
### Environment Configuration
The application uses environment variables for configuration. Create a `.env` file at the parent directory level (same level as the `elemes/` directory) to configure the application. The `podman-compose.yml` file is configured to read environment variables from `../.env`.
Example `.env` file:
```
# Production Environment Configuration
# Flask Configuration
FLASK_ENV=production
FLASK_DEBUG=0
SECRET_KEY=your-super-secret-key-here-change-this-in-production
# Application Configuration
CONTENT_DIR=content
STATIC_DIR=static
TEMPLATES_DIR=templates
TOKENS_FILE=tokens.csv
# Server Configuration
HOST=0.0.0.0
PORT=5000
# Security Configuration
MAX_CONTENT_LENGTH=1024 * 1024 # 1MB max upload size
WTF_CSRF_ENABLED=true
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=/var/log/lms-c/app.log
```
### Home Page (`home.md`)
The home page serves as the main landing page and lesson directory. Here's the template structure:
@ -392,13 +424,121 @@ This LMS can be used as a submodule in another repository. To set it up:
3. Add your lesson files to the content directory
4. Run the LMS from the elemes directory:
4. Create an environment configuration file at the root level:
```bash
touch .env
```
5. Run the LMS from the elemes directory:
```bash
cd elemes
podman-compose -f podman-compose.yml up --build
```
The content directory at the root level will be automatically mounted to the application container.
The content directory and environment configuration at the root level will be automatically mounted to the application container.
## Production Deployment
For production deployment, additional considerations and configurations are required to ensure security, performance, and reliability.
### Production Environment Setup
1. **Environment Variables**: Use the following environment variables for production:
- `FLASK_ENV=production` - Sets the environment to production
- `FLASK_DEBUG=0` - Disables debug mode
- `PORT=5000` - Specifies the port to run the application on
2. **Security Considerations**:
- Run the container as a non-root user (already configured in Dockerfile)
- Limit resource usage with podman/docker resource constraints
- Use a reverse proxy (like Nginx) in front of the application
- Implement proper HTTPS with SSL/TLS certificates
3. **Performance Optimizations**:
- Use the multi-worker Gunicorn configuration (already configured)
- Set appropriate worker processes based on your server's CPU cores
- Configure proper caching mechanisms if needed
### Production Deployment with Podman
To deploy in a production environment:
1. Build the production image:
```bash
podman build -t lms-c:prod .
```
2. Run with production settings:
```bash
podman run -d \
--name lms-c-prod \
-p 80:5000 \
-v /path/to/production/content:/app/content \
-v /path/to/production/static:/app/static \
-v /path/to/production/tokens.csv:/app/tokens.csv \
--restart=unless-stopped \
--memory=512m \
--cpus=1 \
lms-c:prod
```
### Production Deployment with Podman Compose
Create a production-specific compose file (`podman-compose.prod.yml`):
```yaml
version: '3.8'
services:
lms-c:
build: .
ports:
- "80:5000"
volumes:
- /path/to/production/content:/app/content
- /path/to/production/static:/app/static
- /path/to/production/templates:/app/templates
- /path/to/production/tokens.csv:/app/tokens.csv
env_file:
- /path/to/production/.env
restart: unless-stopped
mem_limit: 512m
cpus: 1.0
```
Run with:
```bash
podman-compose -f podman-compose.prod.yml up -d --build
```
### Reverse Proxy Configuration
For production, it's recommended to put a reverse proxy like Nginx in front of the application:
Example Nginx configuration:
```
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### SSL/HTTPS Setup
For HTTPS, you can use Let's Encrypt with Certbot:
```bash
sudo certbot --nginx -d your-domain.com
```
This will automatically configure SSL certificates for your domain.
## Security Considerations
@ -416,8 +556,12 @@ The content directory at the root level will be automatically mounted to the app
- `tokens.csv`: Student progress tracking file
- `generate_tokens.py`: Script to generate tokens CSV file
- `Dockerfile`: Container configuration
- `podman-compose.yml`: Podman Compose configuration
- `podman-compose.yml`: Podman Compose configuration with env_file support
- `requirements.txt`: Python dependencies
- `test/`: Directory containing test scripts and load testing tools
- `test/test_production.sh`: Production environment test script
- `test/locustfile.py`: Load testing script using Locust
- `test/podman-compose.locust.yml`: Podman Compose configuration for load testing
## Development
@ -427,12 +571,27 @@ To access the running container for development:
podman exec -it <container_name> /bin/bash
```
## Testing
The project includes comprehensive test scripts:
1. **Production Environment Tests**: Run the production test script to verify all functionality:
```bash
./test/test_production.sh
```
2. **Load Testing**: Use the included Locust configuration for load testing:
```bash
podman-compose -f test/podman-compose.locust.yml up --build
```
## Troubleshooting
- If you get permission errors, make sure your user has access to Podman
- If the application doesn't start, check that port 5000 is available
- If code compilation fails, verify that the gcc compiler is available in the container
- If progress tracking isn't working, ensure the tokens.csv file is properly formatted
- If environment variables aren't being loaded, verify that the .env file is in the correct location and properly formatted
## Stopping the Application
@ -562,11 +721,11 @@ The system includes support for load testing using Locust to simulate multiple c
4. **Alternative: Run Locust Directly**
- Install Locust on the load testing machine:
```bash
pip install -r locust/requirements.txt
pip install -r test/requirements.txt
```
- Run Locust directly:
```bash
cd locust
cd test
locust -f locustfile.py --host http://<LMS_SERVER_IP>:5000
```
@ -587,3 +746,19 @@ Monitor the LMS server's resource usage (CPU, memory, disk I/O) during load test
- Compilation performance under load
- Database performance (if one is added in the future)
- Container resource limits
The included `locustfile.py` simulates the following user behaviors:
- Browsing the home page
- Viewing lessons
- Compiling C code
- Validating student tokens
- Logging in with tokens
- Tracking student progress
### Monitoring Performance
Monitor the LMS server's resource usage (CPU, memory, disk I/O) during load testing to identify potential bottlenecks. Pay attention to:
- Response times for API requests
- Compilation performance under load
- Database performance (if one is added in the future)
- Container resource limits

View File

@ -18,7 +18,7 @@ else
podman exec -it lms-c-container /bin/bash
else
echo "Building and starting container..."
podman build -t lms-c . && podman run -d -p 5000:5000 --name lms-c-container -v ../content:/app/content -v ./static:/app/static -v ./templates:/app/templates -v ./tokens.csv:/app/tokens.csv lms-c
podman build -t lms-c . && podman run -d -p 5000:5000 --name lms-c-container -v ../content:/app/content -v ./static:/app/static -v ./templates:/app/templates -v ./tokens.csv:/app/tokens.csv -e FLASK_ENV=production -e FLASK_DEBUG=0 lms-c
sleep 5 # Wait for the application to start
podman exec -it lms-c-container /bin/bash
fi

29
app.py
View File

@ -9,6 +9,7 @@ import subprocess
import tempfile
import markdown
from flask import Flask, render_template, request, jsonify, send_from_directory
from flask_talisman import Talisman
import glob
import csv
import uuid
@ -16,11 +17,29 @@ from datetime import datetime
app = Flask(__name__)
# Configuration
CONTENT_DIR = 'content'
STATIC_DIR = 'static'
TEMPLATES_DIR = 'templates'
TOKENS_FILE = 'tokens.csv'
# Load configuration from environment variables with defaults
CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content')
STATIC_DIR = os.environ.get('STATIC_DIR', 'static')
TEMPLATES_DIR = os.environ.get('TEMPLATES_DIR', 'templates')
TOKENS_FILE = os.environ.get('TOKENS_FILE', 'tokens.csv')
# Security configuration using Talisman
Talisman(app,
force_https=False, # Set to True if using SSL
strict_transport_security=True,
strict_transport_security_max_age=31536000,
frame_options='DENY',
content_security_policy={
'default-src': "'self'",
'script-src': "'self' 'unsafe-inline' https://cdnjs.cloudflare.com",
'style-src': "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
'img-src': "'self' data: https:",
'font-src': "'self' https://cdn.jsdelivr.net",
'connect-src': "'self'",
'frame-ancestors': "'none'",
},
referrer_policy='strict-origin-when-cross-origin'
)
def get_lessons():
"""Get all lesson files from the content directory"""

38
example.env.production Normal file
View File

@ -0,0 +1,38 @@
# Production Environment Configuration
#
# tailscale Configuration
ELEMES_HOST=change-to-lms-topic
TS_AUTHKEY=your-super-secret-key-here-change-this-in-production
# Flask Configuration
FLASK_ENV=production
FLASK_DEBUG=0
SECRET_KEY=your-super-secret-key-here-change-this-in-production
# Application Configuration
CONTENT_DIR=content
STATIC_DIR=static
TEMPLATES_DIR=templates
TOKENS_FILE=tokens.csv
# Server Configuration
HOST=0.0.0.0
PORT=5000
# Security Configuration
MAX_CONTENT_LENGTH=1024 * 1024 # 1MB max upload size
WTF_CSRF_ENABLED=true
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=/var/log/lms-c/app.log
# Database Configuration (if using one in future)
# DATABASE_URL=postgresql://user:password@localhost/dbname
# External Services (if applicable)
# SMTP_SERVER=smtp.example.com
# SMTP_PORT=587
# SMTP_USERNAME=user@example.com
# SMTP_PASSWORD=password

View File

@ -0,0 +1,19 @@
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": {
"Proxy": "http://elemes:5000"
}
}
}
},
"AllowFunnel": {
"${TS_CERT_DOMAIN}:443": true
}
}

38
gunicorn.conf.py Normal file
View File

@ -0,0 +1,38 @@
# Gunicorn configuration file for production
# Server socket
bind = "0.0.0.0:5000"
backlog = 2048
# Worker processes
workers = 4
worker_class = "sync"
worker_connections = 1000
timeout = 120
keepalive = 5
max_requests = 1000
max_requests_jitter = 100
preload_app = True
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = 'lms-c'
# Server mechanics
user = 'app'
group = 'app'
tmp_upload_dir = None
# SSL (uncomment and configure if using SSL)
# keyfile = '/path/to/keyfile'
# certfile = '/path/to/certfile'

View File

@ -1,8 +1,9 @@
version: '3.8'
services:
lms-c:
elemes:
build: .
container_name: elemes
ports:
- "5000:5000"
volumes:
@ -10,6 +11,28 @@ services:
- ./static:/app/static
- ./templates:/app/templates
- ../tokens_siswa.csv:/app/tokens.csv
env_file:
- ../.env
command: gunicorn --config gunicorn.conf.py app:app
elemes-ts:
image: docker.io/tailscale/tailscale:latest
container_name: elemes-ts
hostname: ${ELEMES_HOST}
environment:
- FLASK_ENV=development
command: python app.py
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_SERVE_CONFIG=/config/sinau-c-tail.json
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=true
volumes:
- ./state:/var/lib/tailscale
- ../config:/config
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
networks:
main_network:
drive: bridge
network_mode: service:elemes-ts

View File

@ -1,3 +1,4 @@
Flask==2.3.3
markdown==3.5
gunicorn==21.2.0
flask-talisman==1.1.0

View File

@ -5,15 +5,15 @@
echo "Starting C Programming Learning Management System..."
# Check if container is already running
if [ "$(podman ps -q -f name=lms-c-container)" ]; then
if [ "$(podman ps -q -f name=elemes-container)" ]; then
echo "Container is already running. Access the application at http://localhost:5000"
exit 0
fi
# Check if container exists but is stopped
if [ "$(podman ps -aq -f name=lms-c-container)" ]; then
if [ "$(podman ps -aq -f name=elemes-container)" ]; then
echo "Starting existing container..."
podman start lms-c-container
podman start elemes-container
else
# Build and run the container
echo "Building and starting container..."

101
test/test_production.sh Executable file
View File

@ -0,0 +1,101 @@
#!/bin/bash
# Test script for LMS-C Production Environment
echo "Starting LMS-C Production Environment Tests..."
# Start the application
echo "Starting the application..."
cd ..
./start.sh
cd test
# Wait for the application to start
echo "Waiting for application to start..."
sleep 10
# Test 1: Check if the home page loads and displays lessons
echo "Test 1: Checking home page and lesson display..."
HOME_PAGE_CHECK=$(curl -s http://localhost:5000/ | grep -i "Available Lessons" | wc -l)
if [ $HOME_PAGE_CHECK -gt 0 ]; then
echo "✓ Test 1 PASSED: Home page displays lessons correctly"
else
echo "✗ Test 1 FAILED: Home page does not display lessons"
fi
# Test 2: Check if specific lesson pages load
echo "Test 2: Checking lesson page loading..."
LESSON_PAGE_CHECK=$(curl -s http://localhost:5000/lesson/welcome.md | grep -i "C Programming Learning System" | wc -l)
if [ $LESSON_PAGE_CHECK -gt 0 ]; then
echo "✓ Test 2 PASSED: Lesson pages load correctly"
else
echo "✗ Test 2 FAILED: Lesson pages do not load"
fi
# Test 3: Test the code compilation API
echo "Test 3: Testing code compilation API..."
COMPILATION_TEST=$(curl -s -X POST http://localhost:5000/compile \
-H "Content-Type: application/json" \
-d '{"code":"#include <stdio.h>\n\nint main() {\n printf(\"Hello, World!\\n\");\n return 0;\n}"}')
# Check if the response contains success: true and the expected output
if echo "$COMPILATION_TEST" | grep -q '"success":true' && echo "$COMPILATION_TEST" | grep -q "Hello, World!"; then
echo "✓ Test 3 PASSED: Code compilation API works correctly"
else
echo "✗ Test 3 FAILED: Code compilation API not working"
echo " Response: $COMPILATION_TEST"
fi
# Test 4: Test with a more complex C program
echo "Test 4: Testing complex code compilation..."
COMPLEX_CODE='{
"code": "#include <stdio.h>\nint main() {\n int i, sum = 0;\n for (i = 1; i <= 5; i++) {\n sum += i;\n }\n printf(\"Sum of 1 to 5 is: %d\\n\", sum);\n return 0;\n}"
}'
COMPLEX_TEST=$(curl -s -X POST http://localhost:5000/compile \
-H "Content-Type: application/json" \
-d "$COMPLEX_CODE")
if echo "$COMPLEX_TEST" | grep -q '"success":true' && echo "$COMPLEX_TEST" | grep -q "Sum of 1 to 5"; then
echo "✓ Test 4 PASSED: Complex code compilation works correctly"
else
echo "✗ Test 4 FAILED: Complex code compilation not working"
echo " Response: $COMPLEX_TEST"
fi
# Test 5: Check if all expected lesson files are accessible
echo "Test 5: Checking lesson accessibility..."
LESSON_FILES=("welcome.md" "hello_world.md" "introduction_to_c.md" "arrays.md")
ALL_LESSONS_ACCESSIBLE=true
for lesson in "${LESSON_FILES[@]}"; do
lesson_check=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:5000/lesson/$lesson")
if [ "$lesson_check" -ne 200 ]; then
echo "✗ Lesson $lesson returned HTTP $lesson_check"
ALL_LESSONS_ACCESSIBLE=false
fi
done
if [ "$ALL_LESSONS_ACCESSIBLE" = true ]; then
echo "✓ Test 5 PASSED: All test lessons are accessible"
else
echo "✗ Test 5 FAILED: Some lessons are not accessible"
fi
# Test 6: Check for errors in container logs
echo "Test 6: Checking container logs for errors..."
LOG_ERRORS=$(podman logs elemes_lms-c_1 2>&1 | grep -i error | wc -l)
if [ $LOG_ERRORS -eq 0 ]; then
echo "✓ Test 6 PASSED: No errors found in container logs"
else
echo "✗ Test 6 FAILED: Errors found in container logs"
podman logs elemes_lms-c_1 | grep -i error
fi
# Stop the application
echo "Stopping the application..."
cd ..
./stop.sh
cd test
echo "All tests completed!"