forked from hiway/freedive
Add service list api
This commit is contained in:
parent
a103b10e10
commit
e950e9fc60
6 changed files with 330 additions and 36 deletions
160
lib/freedive/api/command.ex
Normal file
160
lib/freedive/api/command.ex
Normal 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
|
112
lib/freedive/api/service/cli.ex
Normal file
112
lib/freedive/api/service/cli.ex
Normal 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
|
36
lib/freedive/api/service/server.ex
Normal file
36
lib/freedive/api/service/server.ex
Normal 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
|
18
lib/freedive/api/supervisor.ex
Normal file
18
lib/freedive/api/supervisor.ex
Normal 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
|
|
@ -22,8 +22,8 @@ defmodule Freedive.Application do
|
||||||
{Phoenix.PubSub, name: Freedive.PubSub},
|
{Phoenix.PubSub, name: Freedive.PubSub},
|
||||||
# Start the Finch HTTP client for sending emails
|
# Start the Finch HTTP client for sending emails
|
||||||
{Finch, name: Freedive.Finch},
|
{Finch, name: Freedive.Finch},
|
||||||
# Start a worker by calling: Freedive.Worker.start_link(arg)
|
# Start API supervisor
|
||||||
# {Freedive.Worker, arg},
|
{Freedive.Api.Supervisor, []},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
FreediveWeb.Endpoint,
|
FreediveWeb.Endpoint,
|
||||||
Freedive.Features
|
Freedive.Features
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule FreediveWeb.ServiceLive do
|
defmodule FreediveWeb.ServiceLive do
|
||||||
use FreediveWeb.LiliformLive
|
use FreediveWeb.LiliformLive
|
||||||
|
alias Freedive.Api.Service
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
@ -14,40 +15,7 @@ defmodule FreediveWeb.ServiceLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def items() do
|
def items() do
|
||||||
%{
|
Service.list()
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def filters() do
|
def filters() do
|
||||||
|
|
Loading…
Reference in a new issue