Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,28 @@ To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you
```

5. **Execute the Application:**
- For a development environment, run:
- For a development environment with auto-reload, run:
```bash
./web/app.py
python3 web/main.py
```
To change the port, use the `--port` argument:
```bash
python3 web/main.py --port 9000
```
- For a production environment, use:
```bash
gunicorn web.wsgi:application
uvicorn web.main:app --host 0.0.0.0 --port 8080
```

During the coding and testing phases, use the development environment to easily debug and make changes. When deploying the app for end users, use the production environment to ensure better performance, scalability, and security.
During the coding and testing phases, use the development environment to easily debug and make changes with auto-reload enabled. When deploying the app for end users, use the production environment to ensure better performance, scalability, and security.

The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by using the `--basedir` option with the above commands or by setting the `CBS_BASEDIR` environment variable.
The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by setting the `CBS_BASEDIR` environment variable.

6. **Access the Web Interface:**

Once the application is running, you can access the interface in your web browser at http://localhost:5000 if running directly using app.py (development environment), or at http://localhost:8000 if using Gunicorn (production environment).
Once the application is running, you can access the interface in your web browser at http://localhost:8080.

To change the default port when running with app.py, modify the `app.run()` call in web/app.py file by passing `port=<expected-port>` as an argument. For Gunicorn, refer to the [commonly used arguments](https://docs.gunicorn.org/en/latest/run.html#commonly-used-arguments) section of the Gunicorn documentation to specify a different port.
The default port is 8080, or the value of the `WEB_PORT` environment variable if set. You can override this by passing the `--port` argument when running the application directly (e.g., `python3 web/main.py --port 9000`) or when using uvicorn (e.g., `uvicorn web.main:app --port 5000`). Refer to the [uvicorn documentation](https://www.uvicorn.org/) for additional configuration options.

## Directory Structure
The default directory structure is established as follows:
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ services:
CBS_LOG_LEVEL: ${CBS_LOG_LEVEL:-INFO}
CBS_ENABLE_INBUILT_BUILDER: 0
CBS_GITHUB_ACCESS_TOKEN: ${CBS_GITHUB_ACCESS_TOKEN}
CBS_REMOTES_RELOAD_TOKEN: ${CBS_REMOTES_RELOAD_TOKEN}
PYTHONPATH: /app
GUNICORN_CMD_ARGS: --bind=0.0.0.0:80 --timeout=300
volumes:
- ./base:/base:rw
depends_on:
- redis
ports:
- "127.0.0.1:${WEB_PORT:-8080}:80"
- "127.0.0.1:${WEB_PORT:-8080}:8080"

builder:
build:
Expand Down
73 changes: 73 additions & 0 deletions metadata_manager/ap_src_meta_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,79 @@ def get_build_options_at_commit(self, remote: str,
)
return build_options

def get_board_defaults_from_fw_server(
self,
artifacts_url: str,
board_id: str,
vehicle_id: str = None,
) -> dict:
"""
Fetch board defaults from firmware.ardupilot.org features.txt.

The features.txt file contains lines like:
- FEATURE_NAME (enabled features)
- !FEATURE_NAME (disabled features)

Parameters:
artifacts_url (str): Base URL for build artifacts for a version.
board_id (str): Board identifier
vehicle_id (str): Vehicle identifier
(for special handling like Heli)

Returns:
dict: Dictionary mapping feature define to state
(1 for enabled, 0 for disabled), or None if fetch fails
"""
import requests

# Heli builds are stored under a separate folder
artifacts_subdir = board_id
if vehicle_id == "Heli":
artifacts_subdir += "-heli"

features_txt_url = f"{artifacts_url}/{artifacts_subdir}/features.txt"

try:
response = requests.get(features_txt_url, timeout=30)
response.raise_for_status()

feature_states = {}
enabled_count = 0
disabled_count = 0

for line in response.text.splitlines():
line = line.strip()

# Skip empty lines and comments
if not line or line.startswith('#'):
continue

# Check if feature is disabled (prefixed with !)
if line.startswith('!'):
feature_name = line[1:].strip()
if feature_name:
feature_states[feature_name] = 0
disabled_count += 1
else:
# Enabled feature
if line:
feature_states[line] = 1
enabled_count += 1

self.logger.info(
f"Fetched board defaults from firmware server: "
f"{enabled_count} enabled, "
f"{disabled_count} disabled"
)

return feature_states

except requests.RequestException as e:
self.logger.warning(
f"Failed to fetch board defaults from {features_txt_url}: {e}"
)
return None

@staticmethod
def get_singleton():
return APSourceMetadataFetcher.__singleton
9 changes: 7 additions & 2 deletions web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
FROM python:3.10.16-slim-bookworm

RUN apt-get update \
&& apt-get install -y --no-install-recommends git gosu
&& apt-get install -y --no-install-recommends git gosu \
&& rm -rf /var/lib/apt/lists/*

RUN groupadd -g 999 ardupilot && \
useradd -u 999 -g 999 -m ardupilot --shell /bin/false && \
Expand All @@ -12,5 +13,9 @@ COPY --chown=ardupilot:ardupilot . /app
WORKDIR /app/web
RUN pip install --no-cache-dir -r requirements.txt

ENV PYTHONPATH=/app

EXPOSE 8080

ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["gunicorn", "wsgi:application"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
4 changes: 4 additions & 0 deletions web/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""API v1 module."""
from .router import router

__all__ = ["router"]
81 changes: 81 additions & 0 deletions web/api/v1/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from fastapi import APIRouter, HTTPException, Depends, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

from schemas import RefreshRemotesResponse
from services.admin import get_admin_service, AdminService


router = APIRouter(prefix="/admin", tags=["admin"])
security = HTTPBearer()


async def verify_admin_token(
credentials: HTTPAuthorizationCredentials = Depends(security),
admin_service: AdminService = Depends(get_admin_service)
) -> None:
"""
Verify the bearer token for admin authentication.

Args:
credentials: HTTP authorization credentials from request header
admin_service: Admin service instance

Raises:
401: Invalid or missing token
500: Server configuration error (token not configured)
"""
token = credentials.credentials
try:
if not await admin_service.verify_token(token):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token"
)
except RuntimeError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)


@router.post(
"/refresh_remotes",
response_model=RefreshRemotesResponse,
responses={
401: {"description": "Invalid or missing authentication token"},
500: {
"description": (
"Server configuration error (token not configured) "
"or refresh operation failed"
)
}
}
)
async def refresh_remotes(
_: None = Depends(verify_admin_token),
admin_service: AdminService = Depends(get_admin_service)
):
"""
Trigger a hot reset/refresh of remote metadata.

This endpoint requires bearer token authentication in the Authorization
header:
```
Authorization: Bearer <your-token>
```

Returns:
RefreshRemotesResponse: List of remotes that were refreshed

Raises:
401: Invalid or missing authentication token
500: Refresh operation failed
"""
try:
remotes = await admin_service.refresh_remotes()
return RefreshRemotesResponse(remotes=remotes)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to refresh remotes: {str(e)}"
)
Loading