diff --git a/.env.example b/.env.example index d55c1fb..fb13bda 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,4 @@ -CONTROLLER_HOST=ocrd-controller -CONTROLLER_PORT_SSH=22 - -MANAGER_DATA=~/.ssh/id_rsa -MANAGER_KEY=~/ +MANAGER_DATA=~/ MANAGER_HOST=ocrd-manager MANAGER_PORT_WEB=4004 diff --git a/Makefile b/Makefile index c883c78..6a0ecd2 100644 --- a/Makefile +++ b/Makefile @@ -28,44 +28,30 @@ Variables: currently: "$(TAGNAME)" - MONITOR_PORT_WEB TCP port for the (host-side) web server currently: $(MONITOR_PORT_WEB) - - MANAGER_KEY SSH key file to mount (for the Controller client) - currently: "$(MANAGER_KEY)" - MANAGER_DATA host directory to mount into `/data` (shared with Manager) currently: "$(MANAGER_DATA)" - MANAGER_WORKFLOWS host directory to mount into `/workflows` (shared with Manager) currently: "$(MANAGER_WORKFLOWS)" - NETWORK Docker network to use (manage via "docker network") currently: $(NETWORK) - - CONTROLLER_HOST network address for the Controller client - (must be reachable from the container network) - currently: $(CONTROLLER_HOST) - - CONTROLLER_PORT_SSH network port for the Controller client - (must be reachable from the container network) - currently: $(CONTROLLER_PORT_SSH) EOF endef export HELP help: ; @eval "$$HELP" -MANAGER_KEY ?= $(firstword $(filter-out %.pub,$(wildcard $(HOME)/.ssh/id_*))) MANAGER_DATA ?= $(CURDIR) MANAGER_WORKFLOWS ?= $(CURDIR) MONITOR_PORT_WEB ?= 5000 NETWORK ?= bridge -CONTROLLER_HOST ?= $(shell dig +short $$HOSTNAME) -CONTROLLER_PORT_SSH ?= 8022 run: $(DATA) docker run -d --rm \ -h ocrd_monitor \ --name ocrd_monitor \ --network=$(NETWORK) \ -p $(MONITOR_PORT_WEB):5000 \ - -v ${MANAGER_KEY}:/id_rsa \ - --mount type=bind,source=$(MANAGER_KEY),target=/id_rsa \ -v $(MANAGER_DATA):/data \ -v $(MANAGER_WORKFLOWS):/workflows \ -v shared:/run/lock/ocrd.jobs \ - -e CONTROLLER=$(CONTROLLER_HOST):$(CONTROLLER_PORT_SSH) \ -e MONITOR_PORT_LOG=${MONITOR_PORT_LOG} \ $(TAGNAME) diff --git a/README.md b/README.md index b8cc38d..9d879a3 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,8 @@ In order to work properly, the following **environment variables** must be set: | Variable | Description | | ------------------- | -------------------------------------------------------------------------------- | -| CONTROLLER_HOST | Hostname of the OCR-D Controller | -| CONTROLLER_PORT_SSH | Port on the OCR-D Controller host that allows a SSH connection | | MANAGER_DATA | Path to the OCR-D workspaces on the host | | MANAGER_WORKFLOWS | Path to the OCR-D workflows on the host | -| MANAGER_KEY | Path to a private key that can be used to authenticate with the OCR-D Controller | | MONITOR_PORT_WEB | The port at which the OCR-D Monitor will be available on the host | | MONITOR_PORT_LOG | The port at which the Dozzle logs will be available on the host | diff --git a/docker-compose.yml b/docker-compose.yml index dd69359..b53bda6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,6 @@ services: hostname: ${MONITOR_HOST} environment: - CONTROLLER: "${CONTROLLER_HOST}:${CONTROLLER_PORT_SSH}" MANAGER_URL: "http://${MANAGER_HOST}:${MANAGER_PORT_WEB}" MONITOR_PORT_LOG: ${MONITOR_PORT_LOG} MONITOR_DB_CONNECTION: "mongodb://${MONITOR_DB_ROOT_USER:-root}:${MONITOR_DB_ROOT_PASSWORD:-root_password}@ocrd-database:27017" @@ -28,7 +27,6 @@ services: volumes: - ${MANAGER_DATA}:/data - ${MANAGER_WORKFLOWS}:/workflows - - ${MANAGER_KEY}:/id_rsa - shared:/run/lock/ocrd.jobs ocrd-logview: diff --git a/init.sh b/init.sh index c003376..ab68f6b 100755 --- a/init.sh +++ b/init.sh @@ -1,31 +1,18 @@ #!/usr/bin/env bash -mkdir -p ~/.ssh -cat /id_rsa >> ~/.ssh/id_rsa -chmod go-rw ~/.ssh/id_rsa - -# Add ocrd controller as global and known_hosts if env exist -if [ -n "$CONTROLLER" ]; then - CONTROLLER_HOST=${CONTROLLER%:*} - CONTROLLER_PORT=${CONTROLLER#*:} - CONTROLLER_IP=$(nslookup $CONTROLLER_HOST | grep 'Address\:' | awk 'NR==2 {print $2}') - - if test -e /etc/ssh/ssh_known_hosts; then - ssh-keygen -R $CONTROLLER_HOST -f /etc/ssh/ssh_known_hosts - ssh-keygen -R $CONTROLLER_IP -f /etc/ssh/ssh_known_hosts - fi - ssh-keyscan -H -p ${CONTROLLER_PORT:-22} $CONTROLLER_HOST,$CONTROLLER_IP >>/etc/ssh/ssh_known_hosts -fi - export MONITOR_DB_CONNECTION_STRING=$MONITOR_DB_CONNECTION export OCRD_BROWSER__MODE=native -export OCRD_BROWSER__WORKSPACE_DIR=/data/ocr-d +# all OCR-D workspaces on the Manager are under /data/ocr-d +# but since the Manager resolves everything under /data +# it tracks the workspace directory relative to that in the database +# (e.g. ocr-d/testdata-production) +# so if we write /data/ocr-d, we could list workspaces fine, +# but our workspace URLs from the job database would be wrong +# (resolving as /data/ocr-d/ocr-d/...) +# so better just use /data as well here: +export OCRD_BROWSER__WORKSPACE_DIR=/data export OCRD_BROWSER__PORT_RANGE="[9000,9100]" export OCRD_LOGVIEW__PORT=$MONITOR_PORT_LOG -export OCRD_CONTROLLER__HOST=$CONTROLLER_HOST -export OCRD_CONTROLLER__PORT=$CONTROLLER_PORT -export OCRD_CONTROLLER__USER=admin -export OCRD_CONTROLLER__KEYFILE=~/.ssh/id_rsa export OCRD_MANAGER__URL=$MANAGER_URL cd /usr/local/ocrd-monitor diff --git a/ocrdmonitor/database/_browserprocessrepository.py b/ocrdmonitor/database/_browserprocessrepository.py index f3e066d..b051940 100644 --- a/ocrdmonitor/database/_browserprocessrepository.py +++ b/ocrdmonitor/database/_browserprocessrepository.py @@ -29,7 +29,7 @@ def __init__(self, restoring_factory: BrowserRestoringFactory) -> None: self._restoring_factory = restoring_factory async def insert(self, browser: OcrdBrowser) -> None: - await BrowserProcess( # type: ignore + await BrowserProcess( address=browser.address(), owner=browser.owner(), process_id=browser.process_id(), diff --git a/ocrdmonitor/database/_initdb.py b/ocrdmonitor/database/_initdb.py index 0a44bcf..922c30e 100644 --- a/ocrdmonitor/database/_initdb.py +++ b/ocrdmonitor/database/_initdb.py @@ -39,11 +39,11 @@ async def init(connection_str: str, force_initialize: bool = False) -> None: __initialized = True connection_str = rebuild_connection_string(connection_str) - client: AsyncIOMotorClient = AsyncIOMotorClient(connection_str) # type: ignore - client.get_io_loop = asyncio.get_event_loop # type: ignore + client = AsyncIOMotorClient(connection_str) # type: ignore[var-annotated] + client.get_io_loop = asyncio.get_event_loop # type: ignore[method-assign] await init_beanie( - database=client.ocrd, # type: ignore - document_models=[BrowserProcess, MongoOcrdJob], # type: ignore + database=client.ocrd, + document_models=[BrowserProcess, MongoOcrdJob], ) return init diff --git a/ocrdmonitor/database/_ocrdjobrepository.py b/ocrdmonitor/database/_ocrdjobrepository.py index dd07d35..ed86e15 100644 --- a/ocrdmonitor/database/_ocrdjobrepository.py +++ b/ocrdmonitor/database/_ocrdjobrepository.py @@ -20,7 +20,6 @@ class MongoOcrdJob(Document): workdir: Path remotedir: str workflow_file: Path - controller_address: str class Settings: name = "OcrdJob" @@ -36,7 +35,7 @@ class Settings: class MongoJobRepository: async def insert(self, job: OcrdJob) -> None: - await MongoOcrdJob(**asdict(job)).insert() # type: ignore + await MongoOcrdJob(**asdict(job)).insert() async def find_all(self) -> list[OcrdJob]: return [OcrdJob(**j.dict(exclude={"id"})) for j in await MongoOcrdJob.find_all().to_list()] diff --git a/ocrdmonitor/environment.py b/ocrdmonitor/environment.py index 6a01f5e..88b9ac0 100644 --- a/ocrdmonitor/environment.py +++ b/ocrdmonitor/environment.py @@ -9,9 +9,8 @@ SubProcessOcrdBrowserFactory, ) from ocrdmonitor import database -from ocrdmonitor.protocols import RemoteServer, Repositories +from ocrdmonitor.protocols import Repositories from ocrdmonitor.server.settings import Settings -from ocrdmonitor.sshremote import SSHRemote BrowserType = Type[SubProcessOcrdBrowser] | Type[DockerOcrdBrowser] CreatingFactories: dict[str, Callable[[set[int]], OcrdBrowserFactory]] = { @@ -40,6 +39,3 @@ async def repositories(self) -> Repositories: def browser_factory(self) -> OcrdBrowserFactory: port_range_set = set(range(*self.settings.ocrd_browser.port_range)) return CreatingFactories[self.settings.ocrd_browser.mode](port_range_set) - - def controller_server(self) -> RemoteServer: - return SSHRemote(self.settings.ocrd_controller) diff --git a/ocrdmonitor/ocrdcontroller.py b/ocrdmonitor/ocrdcontroller.py deleted file mode 100644 index f869946..0000000 --- a/ocrdmonitor/ocrdcontroller.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -from ocrdmonitor.processstatus import ProcessState, ProcessStatus -from ocrdmonitor.protocols import OcrdJob, RemoteServer - - -class OcrdController: - def __init__(self, remote: RemoteServer) -> None: - self._remote = remote - - async def status_for(self, ocrd_job: OcrdJob) -> ProcessStatus | None: - if ocrd_job.remotedir is None: - return None - - pid = await self._remote.read_file(f"/data/{ocrd_job.remotedir}/ocrd.pid") - process_statuses = await self._remote.process_status(int(pid)) - - for status in process_statuses: - if status.state == ProcessState.RUNNING: - return status - - if process_statuses: - return process_statuses[0] - - return None diff --git a/ocrdmonitor/processstatus.py b/ocrdmonitor/processstatus.py deleted file mode 100644 index 433284b..0000000 --- a/ocrdmonitor/processstatus.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from datetime import timedelta -from enum import Enum - - -class ProcessState(Enum): - # see ps(1)#PROCESS_STATE_CODES - RUNNING = "R" - SLEEPING = "S" - SLEEPIO = "D" - STOPPED = "T" - TRACING = "t" - ZOMBIE = "Z" - UNKNOWN = "?" - - def __str__(self) -> str: - return str(self.name) - - -@dataclass(frozen=True) -class ProcessStatus: - pid: int - state: ProcessState - percent_cpu: float - memory: int - cpu_time: timedelta - - @classmethod - def shell_command(cls, pid: int) -> str: - return f"ps -s {pid} -o pid,state,%cpu,rss,cputime --no-headers" - - @classmethod - def from_shell_output(cls, ps_output: str) -> list["ProcessStatus"]: - def is_error(lines: list[str]) -> bool: - return lines[0].startswith("error:") - - def parse_line(line: str) -> "ProcessStatus": - pid, state, percent_cpu, memory, cpu_time, *_ = line.split() - return cls( - pid=int(pid), - state=ProcessState(state[0]), - percent_cpu=float(percent_cpu), - memory=int(memory), - cpu_time=timedelta(seconds=_cpu_time_to_seconds(cpu_time)), - ) - - lines = ps_output.strip().splitlines() - if not lines or is_error(lines): - return [] - - return [parse_line(line) for line in lines] - - -def _cpu_time_to_seconds(cpu_time: str) -> int: - hours, minutes, seconds, *_ = cpu_time.split(":") - return int(hours) * 3600 + int(minutes) * 60 + int(seconds) diff --git a/ocrdmonitor/protocols.py b/ocrdmonitor/protocols.py index 066c0bf..5a37c84 100644 --- a/ocrdmonitor/protocols.py +++ b/ocrdmonitor/protocols.py @@ -4,7 +4,6 @@ from typing import Collection, NamedTuple, Protocol from ocrdbrowser import OcrdBrowser, OcrdBrowserFactory -from ocrdmonitor.processstatus import ProcessStatus from ocrdmonitor.server.settings import Settings @@ -49,7 +48,6 @@ class OcrdJob: workdir: Path remotedir: str workflow_file: Path - controller_address: str @property def is_running(self) -> bool: @@ -72,13 +70,6 @@ async def find_all(self) -> list[OcrdJob]: ... -class RemoteServer(Protocol): - async def read_file(self, path: str) -> str: - ... - - async def process_status(self, process_group: int) -> list[ProcessStatus]: - ... - class Repositories(NamedTuple): browser_processes: BrowserProcessRepository ocrd_jobs: JobRepository @@ -92,6 +83,3 @@ async def repositories(self) -> Repositories: def browser_factory(self) -> OcrdBrowserFactory: ... - - def controller_server(self) -> RemoteServer: - ... diff --git a/ocrdmonitor/server/jobs.py b/ocrdmonitor/server/jobs.py index fe838d2..cf34dc7 100644 --- a/ocrdmonitor/server/jobs.py +++ b/ocrdmonitor/server/jobs.py @@ -8,20 +8,12 @@ from fastapi.responses import JSONResponse from fastapi.templating import Jinja2Templates -from ocrdmonitor.ocrdcontroller import OcrdController -from ocrdmonitor.processstatus import ProcessStatus from ocrdmonitor.protocols import Environment, OcrdJob, Repositories import httpx import logging -@dataclass -class RunningJob: - ocrd_job: OcrdJob - process_status: ProcessStatus - - def split_into_running_and_completed( jobs: Iterable[OcrdJob], ) -> tuple[list[OcrdJob], list[OcrdJob]]: @@ -30,25 +22,11 @@ def split_into_running_and_completed( return running_ocrd_jobs, completed_ocrd_jobs -def wrap_in_running_job_type( - running_ocrd_jobs: Iterable[OcrdJob], - job_status: Iterable[ProcessStatus | None], -) -> Iterable[RunningJob]: - running_jobs = [ - RunningJob(job, process_status) - for job, process_status in zip(running_ocrd_jobs, job_status) - if process_status is not None - ] - - return running_jobs - - def create_jobs( templates: Jinja2Templates, environment: Environment, ) -> APIRouter: router = APIRouter(prefix="/jobs") - controller = OcrdController(environment.controller_server()) @router.get("/", name="jobs") async def jobs( @@ -58,17 +36,14 @@ async def jobs( jobs = await job_repository.find_all() running, completed = split_into_running_and_completed(jobs) - job_status = [await controller.status_for(job) for job in running] - running_jobs = wrap_in_running_job_type(running, job_status) - now = datetime.now(timezone.utc) return templates.TemplateResponse( "jobs.html.j2", { "request": request, "running_jobs": sorted( - running_jobs, - key=lambda x: x.ocrd_job.time_created or now, + running, + key=lambda x: x.time_created or now, ), "completed_jobs": sorted( completed, diff --git a/ocrdmonitor/server/settings.py b/ocrdmonitor/server/settings.py index b5c88a3..4d2997d 100644 --- a/ocrdmonitor/server/settings.py +++ b/ocrdmonitor/server/settings.py @@ -34,12 +34,6 @@ } -class OcrdControllerSettings(BaseSettings): - host: str - user: str - port: int = 22 - keyfile: Path = Path.home() / ".ssh" / "id_rsa" - class OcrdManagerSettings(BaseSettings): url: str @@ -69,7 +63,7 @@ def validator(cls, value: str | tuple[int, int]) -> tuple[int, int]: if not int_pair or len(int_pair) != 2: raise ValueError("Port range must have exactly two values") - return int_pair # type: ignore + return int_pair class Settings(BaseSettings): @@ -78,7 +72,6 @@ class Settings(BaseSettings): monitor_db_connection_string: str ocrd_browser: OcrdBrowserSettings - ocrd_controller: OcrdControllerSettings ocrd_logview: OcrdLogViewSettings ocrd_manager: OcrdManagerSettings diff --git a/ocrdmonitor/server/templates/jobs.html.j2 b/ocrdmonitor/server/templates/jobs.html.j2 index 05c2459..eab8f3e 100644 --- a/ocrdmonitor/server/templates/jobs.html.j2 +++ b/ocrdmonitor/server/templates/jobs.html.j2 @@ -32,27 +32,19 @@