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},
|
||||
# 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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue