Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remote operator #4

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
Dockerfile
README.md
.env
venv/
__pycache__
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.env
cron-runs.log
venv/
__pycache__
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11.0
12 changes: 12 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ vars:
sh: grep -v '^#' .env | grep -e "OL_IMAGE" | sed -e 's/.*=//'
BRANCH:
sh: grep -v '^#' .env | grep -e "OL_BRANCH" | sed -e 's/.*=//'
TOOLS_IMAGE:
sh: grep -v '^#' .env | grep -e "OL_TOOLS_IMAGE" | sed -e 's/.*=//'

tasks:
docker:build:
Expand All @@ -25,6 +27,11 @@ tasks:
cmds:
- docker build --build-arg BRANCH={{.BRANCH}} --tag {{.IMAGE}}-builder --target builder .

docker:build:tools:
desc: "Build docker [tools] image"
cmds:
- docker build -f tools.Dockerfile --tag {{.TOOLS_IMAGE}} .

docker:push:
desc: "Push docker image"
cmds:
Expand All @@ -40,6 +47,11 @@ tasks:
cmds:
- docker push {{.IMAGE}}-builder

docker:push:tools:
desc: "Push docker [tools] image"
cmds:
- docker push {{.TOOLS_IMAGE}}

shell:
desc: "Start a shell container"
cmds:
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ services:
ports:
- "3030:3030"

remote:
image: "${OL_TOOLS_IMAGE}"
container_name: 0l-remote-operator
restart: "on-failure"
ports:
- "3333:3333"
volumes:
- "./remote_operator:/code"

########## Utility services #############

shell:
Expand Down
12 changes: 10 additions & 2 deletions example.env
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
###### [Build config] ######
# Release tag or branch name to checkout and build
OL_BRANCH='v5.1.1'
OL_BRANCH='main'

###### [Project config] ######

# 0L Docker image
# Available tags: https://hub.docker.com/r/nourspace/0l/tags
OL_IMAGE='nourspace/0l:v5.1.1'
OL_IMAGE='nourspace/0l:main'

# Host path to be mounted (node_data) and used as DATA_DIR for 0L services
OL_DATA_DIR='~/.0L/'
OL_EPOCH_ARCHIVE_DIR='~/epoch-archive/'

# Project name
# This value is prepended along with the service name to the container on start up
Expand All @@ -26,6 +27,7 @@ OL_NODE_MODE='validator'

# Enable if your fullnode/validator are not in sync so tower uses upstream instead
#OL_TOWER_USE_FIRST_UPSTREAM='--use-first-url'
OL_TOWER_USE_FIRST_UPSTREAM=""

# To start tower in operator mode
OL_TOWER_OPERATOR='--is-operator'
Expand All @@ -42,3 +44,9 @@ OL_TOWER_VERBOSE='--verbose'
RUST_LOG='error'
# Capture backtrace on error
RUST_BACKTRACE='1'

###### 0L-Tools ######

# 0L-Tools Docker image
# Available tags: https://hub.docker.com/r/nourspace/0l-tools/tags
OL_TOOLS_IMAGE='nourspace/0l-tools:v0.1'
68 changes: 68 additions & 0 deletions remote_operator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 0L-Remote-Operator

## Goal

Allows 0L node operators to perform critical operations remotely.

## Terms

- Local operator: entity owning or running an 0L node
- Remote operator: entity with permission to perform remote operations on an 0L node setup

## Tech

