Skip to content

Commit 299f36a

Browse files
committed
1.1.0 - improvements for hosts and commands
1 parent 93e7ea2 commit 299f36a

File tree

7 files changed

+175
-92
lines changed

7 files changed

+175
-92
lines changed

lib/hemdal/config/alert.ex

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,10 @@ defmodule Hemdal.Config.Alert do
2020
- `command_args` is a list of arguments to be in use with the command.
2121
- `host` is the `Hemdal.Config.Host` data.
2222
- `command` is a nested structure including the command to be executed,
23-
the name of the command, and the command type. See
24-
`Hemdal.Config.Alert.Command`.
23+
the name of the command, and the command type. See `Hemdal.Config.Command`.
2524
- `notifiers` is a list of `Hemdal.Config.Notifier`.
2625
"""
27-
alias Hemdal.Config.{Host, Notifier}
26+
alias Hemdal.Config.{Command, Host, Notifier}
2827

2928
use Construct do
3029
field(:id, :string)
@@ -37,26 +36,7 @@ defmodule Hemdal.Config.Alert do
3736
field(:command_args, {:array, :string}, default: [])
3837

3938
field(:host, Host)
40-
41-
field :command do
42-
@moduledoc """
43-
The command structure is defining the command to be executed inside of
44-
each alert. The data used is:
45-
46-
- `name` the name of the script.
47-
- `type` the type of the script could be `line`, `script` or
48-
`interactive`. The first one let us define a command in a single line,
49-
the last one let us define a script, a multi-line code which will be
50-
copied and executed in the host, and the last one let us run a command
51-
interacting with a process for providing information in runtime.
52-
- `command` as the command line to be executed. It could be only one line
53-
or a multi-line script.
54-
"""
55-
field(:name, :string)
56-
field(:type, :string)
57-
field(:command, :string)
58-
end
59-
39+
field(:command, Command)
6040
field(:notifiers, {:array, Notifier}, default: [])
6141
end
6242
end

lib/hemdal/config/command.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule Hemdal.Config.Command do
2+
@moduledoc """
3+
The command structure is defining the command to be executed inside of
4+
each alert. The data used is:
5+
6+
- `name` the name of the script.
7+
- `type` the type of the script could be `line` or `script`.
8+
The first one let us define a command in a single line,
9+
the last one let us define a script, a multi-line code which will be
10+
copied and executed in the host, and the last one let us run a command
11+
interacting with a process for providing information in runtime.
12+
- `interactive` specify if the command is handled in an interactive way
13+
(default: false).
14+
- `output` specify if the output should be stored (default: true).
15+
- `command` as the command line to be executed. It could be only one line
16+
or a multi-line script.
17+
"""
18+
19+
use Construct do
20+
field(:name, :string)
21+
field(:type, :string)
22+
field(:interactive, :boolean, default: false)
23+
field(:output, :boolean, default: true)
24+
field(:command, :string)
25+
end
26+
end

lib/hemdal/host.ex

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule Hemdal.Host do
1515
If you want to know more about what could be included in a command or
1616
to be run in a host you can review the following modules:
1717
18-
- `Hemdal.Config.Alert.Command` where you can check what's included
18+
- `Hemdal.Config.Command` where you can check what's included
1919
inside of the command.
2020
- `Hemdal.Config.Host` where you can find information about the host.
2121
@@ -54,7 +54,7 @@ defmodule Hemdal.Host do
5454
"""
5555
use GenServer, restart: :transient
5656
require Logger
57-
alias Hemdal.Config.Alert.Command
57+
alias Hemdal.Config.Command
5858
alias :queue, as: Queue
5959

6060
@default_temporal_dir "/tmp"
@@ -99,6 +99,26 @@ defmodule Hemdal.Host do
9999
@typedoc false
100100
@type host_id() :: String.t()
101101

