diff --git a/brainframe/cli/brainframe_compose.py b/brainframe/cli/brainframe_compose.py index df2aa19..1b61ca4 100644 --- a/brainframe/cli/brainframe_compose.py +++ b/brainframe/cli/brainframe_compose.py @@ -2,26 +2,21 @@ import subprocess import sys from pathlib import Path -from typing import List -from typing import Optional -from typing import Tuple +from typing import List, Optional, Tuple import i18n import requests import yaml -from . import config -from . import frozen_utils -from . import os_utils -from . import print_utils +from . import config, frozen_utils, os_utils, print_utils # The URL to the docker-compose.yml -BRAINFRAME_DOCKER_COMPOSE_URL = "https://{subdomain}aotu.ai/releases/brainframe/{version}/docker-compose.yml" +BRAINFRAME_DOCKER_COMPOSE_URL = ( + "https://{subdomain}aotu.ai/releases/brainframe/{version}/docker-compose.yml" +) # The URL to the latest tag, which is just a file containing the latest version # as a string -BRAINFRAME_LATEST_TAG_URL = ( - "https://{subdomain}aotu.ai/releases/brainframe/latest" -) +BRAINFRAME_LATEST_TAG_URL = "https://{subdomain}aotu.ai/releases/brainframe/latest" def assert_installed(install_path: Path) -> None: diff --git a/brainframe/cli/brainframe_kits.py b/brainframe/cli/brainframe_kits.py new file mode 100644 index 0000000..3d5e7ee --- /dev/null +++ b/brainframe/cli/brainframe_kits.py @@ -0,0 +1,135 @@ +import os +import shutil +import sys +from argparse import ArgumentParser +from pathlib import Path +from typing import List + +import i18n + +from brainframe.cli import brainframe_compose, dependencies, frozen_utils + +from . import os_utils, print_utils +from .brainframe_compose import assert_has_docker_permissions + +install_path = Path("/usr/local/share/brainframe-kits") +data_path = Path("/var/local/brainframe-kits") +db_path = Path(f"{data_path}/brainframe-kits") + + +def install(commands: List[str]): + args = _parse_args() + print(args) + + # Check all dependencies + dependencies.docker.ensure(args.noninteractive, args.install_docker) + # We only require the Docker Compose command in frozen distributions + if frozen_utils.is_frozen() and shutil.which("docker-compose") is None: + print_utils.fail_translate( + "install.install-dependency-manually", + dependency="docker-compose", + ) + + if not os_utils.added_to_group("docker"): + if args.noninteractive: + add_to_group = args.add_to_docker_group + else: + add_to_group = print_utils.ask_yes_no("install.ask-add-to-docker-group") + + if add_to_group: + os_utils.add_to_group("docker") + + install_path.mkdir(parents=True, exist_ok=True) + data_path.mkdir(parents=True, exist_ok=True) + db_path.mkdir(parents=True, exist_ok=True) + + # Set up permissions with the 'brainframe' group + print_utils.translate("install.create-group-justification") + os_utils.create_group("brainframe", os_utils.BRAINFRAME_GROUP_ID) + os_utils.give_brainframe_group_rw_access([data_path, install_path]) + + # Optionally add the user to the "brainframe" group + if not os_utils.added_to_group("brainframe"): + if args.noninteractive: + add_to_group = args.add_to_group + else: + add_to_group = print_utils.ask_yes_no("install.ask-add-to-group") + + if add_to_group: + os_utils.add_to_group("brainframe") + + script_dir = os.path.dirname(os.path.abspath(__file__)) + docker_compose_mgmt = os.path.join(script_dir, "docker-compose-kits.yml") + install_file = install_path.joinpath("docker-compose.yml") + shutil.copy(docker_compose_mgmt, install_file) + + print_utils.translate("install.downloading-images") + brainframe_compose.run(install_path, ["pull"]) + + print(f"The BrainFrame Kits was installed:") + print(f"\t\t\t\t1) deployment path: {install_path}") + print(f"\t\t\t\t2) data path: {data_path}") + + print_utils.translate("kits.description") + print_utils.translate("kits.usage") + + +def start(commands: List[str]): + brainframe_compose.run(install_path, commands) + + +def stop(commands: List[str]): + brainframe_compose.run(install_path, ["up", "-d"]) + + +def run(commands: List[str]) -> None: + assert_has_docker_permissions() + + if "install" in commands: + install(commands) + else: + assert_has_docker_permissions() + brainframe_compose.run(install_path, commands) + + +def _parse_args(): + parser = ArgumentParser( + description=i18n.t("kits.description"), + usage=i18n.t("kits.usage"), + ) + + parser.add_argument( + "--noninteractive", + action="store_true", + help=i18n.t("general.noninteractive-help"), + ) + parser.add_argument( + "--install-docker", + action="store_true", + help=i18n.t("install.install-docker-help"), + ) + parser.add_argument( + "--add-to-group", + action="store_true", + help=i18n.t("install.add-to-group-help"), + ) + parser.add_argument( + "--add-to-docker-group", + action="store_true", + help=i18n.t("install.add-to-docker-group-help"), + ) + parser.add_argument( + "--version", + type=str, + default="latest", + help=i18n.t("install.version-help"), + ) + + arg_list = sys.argv[3:] + args = parser.parse_args(arg_list) + + # Run in non-interactive mode if any flags were provided + if len(arg_list) > 0: + args.noninteractive = True + + return args diff --git a/brainframe/cli/brainframe_shell.py b/brainframe/cli/brainframe_shell.py index 4402493..49de106 100644 --- a/brainframe/cli/brainframe_shell.py +++ b/brainframe/cli/brainframe_shell.py @@ -7,8 +7,7 @@ import requests import yaml -from . import os_utils -from . import print_utils +from . import os_utils, print_utils from .brainframe_compose import assert_has_docker_permissions diff --git a/brainframe/cli/commands/__init__.py b/brainframe/cli/commands/__init__.py index bce28ce..d21579a 100644 --- a/brainframe/cli/commands/__init__.py +++ b/brainframe/cli/commands/__init__.py @@ -2,6 +2,7 @@ from .compose import compose from .info import info from .install import install +from .kits import kits from .self_update import self_update from .shell import shell from .uninstall import uninstall diff --git a/brainframe/cli/commands/backup.py b/brainframe/cli/commands/backup.py index cf96ec4..3717a1e 100644 --- a/brainframe/cli/commands/backup.py +++ b/brainframe/cli/commands/backup.py @@ -4,15 +4,11 @@ from pathlib import Path import i18n -from brainframe.cli import brainframe_compose -from brainframe.cli import config -from brainframe.cli import dependencies -from brainframe.cli import os_utils -from brainframe.cli import print_utils -from .utils import command -from .utils import requires_root -from .utils import subcommand_parse_args +from brainframe.cli import (brainframe_compose, config, dependencies, os_utils, + print_utils) + +from .utils import command, requires_root, subcommand_parse_args BACKUP_DIR_FORMAT = "%Y-%m-%d_%H-%M-%S" @@ -79,9 +75,7 @@ def _parse_args(data_path: Path): parser.add_argument( "--destination", type=Path, - help=i18n.t( - "backup.destination-help", backup_dir=data_path / "backups" - ), + help=i18n.t("backup.destination-help", backup_dir=data_path / "backups"), ) parser.add_argument( diff --git a/brainframe/cli/commands/compose.py b/brainframe/cli/commands/compose.py index 5728d9a..44de3a0 100644 --- a/brainframe/cli/commands/compose.py +++ b/brainframe/cli/commands/compose.py @@ -1,7 +1,6 @@ import sys -from brainframe.cli import brainframe_compose -from brainframe.cli import config +from brainframe.cli import brainframe_compose, config from .utils import command diff --git a/brainframe/cli/commands/info.py b/brainframe/cli/commands/info.py index da5fe65..18e6c99 100644 --- a/brainframe/cli/commands/info.py +++ b/brainframe/cli/commands/info.py @@ -1,12 +1,10 @@ from argparse import ArgumentParser import i18n -from brainframe.cli import brainframe_compose -from brainframe.cli import config -from brainframe.cli import print_utils -from .utils import command -from .utils import subcommand_parse_args +from brainframe.cli import brainframe_compose, config, print_utils + +from .utils import command, subcommand_parse_args @command("info") diff --git a/brainframe/cli/commands/install.py b/brainframe/cli/commands/install.py index 564023a..db672b5 100644 --- a/brainframe/cli/commands/install.py +++ b/brainframe/cli/commands/install.py @@ -3,16 +3,11 @@ from pathlib import Path import i18n -from brainframe.cli import brainframe_compose -from brainframe.cli import config -from brainframe.cli import dependencies -from brainframe.cli import frozen_utils -from brainframe.cli import os_utils -from brainframe.cli import print_utils -from .utils import command -from .utils import requires_root -from .utils import subcommand_parse_args +from brainframe.cli import (brainframe_compose, config, dependencies, + frozen_utils, os_utils, print_utils) + +from .utils import command, requires_root, subcommand_parse_args @command("install") @@ -45,9 +40,7 @@ def install(): if args.noninteractive: add_to_group = args.add_to_docker_group else: - add_to_group = print_utils.ask_yes_no( - "install.ask-add-to-docker-group" - ) + add_to_group = print_utils.ask_yes_no("install.ask-add-to-docker-group") if add_to_group: os_utils.add_to_group("docker") diff --git a/brainframe/cli/commands/kits.py b/brainframe/cli/commands/kits.py new file mode 100644 index 0000000..47f5785 --- /dev/null +++ b/brainframe/cli/commands/kits.py @@ -0,0 +1,24 @@ +import sys +from argparse import ArgumentParser + +import i18n + +from brainframe.cli import brainframe_kits + +from .utils import command, subcommand_parse_args + + +@command("kits") +def kits(): + if "-h" in sys.argv[2:]: + args = _parse_args() + else: + brainframe_kits.run(sys.argv[2:]) + + +def _parse_args(): + parser = ArgumentParser( + description=i18n.t("kits.description"), usage=i18n.t("kits.usage") + ) + + return subcommand_parse_args(parser) diff --git a/brainframe/cli/commands/self_update.py b/brainframe/cli/commands/self_update.py index fe7047e..2297257 100644 --- a/brainframe/cli/commands/self_update.py +++ b/brainframe/cli/commands/self_update.py @@ -5,18 +5,14 @@ from argparse import ArgumentParser from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Optional -from typing import Tuple -from typing import Union +from typing import Optional, Tuple, Union import i18n import requests -from brainframe.cli import __version__ -from brainframe.cli import config -from brainframe.cli import frozen_utils -from brainframe.cli import print_utils from packaging import version +from brainframe.cli import __version__, config, frozen_utils, print_utils + from .utils import command _RELEASES_URL_PREFIX = "https://{subdomain}aotu.ai" @@ -86,9 +82,7 @@ def self_update(): shutil.copy2(new_executable.name, executable_path) print() - print_utils.translate( - "self-update.complete", color=print_utils.Color.GREEN - ) + print_utils.translate("self-update.complete", color=print_utils.Color.GREEN) def _latest_version( diff --git a/brainframe/cli/commands/shell.py b/brainframe/cli/commands/shell.py index e26e3d3..5d95803 100644 --- a/brainframe/cli/commands/shell.py +++ b/brainframe/cli/commands/shell.py @@ -1,10 +1,10 @@ from argparse import ArgumentParser import i18n + from brainframe.cli import brainframe_shell -from .utils import command -from .utils import subcommand_parse_args +from .utils import command, subcommand_parse_args @command("shell") diff --git a/brainframe/cli/commands/uninstall.py b/brainframe/cli/commands/uninstall.py index 680dac6..8048fb3 100644 --- a/brainframe/cli/commands/uninstall.py +++ b/brainframe/cli/commands/uninstall.py @@ -2,14 +2,10 @@ from argparse import ArgumentParser import i18n -from brainframe.cli import brainframe_compose -from brainframe.cli import config -from brainframe.cli import os_utils -from brainframe.cli import print_utils - -from .utils import command -from .utils import requires_root -from .utils import subcommand_parse_args + +from brainframe.cli import brainframe_compose, config, os_utils, print_utils + +from .utils import command, requires_root, subcommand_parse_args @command("uninstall") diff --git a/brainframe/cli/commands/update.py b/brainframe/cli/commands/update.py index 5a27d26..1b9e55c 100644 --- a/brainframe/cli/commands/update.py +++ b/brainframe/cli/commands/update.py @@ -1,13 +1,11 @@ from argparse import ArgumentParser import i18n -from brainframe.cli import brainframe_compose -from brainframe.cli import config -from brainframe.cli import print_utils from packaging import version -from .utils import command -from .utils import subcommand_parse_args +from brainframe.cli import brainframe_compose, config, print_utils + +from .utils import command, subcommand_parse_args @command("update") @@ -23,9 +21,7 @@ def update(): else: requested_version_str = args.version - existing_version_str = brainframe_compose.check_existing_version( - install_path - ) + existing_version_str = brainframe_compose.check_existing_version(install_path) existing_version = version.parse(existing_version_str) requested_version = version.parse(requested_version_str) @@ -37,9 +33,7 @@ def update(): else: # Ask the user if downgrades should be allowed if existing_version >= requested_version: - force_downgrade = print_utils.ask_yes_no( - "update.ask-force-downgrade" - ) + force_downgrade = print_utils.ask_yes_no("update.ask-force-downgrade") if not force_downgrade: # Fail if the requested version is not an upgrade @@ -64,9 +58,7 @@ def update(): print_utils.translate("general.downloading-docker-compose") docker_compose_path = install_path / "docker-compose.yml" - brainframe_compose.download( - docker_compose_path, version=requested_version_str - ) + brainframe_compose.download(docker_compose_path, version=requested_version_str) brainframe_compose.run(install_path, ["pull"]) diff --git a/brainframe/cli/commands/utils.py b/brainframe/cli/commands/utils.py index ad5c5e7..f691622 100644 --- a/brainframe/cli/commands/utils.py +++ b/brainframe/cli/commands/utils.py @@ -1,11 +1,9 @@ import functools import sys from argparse import ArgumentParser -from typing import Any -from typing import Callable +from typing import Any, Callable -from brainframe.cli import os_utils -from brainframe.cli import print_utils +from brainframe.cli import os_utils, print_utils by_name = {} """A dict that maps command names to their corresponding function""" diff --git a/brainframe/cli/config.py b/brainframe/cli/config.py index e9b195a..8931030 100644 --- a/brainframe/cli/config.py +++ b/brainframe/cli/config.py @@ -1,18 +1,11 @@ import os from distutils.util import strtobool from pathlib import Path -from typing import Callable -from typing import Dict -from typing import Generic -from typing import Optional -from typing import Tuple -from typing import TypeVar -from typing import Union +from typing import Callable, Dict, Generic, Optional, Tuple, TypeVar, Union import yaml -from . import frozen_utils -from . import print_utils +from . import frozen_utils, print_utils T = TypeVar("T") @@ -38,9 +31,7 @@ def __init__(self, name: str): def env_var_name(self): return "BRAINFRAME_" + self.name.upper() - def load( - self, converter: Callable[[str], T], defaults: Dict[str, str] - ) -> None: + def load(self, converter: Callable[[str], T], defaults: Dict[str, str]) -> None: default = defaults.get(self.name) value: Optional[str] = os.environ.get(self.env_var_name, default) diff --git a/brainframe/cli/dependencies.py b/brainframe/cli/dependencies.py index e685648..84d69a5 100644 --- a/brainframe/cli/dependencies.py +++ b/brainframe/cli/dependencies.py @@ -3,8 +3,7 @@ import requests -from . import os_utils -from . import print_utils +from . import os_utils, print_utils class Dependency: @@ -28,9 +27,7 @@ def ensure(self, noninteractive: bool, install_requested: bool): """ # Only supported operating systems can request automatic installs if install_requested and not os_utils.is_supported(): - print_utils.fail_translate( - "install.install-dependency-unsupported-os" - ) + print_utils.fail_translate("install.install-dependency-unsupported-os") if shutil.which(self.command_name) is not None: # The command is already installed diff --git a/brainframe/cli/docker-compose-kits.yml b/brainframe/cli/docker-compose-kits.yml new file mode 100644 index 0000000..8a5143f --- /dev/null +++ b/brainframe/cli/docker-compose-kits.yml @@ -0,0 +1,38 @@ +services: + kits: + image: devaotuai/brainframe-kits:latest + restart: unless-stopped + privileged: true + environment: + DBMS_HOSTNAME: database + DBMS_USERNAME: user + DBMS_PASSWORD: password + DB_NAME: brainframe + DEPLOYMENT_MODE: "central" + VC_SERVER_HOST: "${DEFAULT_HOST_IP}" + VC_SERVER_PORT: 7090 + RTSP_PORT: 7554 + HLS_PORT: 7888 + MTX_PROTOCOLS: tcp + MTX_WEBRTCADDITIONALHOSTS: "${DEFAULT_HOST_IP:-127.0.0.1}" + ports: + - "7090:7090" + - "7554:7554" + - "7888:7888" + volumes: + - "/var/local/brainframe-kits:/persistent" + - "/dev/bus/usb:/dev/bus/usb" + - "/dev/dri:/dev/dri" + - "/etc/localtime:/etc/localtime" + depends_on: + - database + database: + image: postgres:15.3-alpine + restart: unless-stopped + environment: + POSTGRES_DB: "brainframe" + POSTGRES_USER: "user" + POSTGRES_PASSWORD: "password" + PGDATA: "/persistent/database" + volumes: + - "/var/local/brainframe-kits:/persistent" \ No newline at end of file diff --git a/brainframe/cli/frozen_utils.py b/brainframe/cli/frozen_utils.py index 186bacd..70cd0bc 100644 --- a/brainframe/cli/frozen_utils.py +++ b/brainframe/cli/frozen_utils.py @@ -20,9 +20,7 @@ def _get_absolute_path(*args: Union[str, Path]) -> Path: path = Path(_pyinstaller_tmp_path(), *args) if not path.exists(): - raise RuntimeError( - f"Missing resource in PyInstaller bundle: {args}" - ) + raise RuntimeError(f"Missing resource in PyInstaller bundle: {args}") return path else: @@ -31,9 +29,7 @@ def _get_absolute_path(*args: Union[str, Path]) -> Path: if path.exists(): return path - raise RuntimeError( - f"Could not find the absolute path for resource: {args}" - ) + raise RuntimeError(f"Could not find the absolute path for resource: {args}") def _pyinstaller_tmp_path() -> Path: diff --git a/brainframe/cli/main.py b/brainframe/cli/main.py index a5695b0..99cf06e 100644 --- a/brainframe/cli/main.py +++ b/brainframe/cli/main.py @@ -6,11 +6,9 @@ from argparse import ArgumentParser import i18n -from brainframe.cli import commands -from brainframe.cli import config -from brainframe.cli import frozen_utils -from brainframe.cli import os_utils -from brainframe.cli import print_utils + +from brainframe.cli import (commands, config, frozen_utils, os_utils, + print_utils) def main(): diff --git a/brainframe/cli/os_utils.py b/brainframe/cli/os_utils.py index ef2d0f1..0f6e40e 100644 --- a/brainframe/cli/os_utils.py +++ b/brainframe/cli/os_utils.py @@ -3,8 +3,7 @@ import sys from pathlib import Path from threading import RLock -from typing import List -from typing import Optional +from typing import List, Optional import distro import i18n @@ -58,9 +57,7 @@ def send_signal(self, sig: int): """ with self._lock: if self._process is None: - message = ( - "Attempted to send a signal when no process was running" - ) + message = "Attempted to send a signal when no process was running" raise RuntimeError(message) self._interrupted = True self._process.send_signal(sig) diff --git a/brainframe/cli/translations/kits.en.yml b/brainframe/cli/translations/kits.en.yml new file mode 100644 index 0000000..37d2a63 --- /dev/null +++ b/brainframe/cli/translations/kits.en.yml @@ -0,0 +1,11 @@ +en: + description: "Install and control the BrainFrame Kits" + usage: >- + brainframe kits + + Commands: + install Install the BrainFrame Kits + up -d Start the BrainFrame Kits + down Stop the BrainFrame Kits + restart Restart the BrainFrame Kits + logs -f Print the logs of the BrainFrame Kits diff --git a/brainframe/cli/translations/portal.en.yml b/brainframe/cli/translations/portal.en.yml index 7f48f49..0512ee3 100644 --- a/brainframe/cli/translations/portal.en.yml +++ b/brainframe/cli/translations/portal.en.yml @@ -12,6 +12,7 @@ en: compose Runs all following commands and flags through docker-compose uninstall Uninstalls the BrainFrame server shell Runs preinstalled brainframe-cli commands in a docker shell + kits Installs and runs preinstalled BrainFrame Kits Examples: brainframe install Installs BrainFrame interactively diff --git a/deployment/build.py b/deployment/build.py index 2501765..bd4d2c9 100644 --- a/deployment/build.py +++ b/deployment/build.py @@ -90,9 +90,7 @@ def clean(): def main(): - parser = argparse.ArgumentParser( - description="Build Python wheels or clean project" - ) + parser = argparse.ArgumentParser(description="Build Python wheels or clean project") parser.add_argument( "--clean", action="store_true", diff --git a/package/upload_binary.py b/package/upload_binary.py index 490b614..c6668b3 100644 --- a/package/upload_binary.py +++ b/package/upload_binary.py @@ -9,6 +9,7 @@ from pathlib import Path import boto3 + from brainframe.cli import __version__ from brainframe.cli.print_utils import fail