defmodule Mix.Tasks.HostKit.Facts do
@moduledoc """
Collects bounded host facts through a HostKit target.
mix host_kit.facts [options] [config.exs]
Examples:
mix host_kit.facts --local --only os,users
mix host_kit.facts --host prod infra/config.exs --only os,systemd,ports
"""
use Mix.Task
alias Mix.Tasks.HostKit.Options
@shortdoc "Collect host facts"
@impl true
def run(args) do
Mix.Task.run("app.start")
{opts, positional} = parse!(args)
project = maybe_load_project(positional, opts)
Options.with_target_opts(opts, project, fn target_opts ->
facts_opts = target_opts |> Options.expand_target_opts() |> Keyword.put(:only, only(opts))
{:ok, facts} = HostKit.Facts.collect(facts_opts)
IO.puts(format_facts(facts, opts))
end)
end
defp parse!(args) do
OptionParser.parse!(args,
strict: [
local: :boolean,
host: :string,
remote: :string,
user: :string,
port: :integer,
identity_file: :string,
password: :string,
password_env: :string,
silently_accept_hosts: :boolean,
sudo: :boolean,
require: :keep,
format: :string,
only: :string
]
)
end
defp maybe_load_project(positional, opts) do
cond do
path = List.first(positional) ->
HostKit.load!(path, require: Keyword.get_values(opts, :require))
Keyword.has_key?(opts, :host) ->
HostKit.load!("infra/config.exs", require: Keyword.get_values(opts, :require))
true ->
nil
end
end
defp only(opts) do
opts
|> Keyword.get(:only, "os,users,systemd,ports")
|> String.split(",", trim: true)
|> Enum.map(fn name -> name |> String.trim() |> String.to_existing_atom() end)
rescue
ArgumentError ->
Mix.raise("invalid --only value, expected comma-separated os,users,systemd,ports")
end
defp format_facts(facts, opts) do
case Keyword.get(opts, :format, "text") do
"json" ->
Jason.encode_to_iodata!(facts, pretty: true)
"inspect" ->
inspect(facts, pretty: true, limit: :infinity, structs: true)
"text" ->
facts
|> Enum.map(&format_fact/1)
|> Enum.intersperse("\n")
|> IO.iodata_to_binary()
format ->
Mix.raise("unknown --format #{inspect(format)}, expected text, inspect, or json")
end
end
defp format_fact({:os, %{os_release: os_release, kernel: kernel}}) do
name = Map.get(os_release, "PRETTY_NAME") || Map.get(os_release, "NAME") || "unknown"
["os: ", name, " (", kernel || "unknown kernel", ")"]
end
defp format_fact({:users, users}) do
["users: ", Enum.map_join(users, ", ", & &1.name)]
end
defp format_fact({:systemd, systemd}) do
[
"systemd: ",
systemd.version || "unknown",
", failed_units=",
systemd.failed_units |> length() |> to_string()
]
end
defp format_fact({:ports, ports}) do
rendered = Enum.map_join(ports, ", ", fn port -> "#{port.address}:#{port.port}" end)
["ports: ", rendered]
end
end