102+
@typedoc """
103+
The options for exec are providing to the execution valid information, and
104+
sometimes required information about the running process.
105+
106+
For example, if we want to run an interactive command but using a
107+
non-background execution, then we will need to provide `caller` option.
108+
109+
In addition, `args` are a list of arguments we pass to a script usually,
110+
and `timeout` makes sense mainly for non-interactive executions. Indeed,
111+
`timeout` is only applicable for the calling of the function, because
112+
it returns usually fast (depending on the loaded queue), it's not very
113+
useful for execution in background mode, and it could be dangerous in
114+
interactive and non-background mode.
115+
"""
116+
@type exec_opts() :: [
117+
timeout: timeout(),
118+
args: [String.t()],
119+
caller: pid()
120+
]
121+
102122
@doc """
103123
Run or execute the command passed as parameter. It's needed to pass the host ID
104124
to find the process where to send the request, and the the command and the
@@ -118,22 +138,24 @@ defmodule Hemdal.Host do
118138
If the JSON sent back from the command is valid, it's usually using that as
119139
data and it's marked as `UNKNOWN` status if there is no status defined.
120140
"""
121-
@spec exec(host_id(), Hemdal.Config.Alert.Command.t(), command_args()) ::
122-
{:ok, map()} | {:error, map()}
123-
@spec exec(host_id(), Hemdal.Config.Alert.Command.t(), command_args(), timeout()) ::
124-
{:ok, map()} | {:error, map()}
125-
def exec(host_id, cmd, args \\ [], timeout \\ @timeout_exec)
126-
127-
def exec(host_id, %Command{type: "line"} = cmd, _args, timeout) do
128-
GenServer.call(via(host_id), {:exec, cmd, []}, timeout)
129-
end
130-
131-
def exec(host_id, %Command{type: "script"} = cmd, args, timeout) do
132-
GenServer.call(via(host_id), {:exec, cmd, args}, timeout)
141+
@spec exec(host_id(), Hemdal.Config.Command.t(), exec_opts()) :: {:ok, map()} | {:error, map()}
142+
def exec(host_id, cmd, opts \\ [])
143+
144+
def exec(host_id, %Command{interactive: true} = cmd, opts) do
145+
if caller = opts[:caller] do
146+
timeout = opts[:timeout] || @timeout_exec
147+
args = opts[:args] || []
148+
GenServer.call(via(host_id), {:exec, caller, cmd, args}, timeout)
149+
else
150+
{:error, %{"message" => "Impossible combination"}}
151+
end
133152
end
134153

135-
def exec(host_id, %Command{type: "interactive"} = cmd, [pid], timeout) do
136-
GenServer.call(via(host_id), {:exec, cmd, [pid]}, timeout)
154+
def exec(host_id, %Command{} = cmd, opts) do
155+
caller = opts[:caller] || self()
156+
timeout = opts[:timeout] || @timeout_exec
157+
args = opts[:args] || []
158+
GenServer.call(via(host_id), {:exec, caller, cmd, args}, timeout)
137159
end
138160

139161
@doc """
@@ -145,10 +167,13 @@ defmodule Hemdal.Host do
145167
which process is in charge of running the background process and to know if it's
146168
still running or not.
147169
"""
148-
@spec exec_background(host_id(), Hemdal.Config.Alert.Command.t(), command_args()) ::
170+
@spec exec_background(host_id(), Hemdal.Config.Command.t(), exec_opts()) ::
149171
{:ok, pid()} | {:error, any()}
150-
def exec_background(host_id, cmd, args \\ []) do
151-
GenServer.call(via(host_id), {:exec_background, self(), cmd, args}, @timeout_exec)
172+
def exec_background(host_id, cmd, opts \\ []) do
173+
caller = opts[:caller] || self()
174+
timeout = opts[:timeout] || @timeout_exec
175+
args = opts[:args] || []
176+
GenServer.call(via(host_id), {:exec_background, caller, cmd, args}, timeout)
152177
end
153178

