defmodule HAHandler.DRBD do require Logger @supervisor HAHandler.DRBD.Supervisor # There might be >1 resources configured in DRBD! @default_resource_id "1" # We don't support DRBD 9 for the time being, as /proc/drbd does not have a # stable API. @supported_drbd_major_version "8" # Parsing of /proc/drbd, assuming DRBD 8. Splitting the regexes helps humans # wrapping their head around what's going on. And yes, it's fragile: we need # drbd 9 to get a JSON interface to `drbdadm status`. @drbd_proc_cmd "cat /proc/drbd" @block_regex ~r/(?(.|\n)*)\n(?\n\s(.|\n)*)/ @version_regex ~r/version: (?(?\d+)\.(?\d+)\.(?\d))/ @resource_split_regex ~r{(\n\s(\d+)\:\s)} @id_extraction_regex ~r/\n\s(?\d+)\:\s/ @data_extraction_regex ~r/cs:(?(\w|\/)+)\sro:(?(\w|\/)+)\sds:(?(\w|\/)+)\s/ # Empty state, when backend is not queryable for some reason. @empty_state %{ hostname: "unknown", version: "", mode: "", status: "unknown", data: "" } def get_instances() do watchers = Supervisor.which_children(@supervisor) for {hostname, pid, _type, _modules} <- watchers do {hostname, pid} end end def get_stats() do get_instances() |> Enum.map(fn instance -> get_state(instance) end) end def get_state({hostname, pid}) do empty_reply = %{@empty_state | hostname: hostname} case GenServer.call(pid, {:execute, @drbd_proc_cmd}) do {:ok, raw, 0} -> case Regex.named_captures(@block_regex, raw) do %{"version_block" => version_block, "resource_block" => resource_block} -> version = Regex.named_captures(@version_regex, version_block) if Map.get(version, "major") != @supported_drbd_major_version do {:error, "unsupported DRBD version #{inspect(version)}"} else resources = Regex.split( @resource_split_regex, resource_block, [include_captures: true, trim: true]) |> Enum.chunk_every(2) |> Enum.map(fn [raw_id, raw_data] -> %{} |> Map.merge(Regex.named_captures(@id_extraction_regex, raw_id)) |> Map.merge(Regex.named_captures(@data_extraction_regex, raw_data)) end) default_resource = resources |> Enum.filter(fn r -> r["id"] == @default_resource_id end) |> Enum.at(0) processed_reply = %{ version: Map.get(version, "full"), mode: Map.get(default_resource, "ro"), status: Map.get(default_resource, "ds"), data: resources } Map.merge(empty_reply, processed_reply) end _ -> Logger.warning("Failed to query DRBD backend: could not parse /proc/drbd.") end {:ok, _, posix_err} -> Logger.warning("Failed to query DRBD backend: POSIX #{inspect(posix_err)}.") empty_reply {:error, err} -> Logger.warning("Failed to query DRBD backend: #{inspect(err)}.") empty_reply end end end