- Backend (Python)
- [FastAPI](https://fastapi.tiangolo.com/) for exposing API endpoints
- [pydantic](https://pydantic-docs.helpmanual.io/) for data validation and settings management
- Frontend (Javascript)
- Single HTML page running [Vue](https://vuejs.org/) app
- Optional: [Buefy components](https://buefy.org/) based on[Bulma](http://bulma.io/)

## Deployment

- Extra service in 0l-operations' [docker-compose](../docker-compose.yml)
- Based on [3.11.0-slim-bullseye](https://hub.docker.com/_/python/tags?page=1&name=3.11.0-slim-bullseye) docker image
- Only python requirements are installed in the image
- Source code resides in the repo and gets mounted as a host volume

## Features

- Restarting node and other services
- Managing cron: start, stop
- Updating specific values in `0L.toml`
- Updating specific values in `validator.node.yaml`
- else?

## Security concerns and measures

- Source code is always auditable by node operators
- Python is chosen to be clearly interpreted
- Docker image only provides runtime environment
- Local operators must create firewall rules to only allow traffic from trusted sources to particular ports
- Entire request bodies are validated not allowing any arbitrary payloads
- All endpoints are auth protected; preferably JWT with short expiry
- Basic auth endpoint to acquire JWT token
- Each operation must
- provide a reason
- creates a backup of files it has modified

## Questions

- How many are currently using the 0l-operations setup? in general, we want a statistic on different setups being used
- What other security concerns do we have?

## Tasks

- [ ] Collect more info
- [ ] Finalise features
- [ ] Add basic auth
- [ ] Publish initial version with restart feature
- [ ] Implement logging and backups

## Dynamic Operations

Using operations.json node owners can define arbitrary allowed operations that suit their needs without having to update
the service code.

The main reason behind this is that not everyone is running the docker setup, and it would not be as useful to hardcode
docker-related operations only.
96 changes: 96 additions & 0 deletions remote_operator/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import json
from enum import Enum

from fastapi import FastAPI
from pydantic import BaseModel, create_model

app = FastAPI(title="OL Remote Operator", version="0.0.1")


@app.get("/")
def root():
return {"Hello": "0L Operator!"}


class OperationType(str, Enum):
"""
Operation types

cmd: run a command and return its output
patch: given a filename, update part of its content

"""
CMD = 'cmd'
PATCH = 'patch'


class BaseOperation(BaseModel):
name: str
description: str
method: str
type: OperationType
checks: list[str] = []


class CmdOperation(BaseOperation):
cmd: list[str] = None
filename: str = None
payload: str = None


class PatchOperation(BaseOperation):
cmd: list[str] = None
filename: str = None
payload: str = None


class BaseRequest(BaseModel):
reason: str


# Load operations schema
with open("operations.json") as f:
operations = json.load(f)

for op_dict in operations:
op: BaseOperation = BaseOperation.parse_obj(op_dict)

# Create request model
request_model_attrs = {
'__base__': BaseRequest,
}
# Create response model

if op.type == OperationType.CMD:
op: CmdOperation = CmdOperation.parse_obj(op)

elif op.type == OperationType.PATCH:
op: PatchOperation = PatchOperation.parse_obj(op)
# create payload model based on schema from op.payload

OperationRequestModel = create_model(f"Request{op.name.capitalize()}", **request_model_attrs)

# Create endpoint

def endpoint(data: OperationRequestModel):
if op.type == OperationType.CMD:
output = 1
elif op.type == OperationType.PATCH:
output = 2

return {
"operation": op.name,
"description": op.description,
"not": "implemented",
"output": output
}


# Add endpoint to api
app.add_api_route(
path=f"/{op.name}",
methods=[op.method],
endpoint=endpoint,
name=f"{op.name.capitalize()} Endpoint",
description=op.description
)
60 changes: 60 additions & 0 deletions remote_operator/operations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[
{
"name": "restart",
"description": "Restarts node",
"method": "post",
"type": "cmd",
"checks": [
"echo checking..."
],
"cmd": [
"echo restarting..."
]
},
{
"name": "cron_on",
"description": "Setup cron",
"method": "post",
"type": "cmd",
"checks": [
"echo checking..."
],
"cmd": [
"echo setting cron..."
]
},
{
"name": "cron_off",
"description": "Disable cron",
"method": "post",
"type": "cmd",
"checks": [
"echo checking..."
],
"cmd": [
"echo disabling cron..."
]
},
{
"name": "patch_validator_yaml",
"description": "Patch validator.node.yaml",
"method": "patch",
"type": "patch",
"checks": [
"echo checking..."
],
"filename": "validator.node.yaml",
"payload": "str"
},
{
"name": "patch_ol_toml",
"description": "Patch 0L.toml",
"method": "patch",
"type": "patch",
"checks": [
"echo checking..."
],
"filename": "0L.toml",
"payload": "str"
}
]
17 changes: 17 additions & 0 deletions tools.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.11-slim-bullseye

ENV PYTHONUNBUFFERED 1

# Install pip requirements
COPY ./tools.requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt

# Define working directory
RUN mkdir /code
WORKDIR /code

# Expose Uvicorn port
EXPOSE 3333

# Command to serve API
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3333"]
4 changes: 4 additions & 0 deletions tools.requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fastapi==0.87.0
pydantic==1.10.2
python-dotenv==0.21.0
uvicorn==0.19.0