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
|
# 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.
|
||||||
|
|
|
@ -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 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)
|
|
||||||
|
|
Reference in a new issue