Add service list api

This commit is contained in:
Harshad Sharma 2024-05-16 20:41:00 +05:30
parent a103b10e10
commit e950e9fc60
6 changed files with 330 additions and 36 deletions

160
lib/freedive/api/command.ex Normal file
View file

@ -0,0 +1,160 @@
defmodule Freedive.Api.Command do
@moduledoc """
Freedive keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
require Logger
@doas_path "/usr/local/bin/doas"
@jexec_path "/usr/sbin/jexec"
@env_path "/usr/bin/env"
@env_path_var "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
@doc """
Execute commands, raise error on failure.
## Examples
iex> Freedive.execute!("whoami", [], doas: true)
"root"
iex> Freedive.execute!("whoami", [], doas: "www")
"www"
iex> Freedive.execute!("whoami", [], jail: "testjail")
"root"
iex> Freedive.execute!("whoami", [], jail: "testjail", doas: "operator")
"operator"
iex> Freedive.execute("hostname", [], jail: "testjail")
{:ok, "testjail"}
iex> Freedive.execute!("sysctl", ["-n", "security.jail.jailed"])
"0"
iex> Freedive.execute!("sysctl", ["-n", "security.jail.jailed"], jail: "testjail")
"1"
iex> Freedive.execute!("printenv", ["FOO"], jail: "testjail", env: [{"FOO", "bar"}])
"bar"
"""
def execute!(command, args, opts \\ []), do: raise_on_error(execute(command, args, opts))
@doc """
Execute commands, return {:ok, output} or {:error, {output, code}}.
## Examples
iex> Freedive.execute("whoami", [], doas: true)
{:ok, "root"}
iex> Freedive.execute("whoami", [], doas: "www")
{:ok, "www"}
iex> Freedive.execute("whoami", [], jail: "testjail")
{:ok, "root"}
iex> Freedive.execute("whoami", [], jail: "testjail", doas: "operator")
{:ok, "operator"}
iex> Freedive.execute("hostname", [], jail: "testjail")
{:ok, "testjail"}
iex> Freedive.execute("sysctl", ["-n", "security.jail.jailed"])
{:ok, "0"}
iex> Freedive.execute("sysctl", ["-n", "security.jail.jailed"], jail: "testjail")
{:ok, "1"}
iex> Freedive.execute("printenv", ["FOO"], jail: "testjail", env: [{"FOO", "bar"}])
{:ok, "bar"}
"""
@spec execute(String.t(), list(String.t()), Keyword.t()) ::
{:ok, String.t()} | {:error, {String.t(), integer()}}
def execute(command, args, opts \\ []) do
doas = Keyword.get(opts, :doas, false)
jail = Keyword.get(opts, :jail, nil)
envars =
Keyword.get(opts, :env, [
{"PATH", @env_path_var}
])
opts =
opts
|> Keyword.delete(:doas)
|> Keyword.delete(:jail)
|> Keyword.put(:stderr_to_stdout, true)
case {jail, doas} do
{nil, false} ->
cmd(command, args, opts)
{nil, true} ->
cmd(@doas_path, [command] ++ args, opts)
{nil, user} ->
cmd(@doas_path, ["-u", user, command] ++ args, opts)
{_, false} ->
cmd(
@doas_path,
["--", @jexec_path, "-l", jail, @env_path, "-P#{@env_path_var}", env(envars), command] ++
args,
opts
)
{_, true} ->
cmd(
@doas_path,
["--", @jexec_path, "-l", jail, @env_path, "-P#{@env_path_var}", env(envars), command] ++
args,
opts
)
{_, user} ->
cmd(
@doas_path,
[
"--",
@jexec_path,
"-l",
"-U",
user,
jail,
@env_path,
"-P#{@env_path_var}",
env(envars),
command
] ++ args,
opts
)
end
end
defp cmd(command, args, opts) do
# Logger.debug("Executing command: #{command} #{inspect(args)} with opts: #{inspect(opts)}")
case System.cmd(command, args, opts) do
{output, 0} -> {:ok, output |> String.trim()}
{output, code} -> {:error, {output |> String.trim(), code}}
end
end
defp env(envars) do
envars
|> Enum.map(fn {k, v} -> "#{k}=#{v}" end)
|> Enum.join(" ")
end
def raise_on_error(result) do
case result do
{:ok, output} -> output
{:error, message} -> raise message
end
end
end

View file

@ -0,0 +1,112 @@
defmodule Freedive.Api.Service.Cli do
@moduledoc """
Wraps `service` command to manage services on the host.
"""
require Logger
import Freedive.Api.Command
@service_bin "/usr/sbin/service"
def list_services() do
case execute(@service_bin, ["-l"]) do
{:ok, stdout} ->
service_names =
stdout
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.reject(
&Enum.member?(["DAEMON", "FILESYSTEMS", "LOGIN", "NETWORKING", "SERVERS"], &1)
)
case execute(@service_bin, ["-e"]) do
{:ok, stdout} ->
enabled_service_names =
stdout
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.map(&Path.basename/1)
|> Enum.into(%{}, &{&1, true})
services =
service_names
|> Enum.map(fn name ->
%{
name: name,
icon: "puzzle",
enabled: Map.has_key?(enabled_service_names, name),
running:
if Map.has_key?(enabled_service_names, name) do
service_is_running?(name)
else
nil
end,
description:
if Map.has_key?(enabled_service_names, name) do
case service_description(name) do
{:ok, desc} -> desc
_ -> nil
end
else
nil
end,
commands: nil,
rcvars: nil
}
end)
|> Enum.into(%{}, &{&1[:name], &1})
{:ok, services}
{:error, {stderr, _code}} ->
Logger.error("list_services enabled, log: #{inspect(stderr)}")
{:error, stderr}
end
{:error, {stderr, _code}} ->
Logger.error("list_services, log: #{inspect(stderr)}")
{:error, stderr}
end
end
def service_is_running?(name, args \\ []) do
case service(name, "onestatus", args) do
{:ok, _stdout} ->
true
{:error, {_stderr, _code}} ->
false
end
end
def service_is_enabled?(name, args \\ []) do
case service(name, "enabled", args) do
{:ok, _stdout} ->
true
{:error, {_stderr, _code}} ->
false
end
end
def service_description(name, args \\ []) do
case service(name, "onedescribe", args) do
{:ok, stdout} ->
stdout = String.trim(stdout)
{:ok, stdout}
{:error, {stderr, _code}} ->
{:error, stderr}
end
end
defp service(name, action, args) do
case execute(@service_bin, [name, action] ++ args, doas: true) do
{:ok, stdout} ->
# Logger.debug("service, log: #{inspect(stdout)}")
{:ok, stdout}
{:error, {stderr, code}} ->
{:error, {stderr, code}}
end
end
end

View file

@ -0,0 +1,36 @@
defmodule Freedive.Api.Service do
@moduledoc """
Provides API to manage services on the host.
"""
use GenServer
require Logger
import Freedive.Api.Service.Cli
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def list() do
GenServer.call(__MODULE__, {:list})
end
@impl true
def init(opts) do
state = %{opts: opts, services: []}
Logger.info("Starting Service.Server with opts: #{inspect(opts)}")
{:ok, state, {:continue, opts}}
end
@impl true
def handle_continue(_opts, state) do
{:ok, services} = list_services()
state = %{state | services: services}
{:noreply, state}
end
@impl true
def handle_call({:list}, _from, state) do
services = state[:services]
{:reply, services, state}
end
end

View file

@ -0,0 +1,18 @@
defmodule Freedive.Api.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(opts) do
children = [
Supervisor.child_spec({Freedive.Api.Service, opts},
id: Freedive.Api.Service
)
]
Supervisor.init(children, strategy: :one_for_one)
end
end

View file

@ -22,8 +22,8 @@ defmodule Freedive.Application do
{Phoenix.PubSub, name: Freedive.PubSub},
# Start the Finch HTTP client for sending emails
{Finch, name: Freedive.Finch},
# Start a worker by calling: Freedive.Worker.start_link(arg)
# {Freedive.Worker, arg},
# Start API supervisor
{Freedive.Api.Supervisor, []},
# Start to serve requests, typically the last entry
FreediveWeb.Endpoint,
Freedive.Features

View file

@ -1,5 +1,6 @@
defmodule FreediveWeb.ServiceLive do
use FreediveWeb.LiliformLive
alias Freedive.Api.Service
def render(assigns) do
~H"""
@ -14,40 +15,7 @@ defmodule FreediveWeb.ServiceLive do
end
def items() do
%{
"sshd" => %{
name: "sshd",
path: "/services/ssh",
icon: "lock",
description: "Secure Shell Daemon",
enabled: true,
running: true
},
"pf" => %{
name: "pf",
path: "/services/pf",
icon: "shield",
description: "Packet Filter",
enabled: true,
running: true
},
"ntpdate" => %{
name: "ntpdate",
path: "/services/ntp",
icon: "clock",
description: "Network Time Protocol Daemon",
enabled: true,
running: false
},
"httpd" => %{
name: "httpd",
path: "/services/httpd",
icon: "globe",
description: "Hypertext Transfer Protocol Daemon",
enabled: false,
running: false
}
}
Service.list()
end
def filters() do