154179
@doc """
@@ -248,59 +273,58 @@ defmodule Hemdal.Host do
248273

249274
@impl GenServer
250275
@doc false
251-
def handle_call({:exec_background, pid, cmd, args}, _from, %__MODULE__{max_workers: :infinity} = state) do
276+
def handle_call({:exec_background, caller, cmd, args}, _from, %__MODULE__{max_workers: :infinity} = state) do
252277
Logger.debug("host => workers: #{state.workers}/infinity ; queue length: #{Queue.len(state.queue)}")
253278

254-
send_result = &send(pid, &1)
255-
ref = spawn_monitor(fn -> run_in_background(cmd, args, send_result, state) end)
256-
{:reply, {:ok, ref}, %__MODULE__{state | workers: state.workers + 1}}
279+
send_result = &send(caller, &1)
280+
{worker, _ref} = spawn_monitor(fn -> run_in_background(caller, cmd, args, send_result, state) end)
281+
{:reply, {:ok, worker}, %__MODULE__{state | workers: state.workers + 1}}
257282
end
258283

259284
def handle_call(
260-
{:exec_background, pid, cmd, args},
285+
{:exec_background, caller, cmd, args},
261286
from,
262287
%__MODULE__{max_workers: max_workers, workers: workers, queue: queue} = state
263288
)
264289
when workers >= max_workers do
265290
Logger.debug("host => workers: #{workers}/#{max_workers} ; queue length: #{Queue.len(queue) + 1}")
266291

267-
queue = Queue.in({{:exec_background, pid, cmd, args}, from}, queue)
292+
queue = Queue.in({{:exec_background, caller, cmd, args}, from}, queue)
268293
{:noreply, %__MODULE__{state | queue: queue}}
269294
end
270295

271-
def handle_call({:exec_background, pid, cmd, args}, _from, state) do
272-
send_result = &send(pid, &1)
273-
ref = spawn_monitor(fn -> run_in_background(cmd, args, send_result, state) end)
296+
def handle_call({:exec_background, caller, cmd, args}, _from, state) do
297+
send_result = &send(caller, &1)
298+
{worker, _ref} = spawn_monitor(fn -> run_in_background(caller, cmd, args, send_result, state) end)
274299
workers = state.workers + 1
275300

276301
Logger.debug("host => workers: #{workers}/#{state.max_workers} ; queue length: #{Queue.len(state.queue)}")
277302

278-
{:reply, {:ok, ref}, %__MODULE__{state | workers: workers}}
303+
{:reply, {:ok, worker}, %__MODULE__{state | workers: workers}}
279304
end
280305

281-
def handle_call({:exec, cmd, args}, from, %__MODULE__{max_workers: :infinity} = state) do
306+
def handle_call({:exec, caller, cmd, args}, from, %__MODULE__{max_workers: :infinity} = state) do
282307
Logger.debug("host => workers: #{state.workers}/infinity ; queue length: #{Queue.len(state.queue)}")
283308

284309
send_result = &GenServer.reply(from, &1)
285-
spawn_monitor(fn -> run_in_background(cmd, args, send_result, state) end)
310+
spawn_monitor(fn -> run_in_background(caller, cmd, args, send_result, state) end)
286311
{:noreply, %__MODULE__{state | workers: state.workers + 1}}
287312
end
288313

289314
def handle_call(
290-
{:exec, cmd, args},
315+
{:exec, caller, cmd, args},
291316
from,
292317
%__MODULE__{max_workers: max_workers, workers: workers, queue: queue} = state
293318
)
294319
when workers >= max_workers do
295320
Logger.debug("host => workers: #{workers}/#{max_workers} ; queue length: #{Queue.len(queue) + 1}")
296-
297-
queue = Queue.in({{:exec, cmd, args}, from}, queue)
321+
queue = Queue.in({{:exec, caller, cmd, args}, from}, queue)
298322
{:noreply, %__MODULE__{state | queue: queue}}
299323
end
300324

301-
def handle_call({:exec, cmd, args}, from, state) do
325+
def handle_call({:exec, caller, cmd, args}, from, state) do
302326
send_result = &GenServer.reply(from, &1)
303-
spawn_monitor(fn -> run_in_background(cmd, args, send_result, state) end)
327+
spawn_monitor(fn -> run_in_background(caller, cmd, args, send_result, state) end)
304328
workers = state.workers + 1
305329

306330
Logger.debug("host => workers: #{workers}/#{state.max_workers} ; queue length: #{Queue.len(state.queue)}")
@@ -347,20 +371,17 @@ defmodule Hemdal.Host do
347371
@doc false
348372
def handle_info({:DOWN, _ref, :process, _pid, _reason}, state) do
349373
case Queue.out(state.queue) do
350-
{:empty, _} ->
374+
{:empty, queue} ->
351375
workers = state.workers - 1
352376
Logger.debug("host => workers: #{workers}/#{state.max_workers} ; queue length: 0")
353-
{:noreply, %__MODULE__{state | workers: workers}}
377+
{:noreply, %__MODULE__{state | workers: workers, queue: queue}}
354378

355-
{{:value, {{:exec, cmd, args}, from}}, queue} ->
379+
{{:value, {{:exec, caller, cmd, args}, from}}, queue} ->
356380
state = %__MODULE__{state | queue: queue}
357381
send_result = &GenServer.reply(from, &1)
358-
spawn_monitor(fn -> run_in_background(cmd, args, send_result, state) end)
359-
360-
Logger.debug(
361-
"host => workers: #{state.workers}/#{state.max_workers} ; queue length: #{Queue.len(queue)}"
362-
)
363-
382+
spawn_monitor(fn -> run_in_background(caller, cmd, args, send_result, state) end)
383+
qlen = Queue.len(queue)
384+
Logger.debug("host => workers: #{state.workers}/#{state.max_workers} ; queue length: #{qlen}")
364385
{:noreply, state}
365386
end
366387
end
@@ -393,9 +414,9 @@ defmodule Hemdal.Host do
393414
{:error, %{"message" => other, "status" => "FAIL"}}
394415
end
395416

396-
defp run_in_background(cmd, args, send_result, %__MODULE__{host: %_{module: mod} = host}) do
417+
defp run_in_background(caller, cmd, args, send_result, %__MODULE__{host: %_{module: mod} = host}) do
397418
mod.transaction(host, fn handler ->
398-
with {:ok, errorlevel, output} <- exec_cmd(handler, mod, cmd, args),
419+
with {:ok, errorlevel, output} <- exec_cmd(handler, mod, caller, cmd, args),
399420
{:ok, %{"status" => status} = data} <- decode(output) do
400421
Logger.debug("command exit(#{errorlevel}) output: #{inspect(data)}")
401422
run_result(data, errorlevel, status)
@@ -432,11 +453,15 @@ defmodule Hemdal.Host do
432453
|> String.pad_leading(7, "0")
433454
end
434455

435-
defp exec_cmd(handler, mod, %Command{type: "line", command: command}, _args) do
456+
defp exec_cmd(handler, mod, _caller, %Command{type: "line", command: command, interactive: false}, _args) do
436457
mod.exec(handler, command)
437458
end
438459

439-
defp exec_cmd(handler, mod, %Command{type: "script", command: script}, args) do
460+
defp exec_cmd(handler, mod, caller, %Command{type: "line", command: command}, _args) do
461+
mod.exec_interactive(handler, command, caller)
462+
end
463+
464+
defp exec_cmd(handler, mod, _caller, %Command{type: "script", command: script, interactive: false}, args) do
440465
tmp_file = Path.join([@default_temporal_dir, random_string()])
441466

442467
try do
@@ -454,8 +479,18 @@ defmodule Hemdal.Host do
454479
end
455480
end
456481

457-
defp exec_cmd(handler, mod, %Command{type: "interactive", command: command}, [pid]) do
458-
mod.exec_interactive(handler, command, pid)
482+
defp exec_cmd(handler, mod, caller, %Command{type: "script", command: script}, args) do
483+
tmp_file = Path.join([@default_temporal_dir, random_string()])
484+
485+
sh =
486+
case String.split(script, ["\n"], trim: true) do
487+
["#!" <> shell | _] -> shell
488+
_ -> @default_shell
489+
end
490+
491+
mod.write_file(handler, tmp_file, script)
492+
cmd = Enum.join([sh, tmp_file | args], " ")
493+
mod.exec_interactive(handler, cmd, caller)
459494
end
460495

461496
@typedoc """

