159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
"""Finite ordinary-cookie metadata queries; no queue/user mutation or message reads."""
|
|
|
|
import argparse
|
|
import json
|
|
import stat
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from . import broker_snapshot, operator_units, preflight
|
|
from . import dependency_policy as dependencies
|
|
from .policy import DeliveryError, canonical, fields, integer
|
|
|
|
CTL = broker_snapshot.CTL
|
|
|
|
|
|
def queue_arguments(value) -> dict:
|
|
if isinstance(value, dict):
|
|
result = dict(value)
|
|
elif isinstance(value, list) and len(value) in (4, 5):
|
|
result = {}
|
|
for row in value:
|
|
if not isinstance(row, list) or len(row) != 3 or row[0] in result:
|
|
raise DeliveryError("duplicate/unknown broker argument")
|
|
name, kind, argument = row
|
|
if name not in {*dependencies.QUEUE_ARGUMENTS, "x-queue-type"} or kind != (
|
|
"longstr" if name in {"x-overflow", "x-queue-type"} else "long"
|
|
):
|
|
raise DeliveryError("broker argument type differs")
|
|
result[name] = argument
|
|
else:
|
|
raise DeliveryError("broker argument shape differs")
|
|
if "x-queue-type" in result and result.pop("x-queue-type") != "classic":
|
|
raise DeliveryError("actual broker queue type differs")
|
|
if canonical(result) != canonical(dependencies.QUEUE_ARGUMENTS):
|
|
raise DeliveryError("actual broker queue budgets differ")
|
|
return result
|
|
|
|
|
|
def validate_snapshot(value: dict) -> dict:
|
|
fields(value, {"queues", "vhost_limits", "users", "permissions"}, "broker snapshot")
|
|
rows = value["queues"]
|
|
if not isinstance(rows, list) or len(rows) != 1:
|
|
raise DeliveryError("actual broker queue inventory differs")
|
|
queue = fields(
|
|
rows[0],
|
|
{
|
|
"name",
|
|
"durable",
|
|
"auto_delete",
|
|
"exclusive",
|
|
"type",
|
|
"state",
|
|
"arguments",
|
|
"messages_ready",
|
|
"messages_unacknowledged",
|
|
"message_bytes",
|
|
"consumers",
|
|
"policy",
|
|
"operator_policy",
|
|
"effective_policy_definition",
|
|
},
|
|
"broker queue",
|
|
)
|
|
if (
|
|
queue["name"] != dependencies.QUEUE
|
|
or queue["durable"] is not True
|
|
or queue["auto_delete"] is not False
|
|
or queue["exclusive"] is not False
|
|
or queue["type"] != "classic"
|
|
or queue["state"] != "running"
|
|
or queue["policy"] not in (None, "")
|
|
or queue["operator_policy"] not in (None, "")
|
|
or queue["effective_policy_definition"] not in ({}, [])
|
|
):
|
|
raise DeliveryError("actual broker queue semantics differ")
|
|
queue_arguments(queue["arguments"])
|
|
limits = value["vhost_limits"]
|
|
if not isinstance(limits, list) or len(limits) != 1:
|
|
raise DeliveryError("actual broker vhost limits unavailable")
|
|
row = fields(limits[0], {"vhost", "limits"}, "broker vhost limits")
|
|
if row["vhost"] != "kf" or not isinstance(row["limits"], str):
|
|
raise DeliveryError("actual broker vhost binding differs")
|
|
if canonical(operator_units.decode(row["limits"].encode(), 1024)) != canonical(
|
|
{"max-connections": 4, "max-queues": 1}
|
|
):
|
|
raise DeliveryError("actual broker vhost budgets differ")
|
|
users = value["users"]
|
|
if (
|
|
not isinstance(users, list)
|
|
or len(users) != 2
|
|
or canonical(sorted(users, key=lambda u: u.get("user", "")))
|
|
!= canonical(
|
|
[
|
|
{"user": "kf_outbox", "tags": []},
|
|
{"user": "kf_worker", "tags": []},
|
|
]
|
|
)
|
|
):
|
|
raise DeliveryError("actual broker users/tags differ")
|
|
permissions = value["permissions"]
|
|
expected = [
|
|
{
|
|
"user": "kf_" + role,
|
|
"configure": r"^kf\.judge\.evaluations\.v1$",
|
|
"write": r"^(amq\.default|kf\.judge\.evaluations\.v1)$" if role == "outbox" else "^$",
|
|
"read": r"^kf\.judge\.evaluations\.v1$" if role == "worker" else "^$",
|
|
}
|
|
for role in ("outbox", "worker")
|
|
]
|
|
if (
|
|
not isinstance(permissions, list)
|
|
or len(permissions) != 2
|
|
or canonical(sorted(permissions, key=lambda u: u.get("user", ""))) != canonical(expected)
|
|
):
|
|
raise DeliveryError("actual broker authority differs")
|
|
return {
|
|
"schema_version": 1,
|
|
"metadata_verified": True,
|
|
"messages_ready": integer(queue["messages_ready"], 0, 64, "broker ready count"),
|
|
"messages_unacknowledged": integer(
|
|
queue["messages_unacknowledged"], 0, 1, "broker unacknowledged count"
|
|
),
|
|
"message_bytes": integer(queue["message_bytes"], 0, 32768 + 4096, "broker bytes"),
|
|
"consumers": integer(queue["consumers"], 0, 1, "broker consumer count"),
|
|
}
|
|
|
|
|
|
def observe() -> dict:
|
|
operator_units.guarded("broker-health")
|
|
preflight.protected(CTL)
|
|
cookie = Path(dependencies.STORAGE["rabbitmq"]["path"]) / ".erlang.cookie"
|
|
info = cookie.lstat()
|
|
if (
|
|
not stat.S_ISREG(info.st_mode)
|
|
or info.st_nlink != 1
|
|
or (info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode)) != (1204, 1204, 0o400)
|
|
):
|
|
raise DeliveryError("ordinary private broker cookie required")
|
|
return validate_snapshot(broker_snapshot.command())
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Bounded ordinary broker metadata")
|
|
parser.add_argument("--inside", action="store_true", required=True)
|
|
parser.parse_args()
|
|
try:
|
|
print(json.dumps(observe(), sort_keys=True))
|
|
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
|
parser.exit(
|
|
1,
|
|
"Broker health refused: "
|
|
+ (str(error) if isinstance(error, DeliveryError) else type(error).__name__)
|
|
+ "\n",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|