refactor: hoist start/stop/restart to top-level (#21)

* refactor: hoist start/stop/restart to top-level

* docs: README updates
This commit is contained in:
Marc 2023-05-28 14:39:27 -04:00
parent 980aa2a8ba
commit 427ee18714
Signed by: marc
GPG key ID: 048E042F22B5DC79
5 changed files with 88 additions and 241 deletions

View file

@ -1,5 +1,10 @@
# spadinaistan # 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 ## Services
|Service|Description| |Service|Description|
@ -8,7 +13,10 @@
|[Deluge](./services/deluge)|Deluge Web service| |[Deluge](./services/deluge)|Deluge Web service|
|[Traefik](./services/traefik)|Traefik API Gateway| |[Traefik](./services/traefik)|Traefik API Gateway|
|[Bitwarden](./services/bitwarden)|Bitwarden secrets management| |[Bitwarden](./services/bitwarden)|Bitwarden secrets management|
|[Auth](./services/auth-service)|Microservice handling authentication, gates access to certain resources.|
## Getting started ## 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.

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -1,13 +1,13 @@
import invoke import invoke
import services.plex.tasks import pathlib
import services.deluge.tasks import typing
import services.traefik.tasks
ns = invoke.Collection() ns = invoke.Collection()
PYINFRA_COMMON_PREFIX = "pyinfra -vvv pyinfra/inventory.py" PYINFRA_COMMON_PREFIX = "pyinfra -vvv pyinfra/inventory.py"
@invoke.task() @invoke.task()
def system_updates(ctx): def system_updates(ctx):
ctx.run(f"{PYINFRA_COMMON_PREFIX} pyinfra/system_updates.py") 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") 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 <service>`.
"""
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 <service>`.
"""
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 = invoke.Collection("server")
server.add_task(system_updates, name="update") server.add_task(system_updates, name="update")
server.add_task(system_reboot, name="reboot") server.add_task(system_reboot, name="reboot")
ns.add_collection(server) ns.add_collection(server)
ns.add_collection(services)
ns.add_collection(services.plex.tasks.ns)
ns.add_collection(services.traefik.tasks.ns)
ns.add_collection(services.deluge.tasks.ns)