Complete coverage of V1 and V2 APIs.
This commit is contained in:
parent
833cf1745f
commit
371bb6c012
9 changed files with 202 additions and 53 deletions
|
@ -53,5 +53,5 @@ COPY mix.exs mix.lock /app/
|
|||
RUN mix deps.get
|
||||
RUN mix deps.compile
|
||||
|
||||
COPY . /app
|
||||
COPY . /app/
|
||||
CMD mix run --no-halt
|
||||
|
|
|
@ -4,9 +4,7 @@ defmodule BalenaDevice.Application do
|
|||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
BalenaDevice.LocalLogStream
|
||||
]
|
||||
children = []
|
||||
|
||||
opts = [strategy: :one_for_one, name: BalenaDevice.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
defmodule BalenaDevice.Error do
|
||||
defexception ~w[message]a
|
||||
@moduledoc false
|
||||
end
|
||||
|
|
|
@ -2,10 +2,16 @@ defmodule BalenaDevice.HTTP do
|
|||
use HTTPoison.Base
|
||||
alias BalenaDevice.Error
|
||||
|
||||
@moduledoc """
|
||||
Uses `HTTPoison.Base` to construct requests for the on-device supervisor.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
def process_request_url(url) do
|
||||
"#{supervisor_address()}#{url}?apikey=#{supervisor_api_key()}"
|
||||
end
|
||||
|
||||
@doc false
|
||||
def process_request_headers(headers) do
|
||||
headers
|
||||
|> Enum.into(%{})
|
||||
|
|
46
lib/balena_device/journald_log_stream.ex
Normal file
46
lib/balena_device/journald_log_stream.ex
Normal file
|
@ -0,0 +1,46 @@
|
|||
defmodule BalenaDevice.JournaldLogStream do
|
||||
use GenServer
|
||||
alias BalenaDevice.HTTP
|
||||
|
||||
@moduledoc """
|
||||
A simple `GenServer` which handles streaming journald logs from the
|
||||
Supervisor. You should not call functions in this module directly - this
|
||||
process is started by `BalenaDevice.V2.journald_logs/1`.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
def start_link(options), do: GenServer.start_link(__MODULE__, options)
|
||||
|
||||
@impl true
|
||||
def init(options) do
|
||||
body = options |> Keyword.get(:body, "")
|
||||
receiver = options |> Keyword.get(:receiver)
|
||||
|
||||
{:ok, conn} =
|
||||
HTTP.post("/v2/journal-logs", body, [],
|
||||
stream_to: self(),
|
||||
timeout: :infinity,
|
||||
recv_timeout: :infinity
|
||||
)
|
||||
|
||||
state = %{
|
||||
conn: conn,
|
||||
receiver: receiver
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%HTTPoison.AsyncStatus{}, state), do: {:noreply, state}
|
||||
def handle_info(%HTTPoison.AsyncHeaders{}, state), do: {:noreply, state}
|
||||
|
||||
def handle_info(%HTTPoison.AsyncChunk{chunk: chunk}, %{receiver: receiver} = state) do
|
||||
send(receiver, {:journald_log, chunk})
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(%HTTPoison.Error{reason: reason}, state), do: {:stop, reason, state}
|
||||
|
||||
def handle_info(%HTTPoison.AsyncEnd{}, state), do: {:stop, :normal, state}
|
||||
end
|
|
@ -3,68 +3,39 @@ defmodule BalenaDevice.LocalLogStream do
|
|||
alias BalenaDevice.HTTP
|
||||
|
||||
@moduledoc """
|
||||
Implements a `GenServer` that when there are subscribers connects to the
|
||||
supervisor API and retrieves logs and sending them to the subscribers.
|
||||
A simple `GenServer` which handles streaming local device logs from the
|
||||
Supervisor. You should not call functions in this module directly - this
|
||||
process is started by `BalenaDevice.V2.local_log_stream/0`.
|
||||
"""
|
||||
|
||||
def start_link(_), do: GenServer.start_link(__MODULE__, [], hibernate_after: 5_000)
|
||||
@doc false
|
||||
def start_link(options), do: GenServer.start_link(__MODULE__, options)
|
||||
|
||||
@impl true
|
||||
def init(_), do: {:ok, %{subscribers: [], connection: nil}, :hibernate}
|
||||
def init(options) do
|
||||
receiver = options |> Keyword.get(:receiver)
|
||||
|
||||
@impl true
|
||||
def handle_cast({:subscribe, pid}, %{subscribers: subscribers, connection: nil} = state) do
|
||||
Process.monitor(pid)
|
||||
{:ok, connection} = start_connection()
|
||||
{:noreply, %{state | subscribers: [pid | subscribers], connection: connection}}
|
||||
end
|
||||
{:ok, conn} =
|
||||
HTTP.get("/v2/local/logs", [],
|
||||
stream_to: self(),
|
||||
timeout: :infinity,
|
||||
recv_timeout: :infinity
|
||||
)
|
||||
|
||||
def handle_cast({:subscribe, pid}, %{subscribers: subscribers} = state) do
|
||||
Process.monitor(pid)
|
||||
{:noreply, %{state | subscribers: [pid | subscribers]}}
|
||||
state = %{conn: conn, receiver: receiver}
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%HTTPoison.AsyncStatus{}, state), do: {:noreply, state}
|
||||
def handle_info(%HTTPoison.AsyncHeaders{}, state), do: {:noreply, state}
|
||||
|
||||
def handle_info(%HTTPoison.AsyncChunk{chunk: chunk}, %{subscribers: subscribers} = state) do
|
||||
chunk = Jason.decode!(chunk)
|
||||
Enum.each(subscribers, &send(&1, chunk))
|
||||
def handle_info(%HTTPoison.AsyncChunk{chunk: chunk}, %{receiver: receiver} = state) do
|
||||
send(receiver, {:local_log, chunk})
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(%HTTPoison.Error{}, %{subscribers: []} = state) do
|
||||
{:noreply, %{state | connection: nil}}
|
||||
end
|
||||
def handle_info(%HTTPoison.Error{reason: reason}, state), do: {:stop, reason, state}
|
||||
|
||||
def handle_info(%HTTPoison.Error{}, %{subscribers: subscribers} = state) do
|
||||
{:ok, connection} = start_connection()
|
||||
{:noreply, %{state | connection: connection}}
|
||||
end
|
||||
|
||||
def handle_info(%HTTPoison.AsyncEnd{}, %{subscribers: []} = state) do
|
||||
{:noreply, %{state | connection: nil}}
|
||||
end
|
||||
|
||||
def handle_info(%HTTPoison.AsyncEnd{}, %{subscribers: subscribers} = state) do
|
||||
{:ok, connection} = start_connection()
|
||||
{:noreply, %{state | connection: connection}}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:DOWN, _ref, :process, pid, _reason},
|
||||
%{subscribers: subscribers, connection: connection} = state
|
||||
) do
|
||||
subscribers = subscribers |> List.delete(pid)
|
||||
{:noreply, %{state | subscribers: subscribers}}
|
||||
end
|
||||
|
||||
defp start_connection,
|
||||
do:
|
||||
HTTP.get("/v2/local/logs", [],
|
||||
stream_to: self(),
|
||||
timeout: :infinity,
|
||||
recv_timeout: :infinity
|
||||
)
|
||||
def handle_info(%HTTPoison.AsyncEnd{}, state), do: {:stop, :normal, state}
|
||||
end
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
defmodule BalenaDevice.Utils do
|
||||
@moduledoc false
|
||||
|
||||
@doc false
|
||||
def underscore_map_keys(value) when is_list(value) do
|
||||
value
|
||||
|> Enum.map(&underscore_map_keys(&1))
|
||||
|
|
|
@ -10,6 +10,11 @@ defmodule BalenaDevice.V1 do
|
|||
|
||||
@doc """
|
||||
Request a device identification by blinking the device's LED for 15 seconds.
|
||||
|
||||
### Example
|
||||
|
||||
iex> V1.blink()
|
||||
:ok
|
||||
"""
|
||||
@spec blink() :: :ok | {:error, term}
|
||||
def blink do
|
||||
|
@ -27,6 +32,11 @@ defmodule BalenaDevice.V1 do
|
|||
|
||||
@doc """
|
||||
Trigger an update check on the supervisor.
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.update()
|
||||
:ok
|
||||
"""
|
||||
@spec update() :: :ok | {:error, term}
|
||||
def update, do: update(false)
|
||||
|
@ -34,6 +44,11 @@ defmodule BalenaDevice.V1 do
|
|||
@doc """
|
||||
Trigger an update check on the supervisor. Optionally, forces an update when
|
||||
updates are locked.
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.update(true)
|
||||
:ok
|
||||
"""
|
||||
@spec update(boolean) :: :ok | {:error, term}
|
||||
def update(force) do
|
||||
|
@ -52,6 +67,11 @@ defmodule BalenaDevice.V1 do
|
|||
@doc """
|
||||
Reboots the device. This will first try to stop applications, and fail if
|
||||
there is an update lock.
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.reboot()
|
||||
:ok
|
||||
"""
|
||||
@spec reboot() :: :ok | {:error, term}
|
||||
def reboot, do: reboot(false)
|
||||
|
@ -60,6 +80,11 @@ defmodule BalenaDevice.V1 do
|
|||
Reboots the device. This will first try to stop applications, and fail if
|
||||
there is an update lock. An optional "force" parameter in the body overrides
|
||||
the lock when true (and the lock can also be overridden from the dashboard).
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.reboot(true)
|
||||
:ok
|
||||
"""
|
||||
@spec reboot(boolean) :: :ok | {:error, term}
|
||||
def reboot(force) do
|
||||
|
@ -78,6 +103,11 @@ defmodule BalenaDevice.V1 do
|
|||
@doc """
|
||||
*Dangerous*. Shuts down the device. This will first try to stop applications,
|
||||
and fail if there is an update lock.
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.shutdown()
|
||||
:ok
|
||||
"""
|
||||
@spec shutdown() :: :ok | {:error, term}
|
||||
def shutdown, do: shutdown(false)
|
||||
|
@ -87,6 +117,11 @@ defmodule BalenaDevice.V1 do
|
|||
and fail if there is an update lock. An optional "force" parameter in the body
|
||||
overrides the lock when true (and the lock can also be overridden from the
|
||||
dashboard).
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.shutdown(true)
|
||||
:ok
|
||||
"""
|
||||
@spec shutdown(boolean) :: :ok | {:error, term}
|
||||
def shutdown(force) do
|
||||
|
@ -104,6 +139,11 @@ defmodule BalenaDevice.V1 do
|
|||
|
||||
@doc """
|
||||
Clears the user application's /data folder.
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.purge(1)
|
||||
:ok
|
||||
"""
|
||||
@spec purge(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||
def purge(app_id) when is_app_id(app_id) do
|
||||
|
@ -121,6 +161,11 @@ defmodule BalenaDevice.V1 do
|
|||
|
||||
@doc """
|
||||
Restarts a user application container
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.restart(1)
|
||||
:ok
|
||||
"""
|
||||
@spec restart(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||
def restart(app_id) when is_app_id(app_id) do
|
||||
|
@ -140,6 +185,11 @@ defmodule BalenaDevice.V1 do
|
|||
Invalidates the current `BALENA_SUPERVISOR_API_KEY` and generates a new one.
|
||||
Responds with the new API key, but *the application will be restarted on the
|
||||
next update cycle* to update the API key environment variable.
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.regenerate_api_key()
|
||||
{:ok, "d7bf0ab2665bb3ddb0da043727974daa46d6cfcb8c92fd068a852969576145"}
|
||||
"""
|
||||
@spec regenerate_api_key() :: {:ok, String.t()} | {:error, term}
|
||||
def regenerate_api_key do
|
||||
|
@ -183,6 +233,23 @@ defmodule BalenaDevice.V1 do
|
|||
|
||||
Other attributes may be added in the future, and some may be missing or null
|
||||
if they haven't been set yet.
|
||||
|
||||
## Example
|
||||
|
||||
iex> V1.state()
|
||||
{:ok,
|
||||
%{
|
||||
"api_port" => 48484,
|
||||
"commit" => "localrelease",
|
||||
"download_progress" => nil,
|
||||
"ip_address" => "192.168.56.101",
|
||||
"os_version" => "balenaOS 2.44.0+rev1",
|
||||
"status" => "Idle",
|
||||
"supervisor_version" => "10.3.7",
|
||||
"update_downloaded" => false,
|
||||
"update_failed" => false,
|
||||
"update_pending" => false
|
||||
}}
|
||||
"""
|
||||
@spec state() :: {:ok, map} | {:error, term}
|
||||
def state do
|
||||
|
|
|
@ -8,6 +8,16 @@ defmodule BalenaDevice.V2 do
|
|||
documentation](https://www.balena.io/docs/reference/supervisor/supervisor-api).
|
||||
"""
|
||||
|
||||
# Options which can be passed to `journald_logs/1`.
|
||||
@type journald_options :: [journald_option]
|
||||
# A valid option for `journald_log/1`.
|
||||
@type journald_option ::
|
||||
{:all, boolean}
|
||||
| {:follow, boolean}
|
||||
| {:count, non_neg_integer}
|
||||
| {:unit, String.t()}
|
||||
| {:format, String.t()}
|
||||
|
||||
@doc """
|
||||
Returns a map of applications and their state.
|
||||
"""
|
||||
|
@ -376,7 +386,7 @@ defmodule BalenaDevice.V2 do
|
|||
"""
|
||||
@spec local_log_stream() :: {:ok, pid} | {:error, term}
|
||||
def local_log_stream,
|
||||
do: GenServer.cast(BalenaDevice.LocalLogStream, {:subscribe, self()})
|
||||
do: BalenaDevice.LocalLogStream.start_link(receiver: self())
|
||||
|
||||
@doc """
|
||||
Get the last returned device name from the balena API. Note that this differs
|
||||
|
@ -448,4 +458,51 @@ defmodule BalenaDevice.V2 do
|
|||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
journald logs
|
||||
|
||||
Added in supervisor version v10.2.0
|
||||
|
||||
Retrieve a stream to the journald logs on device. This is equivalent to
|
||||
running `journalctl --no-pager`. Options supported are:
|
||||
|
||||
* `all: boolean` Show all fields in full, equivalent to `journalctl --all`.
|
||||
* `follow: boolean` Continuously stream logs as they are generated, equivalent
|
||||
to `journalctl --follow`.
|
||||
* `count: integer` Show the most recent count events, equivalent to
|
||||
`journalctl --line=<count>`.
|
||||
* `unit: String.t`
|
||||
Show journal logs from unit only, equivalent to `journalctl --unit=<unit>`.
|
||||
* `format: String.t` Added in supervisor version v10.3.0 The format which will
|
||||
be streamed from journalctl, formats are described here:
|
||||
https://www.freedesktop.org/software/systemd/man/journalctl.html#-o
|
||||
"""
|
||||
@spec journald_logs(journald_options) :: {:ok, String.t()} | {:ok, pid} | {:error, term}
|
||||
def journald_logs(options \\ []) when is_list(options) do
|
||||
options
|
||||
|> Keyword.take(~w[all follow count unit format]a)
|
||||
|> Enum.into(%{})
|
||||
|> do_journald_logs()
|
||||
end
|
||||
|
||||
defp do_journald_logs(%{follow: true} = options) do
|
||||
body = options |> Jason.encode!()
|
||||
BalenaDevice.JournaldLogStream.start_link(receiver: self(), body: body)
|
||||
end
|
||||
|
||||
defp do_journald_logs(options) do
|
||||
body = options |> Jason.encode!()
|
||||
|
||||
case HTTP.post("/v2/journal-logs", body) do
|
||||
{:ok, %Response{status_code: 200, body: body}} ->
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %Response{body: body, status_code: status}} ->
|
||||
{:error, "Status #{status}: #{inspect(body)}"}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Reference in a new issue