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.get
|
||||||
RUN mix deps.compile
|
RUN mix deps.compile
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app/
|
||||||
CMD mix run --no-halt
|
CMD mix run --no-halt
|
||||||
|
|
|
@ -4,9 +4,7 @@ defmodule BalenaDevice.Application do
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
children = [
|
children = []
|
||||||
BalenaDevice.LocalLogStream
|
|
||||||
]
|
|
||||||
|
|
||||||
opts = [strategy: :one_for_one, name: BalenaDevice.Supervisor]
|
opts = [strategy: :one_for_one, name: BalenaDevice.Supervisor]
|
||||||
Supervisor.start_link(children, opts)
|
Supervisor.start_link(children, opts)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
defmodule BalenaDevice.Error do
|
defmodule BalenaDevice.Error do
|
||||||
defexception ~w[message]a
|
defexception ~w[message]a
|
||||||
|
@moduledoc false
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,16 @@ defmodule BalenaDevice.HTTP do
|
||||||
use HTTPoison.Base
|
use HTTPoison.Base
|
||||||
alias BalenaDevice.Error
|
alias BalenaDevice.Error
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
Uses `HTTPoison.Base` to construct requests for the on-device supervisor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc false
|
||||||
def process_request_url(url) do
|
def process_request_url(url) do
|
||||||
"#{supervisor_address()}#{url}?apikey=#{supervisor_api_key()}"
|
"#{supervisor_address()}#{url}?apikey=#{supervisor_api_key()}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def process_request_headers(headers) do
|
def process_request_headers(headers) do
|
||||||
headers
|
headers
|
||||||
|> Enum.into(%{})
|
|> 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
|
alias BalenaDevice.HTTP
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Implements a `GenServer` that when there are subscribers connects to the
|
A simple `GenServer` which handles streaming local device logs from the
|
||||||
supervisor API and retrieves logs and sending them to the subscribers.
|
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
|
@impl true
|
||||||
def init(_), do: {:ok, %{subscribers: [], connection: nil}, :hibernate}
|
def init(options) do
|
||||||
|
receiver = options |> Keyword.get(:receiver)
|
||||||
|
|
||||||
@impl true
|
{:ok, conn} =
|
||||||
def handle_cast({:subscribe, pid}, %{subscribers: subscribers, connection: nil} = state) do
|
HTTP.get("/v2/local/logs", [],
|
||||||
Process.monitor(pid)
|
stream_to: self(),
|
||||||
{:ok, connection} = start_connection()
|
timeout: :infinity,
|
||||||
{:noreply, %{state | subscribers: [pid | subscribers], connection: connection}}
|
recv_timeout: :infinity
|
||||||
end
|
)
|
||||||
|
|
||||||
def handle_cast({:subscribe, pid}, %{subscribers: subscribers} = state) do
|
state = %{conn: conn, receiver: receiver}
|
||||||
Process.monitor(pid)
|
{:ok, state}
|
||||||
{:noreply, %{state | subscribers: [pid | subscribers]}}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(%HTTPoison.AsyncStatus{}, state), do: {:noreply, state}
|
def handle_info(%HTTPoison.AsyncStatus{}, state), do: {:noreply, state}
|
||||||
def handle_info(%HTTPoison.AsyncHeaders{}, state), do: {:noreply, state}
|
def handle_info(%HTTPoison.AsyncHeaders{}, state), do: {:noreply, state}
|
||||||
|
|
||||||
def handle_info(%HTTPoison.AsyncChunk{chunk: chunk}, %{subscribers: subscribers} = state) do
|
def handle_info(%HTTPoison.AsyncChunk{chunk: chunk}, %{receiver: receiver} = state) do
|
||||||
chunk = Jason.decode!(chunk)
|
send(receiver, {:local_log, chunk})
|
||||||
Enum.each(subscribers, &send(&1, chunk))
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(%HTTPoison.Error{}, %{subscribers: []} = state) do
|
def handle_info(%HTTPoison.Error{reason: reason}, state), do: {:stop, reason, state}
|
||||||
{:noreply, %{state | connection: nil}}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_info(%HTTPoison.Error{}, %{subscribers: subscribers} = state) do
|
def handle_info(%HTTPoison.AsyncEnd{}, state), do: {:stop, :normal, state}
|
||||||
{: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
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
defmodule BalenaDevice.Utils do
|
defmodule BalenaDevice.Utils do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
@doc false
|
||||||
def underscore_map_keys(value) when is_list(value) do
|
def underscore_map_keys(value) when is_list(value) do
|
||||||
value
|
value
|
||||||
|> Enum.map(&underscore_map_keys(&1))
|
|> Enum.map(&underscore_map_keys(&1))
|
||||||
|
|
|
@ -10,6 +10,11 @@ defmodule BalenaDevice.V1 do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Request a device identification by blinking the device's LED for 15 seconds.
|
Request a device identification by blinking the device's LED for 15 seconds.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
iex> V1.blink()
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec blink() :: :ok | {:error, term}
|
@spec blink() :: :ok | {:error, term}
|
||||||
def blink do
|
def blink do
|
||||||
|
@ -27,6 +32,11 @@ defmodule BalenaDevice.V1 do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Trigger an update check on the supervisor.
|
Trigger an update check on the supervisor.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> V1.update()
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec update() :: :ok | {:error, term}
|
@spec update() :: :ok | {:error, term}
|
||||||
def update, do: update(false)
|
def update, do: update(false)
|
||||||
|
@ -34,6 +44,11 @@ defmodule BalenaDevice.V1 do
|
||||||
@doc """
|
@doc """
|
||||||
Trigger an update check on the supervisor. Optionally, forces an update when
|
Trigger an update check on the supervisor. Optionally, forces an update when
|
||||||
updates are locked.
|
updates are locked.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> V1.update(true)
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec update(boolean) :: :ok | {:error, term}
|
@spec update(boolean) :: :ok | {:error, term}
|
||||||
def update(force) do
|
def update(force) do
|
||||||
|
@ -52,6 +67,11 @@ defmodule BalenaDevice.V1 do
|
||||||
@doc """
|
@doc """
|
||||||
Reboots the device. This will first try to stop applications, and fail if
|
Reboots the device. This will first try to stop applications, and fail if
|
||||||
there is an update lock.
|
there is an update lock.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> V1.reboot()
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec reboot() :: :ok | {:error, term}
|
@spec reboot() :: :ok | {:error, term}
|
||||||
def reboot, do: reboot(false)
|
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
|
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
|
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).
|
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}
|
@spec reboot(boolean) :: :ok | {:error, term}
|
||||||
def reboot(force) do
|
def reboot(force) do
|
||||||
|
@ -78,6 +103,11 @@ defmodule BalenaDevice.V1 do
|
||||||
@doc """
|
@doc """
|
||||||
*Dangerous*. Shuts down the device. This will first try to stop applications,
|
*Dangerous*. Shuts down the device. This will first try to stop applications,
|
||||||
and fail if there is an update lock.
|
and fail if there is an update lock.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> V1.shutdown()
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec shutdown() :: :ok | {:error, term}
|
@spec shutdown() :: :ok | {:error, term}
|
||||||
def shutdown, do: shutdown(false)
|
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
|
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
|
overrides the lock when true (and the lock can also be overridden from the
|
||||||
dashboard).
|
dashboard).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> V1.shutdown(true)
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec shutdown(boolean) :: :ok | {:error, term}
|
@spec shutdown(boolean) :: :ok | {:error, term}
|
||||||
def shutdown(force) do
|
def shutdown(force) do
|
||||||
|
@ -104,6 +139,11 @@ defmodule BalenaDevice.V1 do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Clears the user application's /data folder.
|
Clears the user application's /data folder.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> V1.purge(1)
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec purge(BalenaDevice.app_id()) :: :ok | {:error, term}
|
@spec purge(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||||
def purge(app_id) when is_app_id(app_id) do
|
def purge(app_id) when is_app_id(app_id) do
|
||||||
|
@ -121,6 +161,11 @@ defmodule BalenaDevice.V1 do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Restarts a user application container
|
Restarts a user application container
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> V1.restart(1)
|
||||||
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec restart(BalenaDevice.app_id()) :: :ok | {:error, term}
|
@spec restart(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||||
def restart(app_id) when is_app_id(app_id) do
|
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.
|
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
|
Responds with the new API key, but *the application will be restarted on the
|
||||||
next update cycle* to update the API key environment variable.
|
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}
|
@spec regenerate_api_key() :: {:ok, String.t()} | {:error, term}
|
||||||
def regenerate_api_key do
|
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
|
Other attributes may be added in the future, and some may be missing or null
|
||||||
if they haven't been set yet.
|
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}
|
@spec state() :: {:ok, map} | {:error, term}
|
||||||
def state do
|
def state do
|
||||||
|
|
|
@ -8,6 +8,16 @@ defmodule BalenaDevice.V2 do
|
||||||
documentation](https://www.balena.io/docs/reference/supervisor/supervisor-api).
|
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 """
|
@doc """
|
||||||
Returns a map of applications and their state.
|
Returns a map of applications and their state.
|
||||||
"""
|
"""
|
||||||
|
@ -376,7 +386,7 @@ defmodule BalenaDevice.V2 do
|
||||||
"""
|
"""
|
||||||
@spec local_log_stream() :: {:ok, pid} | {:error, term}
|
@spec local_log_stream() :: {:ok, pid} | {:error, term}
|
||||||
def local_log_stream,
|
def local_log_stream,
|
||||||
do: GenServer.cast(BalenaDevice.LocalLogStream, {:subscribe, self()})
|
do: BalenaDevice.LocalLogStream.start_link(receiver: self())
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Get the last returned device name from the balena API. Note that this differs
|
Get the last returned device name from the balena API. Note that this differs
|
||||||
|
@ -448,4 +458,51 @@ defmodule BalenaDevice.V2 do
|
||||||
{:error, reason}
|
{:error, reason}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Reference in a new issue