diff --git a/README.md b/README.md index 6572d95..74bdaed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # spadinaistan +## Quoi? + +Spadinaistan is my personal cloud, which runs on an old laptop in my office. This code isn't intended to be used by +anyone else. + ## Services |Service|Description| @@ -8,7 +13,10 @@ |[Deluge](./services/deluge)|Deluge Web service| |[Traefik](./services/traefik)|Traefik API Gateway| |[Bitwarden](./services/bitwarden)|Bitwarden secrets management| +|[Auth](./services/auth-service)|Microservice handling authentication, gates access to certain resources.| ## Getting started -Use `. script/bootstrap` to set up the Python venv required to develop. +Use `. script/bootstrap` to set up the Python environment needed for the invoke and pyinfra tooling to work. + +This expects `pyenv` to be set up on your system. diff --git a/services/deluge/tasks.py b/services/deluge/tasks.py deleted file mode 100644 index 25a0619..0000000 --- a/services/deluge/tasks.py +++ /dev/null @@ -1,64 +0,0 @@ -import invoke -import pathlib -import typing -import json - -PATH = pathlib.Path(__file__).parent - - -@invoke.task() -def start(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose up -d") - - -@invoke.task() -def stop(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose down") - - -@invoke.task() -def restart(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose restart") - - -@invoke.task() -def healthcheck(ctx, as_json=False): - report = { - "containers_running": False, - } - - with ctx.cd(PATH): - healthy = True - # Check that the container is running - running_containers = ( - ctx.run("docker-compose ps --services --filter status=running", hide=True) - .stdout.strip() - .split() - ) - - if "deluge" in running_containers: - print("✅ Deluge container is running!") - report["containers_running"] = True - else: - print("❌ Deluge is not running, use inv deluge.start.") - healthy = False - - if healthy: - print("✅ Deluge is healthy!") - else: - print("❌ Deluge has problems") - - if as_json: - with open(pathlib.Path(PATH, "healthcheck.json"), "w") as outfile: - outfile.write(json.dumps(report)) - - -ns = invoke.Collection("deluge") - -ns.add_task(start) -ns.add_task(restart) -ns.add_task(stop) -ns.add_task(healthcheck) diff --git a/services/plex/tasks.py b/services/plex/tasks.py deleted file mode 100644 index 60d510c..0000000 --- a/services/plex/tasks.py +++ /dev/null @@ -1,140 +0,0 @@ -import invoke -import pathlib -import typing -import json -import os - - -PATH = pathlib.Path(__file__).parent - - -class BlockDevice(typing.TypedDict): - mountpoints: typing.List[str] - uuid: str - - -class ListBlockDevicesOutput(typing.TypedDict): - blockdevices: typing.List[BlockDevice] - - -class Configuration(typing.TypedDict): - mounts: typing.Dict[str, str] - - -def collect_mounted_resources( - lsblk_output: ListBlockDevicesOutput, -) -> typing.Dict[str, str]: - """ - Gathers a map of block device mountpoints to UUID to mountpoints from the - output of lsblk. - """ - block_devices = lsblk_output["blockdevices"] - mounted_resources: typing.Dict[str, str] = {} - - for device in block_devices: - if not all(device["mountpoints"]): - continue - - for mountpoint in device["mountpoints"]: - mounted_resources[mountpoint] = device["uuid"] - - return mounted_resources - - -def load_configuration() -> Configuration: - configuration_path = pathlib.Path(PATH, "config.json") - - with open(configuration_path, "r") as config_file: - config = config_file.read() - - config = json.loads(config) - - return config - - -@invoke.task() -def start(ctx): - with ctx.cd(PATH): - data_root = os.getenv("DATA_STORAGE_ROOT", PATH) - app_data_root = os.getenv("APP_STORAGE_ROOT", PATH) - - ctx.run( - f"PLEX_CLAIM=$(pass show plex-claim) docker-compose up -d", - env={"APP_STORAGE_ROOT": app_data_root, "DATA_STORAGE_ROOT": data_root}, - ) - - -@invoke.task() -def stop(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose down") - - -@invoke.task() -def restart(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose restart") - - -@invoke.task() -def healthcheck(ctx, as_json=False): - config = load_configuration() - - report = { - "containers_running": False, - "mounted_devices": False, - } - - with ctx.cd(PATH): - healthy = True - # Check that the container is running - running_containers = ( - ctx.run("docker-compose ps --services --filter status=running", hide=True) - .stdout.strip() - .split() - ) - - if "plex" in running_containers: - print("✅ Plex container is running!") - report["containers_running"] = True - else: - print("❌ Plex is not running, use inv plex.start.") - healthy = False - - lsblk_output = json.loads( - ctx.run("lsblk --json --output UUID,MOUNTPOINTS", hide=True).stdout.strip() - ) - - mounted_devices = collect_mounted_resources(lsblk_output) - - has_expected_mounts = True - for expected_mount, expected_uuid in config["mounts"].items(): - if mounted_devices.get(expected_mount) != expected_uuid: - print( - f"❌ Expected {expected_mount} to be a mountpoint for device {expected_uuid}. Mount the device to rectify." - ) - has_expected_mounts = False - - if has_expected_mounts: - print("✅ All expected mounted devices found") - report["mounted_devices"] = True - else: - print("❌ Some expected mounted devices are missing") - healthy = False - - if healthy: - print("✅ Plex is healthy!") - else: - print("❌ Plex has problems") - - if as_json: - with open(pathlib.Path(PATH, "healthcheck.json"), "w") as outfile: - outfile.write(json.dumps(report)) - - -ns = invoke.Collection("plex") - -ns.add_task(start) -ns.add_task(restart) -ns.add_task(stop) -ns.add_task(healthcheck) diff --git a/services/traefik/tasks.py b/services/traefik/tasks.py deleted file mode 100644 index 21111f9..0000000 --- a/services/traefik/tasks.py +++ /dev/null @@ -1,29 +0,0 @@ -import invoke -import pathlib - -PATH = pathlib.Path(__file__).parent - - -@invoke.task() -def start(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose up -d") - - -@invoke.task() -def stop(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose down") - - -@invoke.task() -def restart(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose restart") - - -ns = invoke.Collection("traefik") - -ns.add_task(start) -ns.add_task(restart) -ns.add_task(stop) diff --git a/tasks.py b/tasks.py index dd31d50..465f101 100644 --- a/tasks.py +++ b/tasks.py @@ -1,13 +1,13 @@ import invoke -import services.plex.tasks -import services.deluge.tasks -import services.traefik.tasks +import pathlib +import typing ns = invoke.Collection() PYINFRA_COMMON_PREFIX = "pyinfra -vvv pyinfra/inventory.py" + @invoke.task() def system_updates(ctx): ctx.run(f"{PYINFRA_COMMON_PREFIX} pyinfra/system_updates.py") @@ -18,13 +18,85 @@ def system_reboot(ctx): ctx.run(f"{PYINFRA_COMMON_PREFIX} pyinfra/reboot.py") +@invoke.task() +def start(context: invoke.context.Context, service: typing.Optional[str]): + """ + Starts a service. + + Services are assumed to be docker-compose-friendly and start as + docker-compose up -d. + + The supplied service name is used to pathing, with the expected + file structure being + / + /services + /service1 + docker-compose.yml + """ + + if service is None: + raise ValueError("Service name must be provided.") + + service_path = pathlib.Path("services", service) + + if not service_path.exists(): + raise ValueError(f"Service path does not exist: {service_path}") + + with context.cd(service_path): + context.run("docker-compose up --build --force-recreate -d") + + +@invoke.task() +def stop(context: invoke.context.Context, service: typing.Optional[str]): + """ + Stops a service. + + The same assumptions about file and service structure as made as with + `start `. + """ + + if service is None: + raise ValueError("Service name must be provided.") + + service_path = pathlib.Path("services", service) + + if not service_path.exists(): + raise ValueError(f"Service path does not exist: {service_path}") + + with context.cd(service_path): + context.run("docker-compose down") + + +@invoke.task() +def restart(context: invoke.context.Context, service: typing.Optional[str]): + """ + Restarts a service. + + The same assumptions about file and service structure as made as with + `start `. + """ + + if service is None: + raise ValueError("Service name must be provided.") + + service_path = pathlib.Path("services", service) + + if not service_path.exists(): + raise ValueError(f"Service path does not exist: {service_path}") + + with context.cd(service_path): + context.run("docker-compose restart") + + +services = invoke.Collection("services") +services.add_task(start) +services.add_task(stop) +services.add_task(restart) + server = invoke.Collection("server") server.add_task(system_updates, name="update") server.add_task(system_reboot, name="reboot") ns.add_collection(server) - -ns.add_collection(services.plex.tasks.ns) -ns.add_collection(services.traefik.tasks.ns) -ns.add_collection(services.deluge.tasks.ns) +ns.add_collection(services)