pre api key
This commit is contained in:
commit
9569f90aba
8 changed files with 900 additions and 0 deletions
62
.dockerignore
Normal file
62
.dockerignore
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
examples.md
|
||||||
|
|
||||||
|
# Container files
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
run_container.sh
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Runtime data (exclude existing input data from image)
|
||||||
|
input/
|
153
.gitignore
vendored
Normal file
153
.gitignore
vendored
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Application specific
|
||||||
|
input/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Container runtime
|
||||||
|
.container_id
|
39
Dockerfile
Normal file
39
Dockerfile
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
--no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY main.py .
|
||||||
|
|
||||||
|
# Create input directory and set permissions
|
||||||
|
RUN mkdir -p /app/input && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
6
README.md
Normal file
6
README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
- API Auth Bearer Management
|
||||||
|
- Access Logs
|
191
examples.md
Normal file
191
examples.md
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
# Simple Data Collector API - Usage Examples
|
||||||
|
|
||||||
|
## Starting the Server
|
||||||
|
|
||||||
|
### Using Virtual Environment (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create virtual environment (if not already created)
|
||||||
|
python -m venv venv
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
# On Linux/Mac:
|
||||||
|
source venv/bin/activate
|
||||||
|
# On Windows:
|
||||||
|
# venv\Scripts\activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on `http://localhost:8000`
|
||||||
|
|
||||||
|
## Simple Data Collection
|
||||||
|
|
||||||
|
### Basic Usage - curl (with Bearer Authentication)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send simple comma-separated data to run1 (requires input token)
|
||||||
|
curl -X POST http://localhost:8000/api/run1/ \
|
||||||
|
-H "Authorization: Bearer input_token_123" \
|
||||||
|
-d "host_a,is ok"
|
||||||
|
|
||||||
|
# Send data to different runs
|
||||||
|
curl -X POST http://localhost:8000/api/run1/ \
|
||||||
|
-H "Authorization: Bearer input_token_123" \
|
||||||
|
-d "host_b,is ok"
|
||||||
|
curl -X POST http://localhost:8000/api/run1/ \
|
||||||
|
-H "Authorization: Bearer input_token_123" \
|
||||||
|
-d "host_c,failed"
|
||||||
|
curl -X POST http://localhost:8000/api/run2/ \
|
||||||
|
-H "Authorization: Bearer input_token_123" \
|
||||||
|
-d "server_x,healthy"
|
||||||
|
```
|
||||||
|
|
||||||
|
### PowerShell Examples (with Bearer Authentication)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Send OK status from PowerShell (requires input token)
|
||||||
|
$hostname = $env:COMPUTERNAME
|
||||||
|
$status = "is ok"
|
||||||
|
$headers = @{ "Authorization" = "Bearer input_token_123" }
|
||||||
|
Invoke-RestMethod -Uri "http://your-server:8000/api/run1/" -Method Post -Body "$hostname,$status" -Headers $headers
|
||||||
|
|
||||||
|
# Send error status
|
||||||
|
$hostname = $env:COMPUTERNAME
|
||||||
|
$status = "failed - disk full"
|
||||||
|
$headers = @{ "Authorization" = "Bearer input_token_123" }
|
||||||
|
Invoke-RestMethod -Uri "http://your-server:8000/api/run1/" -Method Post -Body "$hostname,$status" -Headers $headers
|
||||||
|
```
|
||||||
|
|
||||||
|
### wget Examples (with Bearer Authentication)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send data using wget (requires input token)
|
||||||
|
wget --post-data="host_d,is ok" \
|
||||||
|
--header="Authorization: Bearer input_token_123" \
|
||||||
|
http://localhost:8000/api/run1/
|
||||||
|
wget --post-data="host_e,network error" \
|
||||||
|
--header="Authorization: Bearer input_token_123" \
|
||||||
|
http://localhost:8000/api/run1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Endpoints (with Bearer Authentication)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Different runs create separate directories and files (requires input token)
|
||||||
|
curl -X POST http://localhost:8000/api/daily-check/ \
|
||||||
|
-H "Authorization: Bearer input_token_123" \
|
||||||
|
-d "server1,ok"
|
||||||
|
curl -X POST http://localhost:8000/api/backup-status/ \
|
||||||
|
-H "Authorization: Bearer input_token_123" \
|
||||||
|
-d "server1,completed"
|
||||||
|
curl -X POST http://localhost:8000/api/security-scan/ \
|
||||||
|
-H "Authorization: Bearer input_token_123" \
|
||||||
|
-d "server1,clean"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retrieving Results
|
||||||
|
|
||||||
|
### View all results for a run (with Bearer Authentication)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all results for run1 (requires read token)
|
||||||
|
curl -H "Authorization: Bearer read_token_456" \
|
||||||
|
http://localhost:8000/results/run1/
|
||||||
|
|
||||||
|
# Get results for other runs
|
||||||
|
curl -H "Authorization: Bearer read_token_456" \
|
||||||
|
http://localhost:8000/results/daily-check/
|
||||||
|
curl -H "Authorization: Bearer read_token_456" \
|
||||||
|
http://localhost:8000/results/backup-status/
|
||||||
|
```
|
||||||
|
|
||||||
|
### PowerShell - Get results (with Bearer Authentication)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Get results from PowerShell (requires read token)
|
||||||
|
$headers = @{ "Authorization" = "Bearer read_token_456" }
|
||||||
|
Invoke-RestMethod -Uri "http://your-server:8000/results/run1/" -Method Get -Headers $headers
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Output
|
||||||
|
|
||||||
|
Data is saved to text files with timestamps:
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
input/
|
||||||
|
├── run1/
|
||||||
|
│ └── results.txt
|
||||||
|
├── daily-check/
|
||||||
|
│ └── results.txt
|
||||||
|
└── backup-status/
|
||||||
|
└── results.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example File Content (input/run1/results.txt)
|
||||||
|
```
|
||||||
|
2025-01-06 10:30:15 - host_a,is ok
|
||||||
|
2025-01-06 10:30:22 - host_b,is ok
|
||||||
|
2025-01-06 10:30:45 - host_c,failed
|
||||||
|
2025-01-06 10:31:02 - host_d,is ok
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-World Intune PowerShell Example (with Bearer Authentication)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Place this in your Intune PowerShell script
|
||||||
|
try {
|
||||||
|
# Your script logic here
|
||||||
|
Write-Host "Script executed successfully"
|
||||||
|
|
||||||
|
# Send success status (requires input token)
|
||||||
|
$hostname = $env:COMPUTERNAME
|
||||||
|
$headers = @{ "Authorization" = "Bearer input_token_123" }
|
||||||
|
Invoke-RestMethod -Uri "http://your-api-server:8000/api/intune-deployment/" -Method Post -Body "$hostname,success" -Headers $headers
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
# Send failure status with error
|
||||||
|
$hostname = $env:COMPUTERNAME
|
||||||
|
$error = $_.Exception.Message
|
||||||
|
$headers = @{ "Authorization" = "Bearer input_token_123" }
|
||||||
|
Invoke-RestMethod -Uri "http://your-api-server:8000/api/intune-deployment/" -Method Post -Body "$hostname,failed - $error" -Headers $headers
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
Visit `http://localhost:8000/docs` for interactive API documentation.
|
||||||
|
|
||||||
|
## Authentication Tokens
|
||||||
|
|
||||||
|
### Required Tokens
|
||||||
|
|
||||||
|
- **Input Token** (for POST operations): `input_token_123`
|
||||||
|
- **Read Token** (for GET operations): `read_token_456`
|
||||||
|
|
||||||
|
### Security Notes
|
||||||
|
|
||||||
|
- POST endpoints require the `input_token_123` bearer token
|
||||||
|
- GET endpoints require the `read_token_456` bearer token
|
||||||
|
- Change these tokens in `main.py` lines 22-23 for production use
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Simple Input**: Just send plain text data (e.g., "host_a,is ok")
|
||||||
|
- **Bearer Authentication**: Separate tokens for input and read operations
|
||||||
|
- **Automatic Directories**: Each run name creates its own directory
|
||||||
|
- **Timestamped Entries**: Every entry gets a timestamp
|
||||||
|
- **Multiple Runs**: Support multiple concurrent data collection runs
|
||||||
|
- **Easy Retrieval**: Get all results via REST API
|
||||||
|
- **Plain Text Output**: Results saved as simple text files
|
245
main.py
Normal file
245
main.py
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
# Simple FastAPI Data Collector
|
||||||
|
# Purpose: Accept simple comma-separated input and save to text files
|
||||||
|
# Usage: curl -X POST http://localhost:8000/api/run1/ -d "host_a,is ok"
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Request, Depends
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import bleach
|
||||||
|
|
||||||
|
# Initialize FastAPI application
|
||||||
|
app = FastAPI(
|
||||||
|
title="Simple Data Collector",
|
||||||
|
version="1.0.0",
|
||||||
|
description="Simple API for collecting comma-separated data"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define the base directory for input files
|
||||||
|
INPUT_DIR = Path("input")
|
||||||
|
INPUT_DIR.mkdir(exist_ok=True) # Create directory if it doesn't exist
|
||||||
|
|
||||||
|
# Authentication tokens
|
||||||
|
INPUT_TOKEN = "input_token_123" # Token for POST endpoints
|
||||||
|
READ_TOKEN = "read_token_456" # Token for GET endpoints
|
||||||
|
|
||||||
|
# Security schemes
|
||||||
|
input_security = HTTPBearer()
|
||||||
|
read_security = HTTPBearer()
|
||||||
|
|
||||||
|
def verify_input_token(credentials: HTTPAuthorizationCredentials = Depends(input_security)):
|
||||||
|
"""Verify bearer token for input operations"""
|
||||||
|
if credentials.credentials != INPUT_TOKEN:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Invalid authentication token for input operations"
|
||||||
|
)
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
def verify_read_token(credentials: HTTPAuthorizationCredentials = Depends(read_security)):
|
||||||
|
"""Verify bearer token for read operations"""
|
||||||
|
if credentials.credentials != READ_TOKEN:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Invalid authentication token for read operations"
|
||||||
|
)
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
def sanitize_input(data: str) -> str:
|
||||||
|
"""
|
||||||
|
Sanitize input data using bleach to prevent malicious content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (str): Raw input data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Sanitized data safe for storage
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Clean with bleach - strip all HTML tags and dangerous content
|
||||||
|
sanitized = bleach.clean(data, tags=[], attributes={}, strip=True)
|
||||||
|
|
||||||
|
# Remove control characters except newlines and tabs
|
||||||
|
sanitized = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', sanitized)
|
||||||
|
|
||||||
|
# Normalize whitespace
|
||||||
|
sanitized = re.sub(r'\s+', ' ', sanitized).strip()
|
||||||
|
|
||||||
|
# Limit length to prevent DoS
|
||||||
|
max_length = 1000
|
||||||
|
if len(sanitized) > max_length:
|
||||||
|
sanitized = sanitized[:max_length] + "... [truncated]"
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
|
||||||
|
def sanitize_run_name(run_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Sanitize run name to ensure it's safe for filesystem operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_name (str): Raw run name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Sanitized run name safe for filesystem
|
||||||
|
"""
|
||||||
|
if not run_name:
|
||||||
|
raise HTTPException(status_code=400, detail="Run name cannot be empty")
|
||||||
|
|
||||||
|
# Use bleach to clean any potential HTML/script content
|
||||||
|
sanitized = bleach.clean(run_name, tags=[], attributes={}, strip=True)
|
||||||
|
|
||||||
|
# Remove dangerous filesystem characters
|
||||||
|
sanitized = re.sub(r'[^\w\-.]', '_', sanitized)
|
||||||
|
|
||||||
|
# Remove leading/trailing dots and dashes
|
||||||
|
sanitized = sanitized.strip('.-')
|
||||||
|
|
||||||
|
# Prevent reserved names
|
||||||
|
reserved_names = ['con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4',
|
||||||
|
'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2',
|
||||||
|
'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9']
|
||||||
|
|
||||||
|
if sanitized.lower() in reserved_names:
|
||||||
|
sanitized = f"safe_{sanitized}"
|
||||||
|
|
||||||
|
# Limit length
|
||||||
|
if len(sanitized) > 50:
|
||||||
|
sanitized = sanitized[:50]
|
||||||
|
|
||||||
|
if not sanitized:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid run name after sanitization")
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
|
||||||
|
def save_to_file(run_name: str, data: str):
|
||||||
|
"""
|
||||||
|
Save data as a single line to input/{run_name}/results.txt
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_name (str): Name of the run (creates subdirectory)
|
||||||
|
data (str): The data to save as a single line
|
||||||
|
"""
|
||||||
|
# Create run-specific directory
|
||||||
|
run_dir = INPUT_DIR / run_name
|
||||||
|
run_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Define the results file path
|
||||||
|
results_file = run_dir / "results.txt"
|
||||||
|
|
||||||
|
# Get current timestamp
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Append the data with timestamp to the file
|
||||||
|
with open(results_file, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"{timestamp} - {data.strip()}\n")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Root endpoint providing API information"""
|
||||||
|
return {
|
||||||
|
"message": "Simple Data Collector API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"usage": "POST /api/{run_name}/ with data in body",
|
||||||
|
"example": "curl -X POST http://localhost:8000/api/run1/ -d 'host_a,is ok'",
|
||||||
|
"output": "Data saved to input/{run_name}/results.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/{run_name}/")
|
||||||
|
async def collect_data(run_name: str, request: Request, token: HTTPAuthorizationCredentials = Depends(verify_input_token)):
|
||||||
|
"""
|
||||||
|
Collect simple data and save to text file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_name (str): Name of the run (creates input/{run_name}/ directory)
|
||||||
|
request: Raw request body containing the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Confirmation message
|
||||||
|
|
||||||
|
Example:
|
||||||
|
curl -X POST http://localhost:8000/api/run1/ -d "host_a,is ok"
|
||||||
|
-> Saves to input/run1/results.txt
|
||||||
|
"""
|
||||||
|
# Read the raw body data
|
||||||
|
body = await request.body()
|
||||||
|
data = body.decode("utf-8")
|
||||||
|
|
||||||
|
# Validate that we have some data
|
||||||
|
if not data.strip():
|
||||||
|
raise HTTPException(status_code=400, detail="No data provided")
|
||||||
|
|
||||||
|
# Sanitize input data and run name
|
||||||
|
sanitized_data = sanitize_input(data)
|
||||||
|
sanitized_run_name = sanitize_run_name(run_name)
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
save_to_file(sanitized_run_name, sanitized_data)
|
||||||
|
|
||||||
|
# Return confirmation
|
||||||
|
return {
|
||||||
|
"message": "Data saved successfully",
|
||||||
|
"run_name": sanitized_run_name,
|
||||||
|
"data": sanitized_data,
|
||||||
|
"saved_to": f"input/{sanitized_run_name}/results.txt",
|
||||||
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/results/{run_name}/")
|
||||||
|
async def get_results(run_name: str, token: HTTPAuthorizationCredentials = Depends(verify_read_token)):
|
||||||
|
"""
|
||||||
|
Retrieve all results for a specific run.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_name (str): Name of the run to get results for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: All lines from the results file
|
||||||
|
"""
|
||||||
|
# Sanitize run name for safe filesystem access
|
||||||
|
sanitized_run_name = sanitize_run_name(run_name)
|
||||||
|
results_file = INPUT_DIR / sanitized_run_name / "results.txt"
|
||||||
|
|
||||||
|
if not results_file.exists():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"No results found for run '{sanitized_run_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read all lines from the file
|
||||||
|
with open(results_file, "r", encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"run_name": sanitized_run_name,
|
||||||
|
"total_entries": len(lines),
|
||||||
|
"results": [line.strip() for line in lines]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "Simple Data Collector",
|
||||||
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Application entry point
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
print("Starting Simple Data Collector API...")
|
||||||
|
print("Server will be available at: http://localhost:8000")
|
||||||
|
print("API documentation at: http://localhost:8000/docs")
|
||||||
|
print("Example: curl -X POST http://localhost:8000/api/run1/ -d 'host_a,is ok'")
|
||||||
|
print("Results saved to: ./input/{run_name}/results.txt")
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8000,
|
||||||
|
log_level="info"
|
||||||
|
)
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
pydantic==2.5.0
|
||||||
|
bleach==6.1.0
|
200
run_container.sh
Executable file
200
run_container.sh
Executable file
|
@ -0,0 +1,200 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# FastAPI Data Collector Container Runner
|
||||||
|
# Automated script to build and run the data collector in a container
|
||||||
|
# Supports both Podman and Docker with automatic detection
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONTAINER_NAME="fastapi-data-collector"
|
||||||
|
IMAGE_NAME="fastapi-data-collector:latest"
|
||||||
|
HOST_PORT="8000"
|
||||||
|
CONTAINER_PORT="8000"
|
||||||
|
DATA_DIR="./input"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}[HEADER]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect container runtime
|
||||||
|
detect_container_runtime() {
|
||||||
|
if command -v podman &> /dev/null; then
|
||||||
|
CONTAINER_CMD="podman"
|
||||||
|
print_status "Using Podman as container runtime"
|
||||||
|
elif command -v docker &> /dev/null; then
|
||||||
|
CONTAINER_CMD="docker"
|
||||||
|
print_status "Using Docker as container runtime"
|
||||||
|
else
|
||||||
|
print_error "Neither Podman nor Docker found. Please install one of them."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop and remove existing container
|
||||||
|
cleanup_existing_container() {
|
||||||
|
print_status "Checking for existing container..."
|
||||||
|
|
||||||
|
if $CONTAINER_CMD ps -a --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
print_warning "Stopping existing container: ${CONTAINER_NAME}"
|
||||||
|
$CONTAINER_CMD stop "${CONTAINER_NAME}" || true
|
||||||
|
|
||||||
|
print_warning "Removing existing container: ${CONTAINER_NAME}"
|
||||||
|
$CONTAINER_CMD rm "${CONTAINER_NAME}" || true
|
||||||
|
else
|
||||||
|
print_status "No existing container found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build container image
|
||||||
|
build_image() {
|
||||||
|
print_status "Building container image: ${IMAGE_NAME}"
|
||||||
|
|
||||||
|
if ! $CONTAINER_CMD build -t "${IMAGE_NAME}" .; then
|
||||||
|
print_error "Failed to build container image"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Container image built successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
setup_data_directory() {
|
||||||
|
if [ ! -d "${DATA_DIR}" ]; then
|
||||||
|
print_status "Creating data directory: ${DATA_DIR}"
|
||||||
|
mkdir -p "${DATA_DIR}"
|
||||||
|
else
|
||||||
|
print_status "Data directory already exists: ${DATA_DIR}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run container
|
||||||
|
run_container() {
|
||||||
|
print_status "Starting container: ${CONTAINER_NAME}"
|
||||||
|
|
||||||
|
# Convert relative path to absolute path
|
||||||
|
#DATA_DIR_ABS=$(realpath "${DATA_DIR}")
|
||||||
|
DATA_DIR_ABS="/home/user/Documents/Dom.Decimal/30-Projects/30-Misc/30-00-ZeroOne/30-00-002-Work-FastAPI-REST-Endpoint/input"
|
||||||
|
|
||||||
|
$CONTAINER_CMD run -d \
|
||||||
|
--name "${CONTAINER_NAME}" \
|
||||||
|
--publish "${HOST_PORT}:${CONTAINER_PORT}" \
|
||||||
|
--volume "${DATA_DIR_ABS}:/app/input:Z" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--security-opt no-new-privileges \
|
||||||
|
--cap-drop ALL \
|
||||||
|
--cap-add=CHOWN \
|
||||||
|
--cap-add=SETGID \
|
||||||
|
--cap-add=SETUID \
|
||||||
|
--cap-add=DAC_OVERRIDE \
|
||||||
|
--read-only \
|
||||||
|
--tmpfs /tmp \
|
||||||
|
"${IMAGE_NAME}"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_status "Container started successfully"
|
||||||
|
else
|
||||||
|
print_error "Failed to start container"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify container is running
|
||||||
|
verify_container() {
|
||||||
|
print_status "Verifying container startup..."
|
||||||
|
|
||||||
|
# Wait a moment for the container to start
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if $CONTAINER_CMD ps --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
print_status "Container is running"
|
||||||
|
|
||||||
|
# Show container logs
|
||||||
|
print_status "Container logs:"
|
||||||
|
$CONTAINER_CMD logs "${CONTAINER_NAME}" | tail -10
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
print_status "Testing health endpoint..."
|
||||||
|
sleep 2
|
||||||
|
if curl -s "http://localhost:${HOST_PORT}/health" > /dev/null; then
|
||||||
|
print_status "Health check passed"
|
||||||
|
else
|
||||||
|
print_warning "Health check failed - service may still be starting"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Container is not running"
|
||||||
|
print_error "Container logs:"
|
||||||
|
$CONTAINER_CMD logs "${CONTAINER_NAME}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show usage information
|
||||||
|
show_usage_info() {
|
||||||
|
print_header "=== FastAPI Data Collector Container Started ==="
|
||||||
|
echo
|
||||||
|
print_status "Service URL: http://localhost:${HOST_PORT}"
|
||||||
|
print_status "API Documentation: http://localhost:${HOST_PORT}/docs"
|
||||||
|
print_status "Health Check: http://localhost:${HOST_PORT}/health"
|
||||||
|
echo
|
||||||
|
print_status "Authentication Tokens:"
|
||||||
|
echo " - Input Token: input_token_123"
|
||||||
|
echo " - Read Token: read_token_456"
|
||||||
|
echo
|
||||||
|
print_status "Example Usage:"
|
||||||
|
echo " # Send data:"
|
||||||
|
echo ' curl -X POST http://localhost:8000/api/run1/ \'
|
||||||
|
echo ' -H "Authorization: Bearer input_token_123" \'
|
||||||
|
echo ' -d "host_a,is ok"'
|
||||||
|
echo
|
||||||
|
echo " # Read results:"
|
||||||
|
echo ' curl -H "Authorization: Bearer read_token_456" \'
|
||||||
|
echo ' http://localhost:8000/results/run1/'
|
||||||
|
echo
|
||||||
|
print_status "Data Directory: ${DATA_DIR_ABS}"
|
||||||
|
echo
|
||||||
|
print_status "Container Management:"
|
||||||
|
echo " - View logs: ${CONTAINER_CMD} logs ${CONTAINER_NAME}"
|
||||||
|
echo " - Stop container: ${CONTAINER_CMD} stop ${CONTAINER_NAME}"
|
||||||
|
echo " - Restart container: ${CONTAINER_CMD} restart ${CONTAINER_NAME}"
|
||||||
|
echo " - Remove container: ${CONTAINER_CMD} rm ${CONTAINER_NAME}"
|
||||||
|
echo
|
||||||
|
print_header "=============================================="
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
main() {
|
||||||
|
print_header "FastAPI Data Collector Container Setup"
|
||||||
|
echo
|
||||||
|
|
||||||
|
detect_container_runtime
|
||||||
|
cleanup_existing_container
|
||||||
|
build_image
|
||||||
|
setup_data_directory
|
||||||
|
run_container
|
||||||
|
verify_container
|
||||||
|
show_usage_info
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$@"
|
Loading…
Add table
Add a link
Reference in a new issue