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:
parent
980aa2a8ba
commit
427ee18714
5 changed files with 88 additions and 241 deletions
10
README.md
10
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.
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
86
tasks.py
86
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 <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.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)
|
||||
|
|
Reference in a new issue