lib/hemdal/host/local.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ defmodule Hemdal.Host.Local do
1818
end
1919

2020
@impl Hemdal.Host
21-
def exec_interactive(_opts, command, pid) do
21+
def exec_interactive(_opts, command, caller) when is_pid(caller) do
2222
port = Port.open({:spawn, command}, [:binary])
23-
send(pid, {:start, self()})
24-
get_and_send_all(port, pid, "")
23+
send(caller, {:start, self()})
24+
get_and_send_all(port, caller, "")
2525
end
2626

2727
defp get_and_send_all(port, pid, output) do

mix.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Hemdal.MixProject do
22
use Mix.Project
33

4-
@version "1.0.5"
4+
@version "1.1.0"
55

66
def project do
77
[
@@ -82,7 +82,7 @@ defmodule Hemdal.MixProject do
8282
Configuration: [
8383
Hemdal.Config,
8484
Hemdal.Config.Alert,
85-
Hemdal.Config.Alert.Command,
85+
Hemdal.Config.Command,
8686
Hemdal.Config.Host,
8787
Hemdal.Config.Notifier,
8888
Hemdal.Config.Module,

test/hemdal/config_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ defmodule Hemdal.ConfigTest do
5353
module: Hemdal.Host.Local
5454
} = alert.host
5555

56-
assert %Hemdal.Config.Alert.Command{type: "line"} = alert.command
56+
assert %Hemdal.Config.Command{type: "line"} = alert.command
5757

5858
assert [%Hemdal.Config.Notifier{module: Hemdal.Notifier.Mock}] = alert.notifiers
5959

@@ -77,7 +77,7 @@ defmodule Hemdal.ConfigTest do
7777
assert "2f1cc590-624b-4246-b1d4-2bc97416b321" == alert.id
7878
assert "single valid check" == alert.name
7979
assert %Hemdal.Config.Host{module: Hemdal.Host.Local} = alert.host
80-
assert %Hemdal.Config.Alert.Command{type: "line"} = alert.command
80+
assert %Hemdal.Config.Command{type: "line"} = alert.command
8181

8282
assert [%Hemdal.Config.Notifier{module: Hemdal.Notifier.Mock}] = alert.notifiers
8383

0 commit comments

Comments
 (0)