Initial version of the on-device API client.
This commit is contained in:
commit
833cf1745f
17 changed files with 1140 additions and 0 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
_build/
|
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
balena_device-*.tar
|
||||||
|
|
57
Dockerfile.template
Normal file
57
Dockerfile.template
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
FROM balenalib/%%BALENA_MACHINE_NAME%%-debian:buster as build
|
||||||
|
|
||||||
|
ENV ERLANG_VERSION 22.1
|
||||||
|
ENV ERLANG_HASH 7b26f64eb6c712968d8477759fc716d64701d41f6325e8a4d0dd9c31de77284a
|
||||||
|
ENV ELIXIR_VERSION 1.9.1
|
||||||
|
ENV ELIXIR_HASH 94daa716abbd4493405fb2032514195077ac7bc73dc2999922f13c7d8ea58777
|
||||||
|
|
||||||
|
RUN set -x \
|
||||||
|
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-key 8B48AD6246925553 \
|
||||||
|
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-key 7638D0442B90D010 \
|
||||||
|
&& apt-get -q update \
|
||||||
|
&& apt-get install -y --no-install-recommends autoconf build-essential ca-certificates curl python dpkg-dev g++ gcc git-core libc6-dev libncurses-dev libsctp-dev libssl-dev make mercurial python-dev python-pip unixodbc-dev wget \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN set -x \
|
||||||
|
&& ERLANG_DOWNLOAD_URL="https://github.com/erlang/otp/archive/OTP-${ERLANG_VERSION}.tar.gz" \
|
||||||
|
&& ERLANG_ARCHIVE="OTP-$ERLANG_VERSION.tar.gz" \
|
||||||
|
&& curl -fSL -o $ERLANG_ARCHIVE "$ERLANG_DOWNLOAD_URL" \
|
||||||
|
&& echo "$ERLANG_HASH $ERLANG_ARCHIVE" | sha256sum -c - \
|
||||||
|
&& ERL_TOP=/erlang-src \
|
||||||
|
&& mkdir -vp $ERL_TOP \
|
||||||
|
&& tar -xzf $ERLANG_ARCHIVE -C $ERL_TOP --strip-components=1 \
|
||||||
|
&& rm $ERLANG_ARCHIVE \
|
||||||
|
&& ( \
|
||||||
|
cd $ERL_TOP \
|
||||||
|
&& ./otp_build autoconf \
|
||||||
|
&& gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
|
||||||
|
&& ./configure --build="$gnuArch" \
|
||||||
|
&& make \
|
||||||
|
&& make install \
|
||||||
|
)
|
||||||
|
|
||||||
|
RUN set -x \
|
||||||
|
&& ELIXIR_DOWNLOAD_URL="https://github.com/elixir-lang/elixir/archive/v${ELIXIR_VERSION}.tar.gz" \
|
||||||
|
&& curl -fSL -o elixir-src.tar.gz $ELIXIR_DOWNLOAD_URL \
|
||||||
|
&& echo "$ELIXIR_HASH elixir-src.tar.gz" | sha256sum -c - \
|
||||||
|
&& mkdir /elixir-src \
|
||||||
|
&& tar -xzC /elixir-src --strip-components=1 -f elixir-src.tar.gz \
|
||||||
|
&& rm elixir-src.tar.gz \
|
||||||
|
&& (cd /elixir-src && make install) \
|
||||||
|
&& rm -rf /elixir-src
|
||||||
|
|
||||||
|
RUN mix local.hex --force
|
||||||
|
RUN mix local.rebar --force
|
||||||
|
|
||||||
|
ENV ERL_CRASH_DUMP_BYTES=0
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY mix.exs mix.lock /app/
|
||||||
|
RUN mix deps.get
|
||||||
|
RUN mix deps.compile
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
CMD mix run --no-halt
|
21
README.md
Normal file
21
README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# BalenaDevice
|
||||||
|
|
||||||
|
**TODO: Add description**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
by adding `balena_device` to your list of dependencies in `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:balena_device, "~> 0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||||
|
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||||
|
be found at [https://hexdocs.pm/balena_device](https://hexdocs.pm/balena_device).
|
||||||
|
|
29
lib/balena_device.ex
Normal file
29
lib/balena_device.ex
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
defmodule BalenaDevice do
|
||||||
|
alias BalenaDevice.HTTP
|
||||||
|
alias HTTPoison.Response
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
Wraps the Balena DBUS and Supervisor HTTP APIs to allow Elixir apps running on
|
||||||
|
the Balena platform to manipulate their environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# An application ID.
|
||||||
|
@type app_id :: non_neg_integer
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Ping the device supervisor to ensure that it is alive and well.
|
||||||
|
"""
|
||||||
|
@spec ping() :: :ok | {:error, term}
|
||||||
|
def ping do
|
||||||
|
case HTTP.get("/ping") do
|
||||||
|
{:ok, %Response{body: "OK", status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
lib/balena_device/application.ex
Normal file
14
lib/balena_device/application.ex
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
defmodule BalenaDevice.Application do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Application
|
||||||
|
|
||||||
|
def start(_type, _args) do
|
||||||
|
children = [
|
||||||
|
BalenaDevice.LocalLogStream
|
||||||
|
]
|
||||||
|
|
||||||
|
opts = [strategy: :one_for_one, name: BalenaDevice.Supervisor]
|
||||||
|
Supervisor.start_link(children, opts)
|
||||||
|
end
|
||||||
|
end
|
3
lib/balena_device/error.ex
Normal file
3
lib/balena_device/error.ex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule BalenaDevice.Error do
|
||||||
|
defexception ~w[message]a
|
||||||
|
end
|
28
lib/balena_device/http.ex
Normal file
28
lib/balena_device/http.ex
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule BalenaDevice.HTTP do
|
||||||
|
use HTTPoison.Base
|
||||||
|
alias BalenaDevice.Error
|
||||||
|
|
||||||
|
def process_request_url(url) do
|
||||||
|
"#{supervisor_address()}#{url}?apikey=#{supervisor_api_key()}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_request_headers(headers) do
|
||||||
|
headers
|
||||||
|
|> Enum.into(%{})
|
||||||
|
|> Map.put_new("content-type", "application/json")
|
||||||
|
|> Enum.into([])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp supervisor_address, do: get_env_var("BALENA_SUPERVISOR_ADDRESS")
|
||||||
|
defp supervisor_api_key, do: get_env_var("BALENA_SUPERVISOR_API_KEY")
|
||||||
|
|
||||||
|
defp get_env_var(variable) do
|
||||||
|
case System.get_env(variable) do
|
||||||
|
var when is_binary(var) ->
|
||||||
|
var
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
raise Error, message: "No `#{variable}` environment variable set"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
70
lib/balena_device/local_log_stream.ex
Normal file
70
lib/balena_device/local_log_stream.ex
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
defmodule BalenaDevice.LocalLogStream do
|
||||||
|
use GenServer
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def start_link(_), do: GenServer.start_link(__MODULE__, [], hibernate_after: 5_000)
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_), do: {:ok, %{subscribers: [], connection: nil}, :hibernate}
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
def handle_cast({:subscribe, pid}, %{subscribers: subscribers} = state) do
|
||||||
|
Process.monitor(pid)
|
||||||
|
{:noreply, %{state | subscribers: [pid | subscribers]}}
|
||||||
|
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))
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(%HTTPoison.Error{}, %{subscribers: []} = state) do
|
||||||
|
{:noreply, %{state | connection: nil}}
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
end
|
18
lib/balena_device/utils.ex
Normal file
18
lib/balena_device/utils.ex
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule BalenaDevice.Utils do
|
||||||
|
def underscore_map_keys(value) when is_list(value) do
|
||||||
|
value
|
||||||
|
|> Enum.map(&underscore_map_keys(&1))
|
||||||
|
end
|
||||||
|
|
||||||
|
def underscore_map_keys(map) when is_map(map) do
|
||||||
|
map
|
||||||
|
|> Enum.reduce(%{}, fn
|
||||||
|
{key, value}, acc ->
|
||||||
|
Map.put(acc, Macro.underscore(key), underscore_map_keys(value))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def underscore_map_keys(value), do: value
|
||||||
|
|
||||||
|
defguard is_app_id(app_id) when is_integer(app_id) and app_id > 0
|
||||||
|
end
|
372
lib/balena_device/v1.ex
Normal file
372
lib/balena_device/v1.ex
Normal file
|
@ -0,0 +1,372 @@
|
||||||
|
defmodule BalenaDevice.V1 do
|
||||||
|
alias BalenaDevice.HTTP
|
||||||
|
import BalenaDevice.Utils
|
||||||
|
alias HTTPoison.Response
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains the version one API endpoints as described in [the API
|
||||||
|
documentation](https://www.balena.io/docs/reference/supervisor/supervisor-api).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Request a device identification by blinking the device's LED for 15 seconds.
|
||||||
|
"""
|
||||||
|
@spec blink() :: :ok | {:error, term}
|
||||||
|
def blink do
|
||||||
|
case HTTP.post("/v1/blink", "") do
|
||||||
|
{:ok, %Response{body: "OK", status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Trigger an update check on the supervisor.
|
||||||
|
"""
|
||||||
|
@spec update() :: :ok | {:error, term}
|
||||||
|
def update, do: update(false)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Trigger an update check on the supervisor. Optionally, forces an update when
|
||||||
|
updates are locked.
|
||||||
|
"""
|
||||||
|
@spec update(boolean) :: :ok | {:error, term}
|
||||||
|
def update(force) do
|
||||||
|
case HTTP.post("/v1/update", Jason.encode!(%{force: force})) do
|
||||||
|
{:ok, %Response{status_code: 204}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Reboots the device. This will first try to stop applications, and fail if
|
||||||
|
there is an update lock.
|
||||||
|
"""
|
||||||
|
@spec reboot() :: :ok | {:error, term}
|
||||||
|
def reboot, do: reboot(false)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
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).
|
||||||
|
"""
|
||||||
|
@spec reboot(boolean) :: :ok | {:error, term}
|
||||||
|
def reboot(force) do
|
||||||
|
case HTTP.post("/v1/reboot", Jason.encode!(%{force: force})) do
|
||||||
|
{:ok, %Response{status_code: 202}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
*Dangerous*. Shuts down the device. This will first try to stop applications,
|
||||||
|
and fail if there is an update lock.
|
||||||
|
"""
|
||||||
|
@spec shutdown() :: :ok | {:error, term}
|
||||||
|
def shutdown, do: shutdown(false)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
*Dangerous*. Shuts down 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).
|
||||||
|
"""
|
||||||
|
@spec shutdown(boolean) :: :ok | {:error, term}
|
||||||
|
def shutdown(force) do
|
||||||
|
case HTTP.post("/v1/shutdown", Jason.encode!(%{force: force})) do
|
||||||
|
{:ok, %Response{status_code: 202}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Clears the user application's /data folder.
|
||||||
|
"""
|
||||||
|
@spec purge(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||||
|
def purge(app_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post("/v1/purge", Jason.encode!(%{"appId" => app_id})) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Restarts a user application container
|
||||||
|
"""
|
||||||
|
@spec restart(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||||
|
def restart(app_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post("/v1/restart", Jason.encode!(%{"appId" => app_id})) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
@spec regenerate_api_key() :: {:ok, String.t()} | {:error, term}
|
||||||
|
def regenerate_api_key do
|
||||||
|
case HTTP.post("/v1/regenerate-api-key", "") do
|
||||||
|
{:ok, %Response{status_code: 200, body: key}} ->
|
||||||
|
{:ok, key}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the current device state, as reported to the balenaCloud API and with
|
||||||
|
some extra fields added to allow control over pending/locked updates.
|
||||||
|
|
||||||
|
The state is a map that contains some or all of the following:
|
||||||
|
|
||||||
|
* `api_port`: Port on which the supervisor is listening.
|
||||||
|
* `commit`: Hash of the current commit of the application that is running.
|
||||||
|
* `ip_address`: Space-separated list of IP addresses of the device.
|
||||||
|
* `status`: Status of the device regarding the app, as a string, i.e.
|
||||||
|
"Stopping", "Starting", "Downloading", "Installing", "Idle".
|
||||||
|
* `download_progress`: Amount of the application image that has been
|
||||||
|
downloaded, expressed as a percentage. If the update has already been
|
||||||
|
downloaded, this will be null.
|
||||||
|
* `os_version`: Version of the host OS running on the device.
|
||||||
|
* `supervisor_version`: Version of the supervisor running on the device.
|
||||||
|
* `update_pending`: This one is not reported to the balenaCloud API. It's a
|
||||||
|
boolean that will be true if the supervisor has detected there is a
|
||||||
|
pending update.
|
||||||
|
* `update_downloaded`: Not reported to the balenaCloud API either. Boolean
|
||||||
|
that will be true if a pending update has already been downloaded.
|
||||||
|
* `update_failed`: Not reported to the balenaCloud API. Boolean that will be
|
||||||
|
true if the supervisor has tried to apply a pending update but failed
|
||||||
|
(i.e. if the app was locked, there was a network failure or anything else
|
||||||
|
went wrong).
|
||||||
|
|
||||||
|
Other attributes may be added in the future, and some may be missing or null
|
||||||
|
if they haven't been set yet.
|
||||||
|
"""
|
||||||
|
@spec state() :: {:ok, map} | {:error, term}
|
||||||
|
def state do
|
||||||
|
case HTTP.get("/v1/device") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
state =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Temporarily stops a user application container. A reboot or supervisor restart
|
||||||
|
will cause the container to start again. The container is not removed with
|
||||||
|
this endpoint.
|
||||||
|
|
||||||
|
This is only supported on single-container devices, and will fail on devices
|
||||||
|
running multiple containers.
|
||||||
|
|
||||||
|
Returns an ok tuple with the ID of the stopped container.
|
||||||
|
"""
|
||||||
|
@spec stop_app(BalenaDevice.app_id()) :: {:ok, String.t()} | {:error, term}
|
||||||
|
def stop_app(app_id) when is_app_id(app_id), do: stop_app(app_id, false)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Temporarily stops a user application container. A reboot or supervisor restart
|
||||||
|
will cause the container to start again. The container is not removed with
|
||||||
|
this endpoint.
|
||||||
|
|
||||||
|
This is only supported on single-container devices, and will fail on devices
|
||||||
|
running multiple containers.
|
||||||
|
|
||||||
|
Returns an ok tuple with the ID of the stopped container.
|
||||||
|
"""
|
||||||
|
@spec stop_app(BalenaDevice.app_id(), boolean) :: {:ok, String.t()} | {:error, term}
|
||||||
|
def stop_app(app_id, force) when is_app_id(app_id) when is_boolean(force) do
|
||||||
|
case HTTP.post("/v1/apps/#{app_id}/stop", Jason.encode!(%{force: force})) do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
container_id =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("containerId")
|
||||||
|
|
||||||
|
{:ok, container_id}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts a user application container, usually after it has been stopped with
|
||||||
|
`stop_app/2`.
|
||||||
|
|
||||||
|
This is only supported on single-container devices, and will return `:error` on
|
||||||
|
devices running multiple containers.
|
||||||
|
|
||||||
|
When successful, responds with :ok and the id of the started container.
|
||||||
|
"""
|
||||||
|
@spec start_app(BalenaDevice.app_id()) :: {:ok, String.t()} | {:error, term}
|
||||||
|
def start_app(app_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post("/v1/apps/#{app_id}/start", "") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
container_id =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("containerId")
|
||||||
|
|
||||||
|
{:ok, container_id}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the application running on the device. The app is a map that
|
||||||
|
contains the following:
|
||||||
|
|
||||||
|
* `app_id`: The id of the app as per the balenaCloud API.
|
||||||
|
* `commit`: Application commit that is running.
|
||||||
|
* `image_id`: The docker image of the current application build.
|
||||||
|
* `container_id`: ID of the docker container of the running app.
|
||||||
|
* `env`: A key-value store of the app's environment variables.
|
||||||
|
|
||||||
|
This is only supported on single-container devices, and will return an error on
|
||||||
|
devices running multiple containers.
|
||||||
|
"""
|
||||||
|
@spec app_state(BalenaDevice.app_id()) :: {:ok, map} | {:error, term}
|
||||||
|
def app_state(app_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.get("/v1/apps/#{app_id}") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
state =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> underscore_map_keys()
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Used internally to check whether the supervisor is running correctly,
|
||||||
|
according to some heuristics that help determine whether the internal
|
||||||
|
components, application updates and reporting to the balenaCloud API are
|
||||||
|
functioning.
|
||||||
|
|
||||||
|
Responds with an `true` response if the supervisor is healthy, or `false` if
|
||||||
|
something is not working correctly.
|
||||||
|
"""
|
||||||
|
@spec healthy? :: boolean
|
||||||
|
def healthy? do
|
||||||
|
case HTTP.get("/v1/healthy") do
|
||||||
|
{:ok, %Response{status_code: 200}} -> true
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
This endpoint allows setting some configuration values for the host OS.
|
||||||
|
Currently it supports proxy and hostname configuration.
|
||||||
|
|
||||||
|
For proxy configuration, balenaOS 2.0.7 and higher provides a transparent
|
||||||
|
proxy redirector (redsocks) that makes all connections be routed to a SOCKS or
|
||||||
|
HTTP proxy. This endpoint allows user applications to modify these proxy
|
||||||
|
settings at runtime.
|
||||||
|
|
||||||
|
See [PATCH /v1/device/host-config](https://www.balena.io/docs/reference/supervisor/supervisor-api/#patch-v1devicehost-config).
|
||||||
|
"""
|
||||||
|
@spec host_config(map) :: :ok | {:error, term}
|
||||||
|
def host_config(config) do
|
||||||
|
case HTTP.patch("/v1/device/host-config", Jason.encode!(config)) do
|
||||||
|
{:ok, %Response{status_code: 200, body: "OK"}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
This endpoint allows reading some configuration values for the host OS,
|
||||||
|
previously set with `host_config/1`. Currently it supports proxy and hostname
|
||||||
|
configuration.
|
||||||
|
"""
|
||||||
|
@spec host_config :: {:ok, map} | {:error, term}
|
||||||
|
def host_config do
|
||||||
|
case HTTP.get("/v1/device/host-config") do
|
||||||
|
{:ok, %Response{body: body, status_code: 200}} ->
|
||||||
|
config =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> underscore_map_keys()
|
||||||
|
|
||||||
|
{:ok, config}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
451
lib/balena_device/v2.ex
Normal file
451
lib/balena_device/v2.ex
Normal file
|
@ -0,0 +1,451 @@
|
||||||
|
defmodule BalenaDevice.V2 do
|
||||||
|
alias BalenaDevice.HTTP
|
||||||
|
import BalenaDevice.Utils
|
||||||
|
alias HTTPoison.Response
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains the version two API endpoints as described in [the API
|
||||||
|
documentation](https://www.balena.io/docs/reference/supervisor/supervisor-api).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a map of applications and their state.
|
||||||
|
"""
|
||||||
|
@spec applications_state :: {:ok, [map]} | {:error, term}
|
||||||
|
def applications_state do
|
||||||
|
case HTTP.get("/v2/applications/state") do
|
||||||
|
{:ok, %Response{body: body, status_code: 200}} ->
|
||||||
|
state =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> underscore_map_keys()
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Use this endpoint to get the state of a single application, given the app_id.
|
||||||
|
"""
|
||||||
|
@spec application_state(BalenaDevice.app_id()) :: {:ok, map} | {:error, term}
|
||||||
|
def application_state(app_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.get("/v2/applications/#{app_id}/state") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
state =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> underscore_map_keys()
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
This will return a list of images, containers, the overall download progress
|
||||||
|
and the status of the state engine.
|
||||||
|
"""
|
||||||
|
@spec status :: {:ok, map} | {:error, term}
|
||||||
|
def status do
|
||||||
|
case HTTP.get("/v2/state/status") do
|
||||||
|
{:ok, %Response{body: body, status_code: 200}} ->
|
||||||
|
status =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> underscore_map_keys()
|
||||||
|
|
||||||
|
{:ok, status}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Restart an application's service based on it's service name or image id.
|
||||||
|
"""
|
||||||
|
@spec restart_service(
|
||||||
|
BalenaDevice.app_id(),
|
||||||
|
[{:service_name, String.t()}] | [{:image_id, String.t()}]
|
||||||
|
) ::
|
||||||
|
:ok | {:error, term}
|
||||||
|
def restart_service(app_id, service_name: service_name) when is_app_id(app_id) do
|
||||||
|
case HTTP.post(
|
||||||
|
"/v2/applications/#{app_id}/restart-service",
|
||||||
|
Jason.encode!(%{"serviceName" => service_name})
|
||||||
|
) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def restart_service(app_id, image_id: image_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post(
|
||||||
|
"/v2/applications/#{app_id}/restart-service",
|
||||||
|
Jason.encode!(%{"imageId" => image_id})
|
||||||
|
) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def restart_service(_app_id, _),
|
||||||
|
do: {:error, "Must specify either a service name or an image id"}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Stop an application's service based on it's service name or image id.
|
||||||
|
"""
|
||||||
|
@spec stop_service(
|
||||||
|
BalenaDevice.app_id(),
|
||||||
|
[{:service_name, String.t()}] | [{:image_id, String.t()}]
|
||||||
|
) ::
|
||||||
|
:ok | {:error, term}
|
||||||
|
def stop_service(app_id, service_name: service_name) when is_app_id(app_id) do
|
||||||
|
case HTTP.post(
|
||||||
|
"/v2/applications/#{app_id}/stop-service",
|
||||||
|
Jason.encode!(%{"serviceName" => service_name})
|
||||||
|
) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_service(app_id, image_id: image_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post(
|
||||||
|
"/v2/applications/#{app_id}/stop-service",
|
||||||
|
Jason.encode!(%{"imageId" => image_id})
|
||||||
|
) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_service(_app_id, _),
|
||||||
|
do: {:error, "Must specify either a service name or an image id"}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Start an application's service based on it's service name or image id.
|
||||||
|
"""
|
||||||
|
@spec start_service(
|
||||||
|
BalenaDevice.app_id(),
|
||||||
|
[{:service_name, String.t()}] | [{:image_id, String.t()}]
|
||||||
|
) ::
|
||||||
|
:ok | {:error, term}
|
||||||
|
def start_service(app_id, service_name: service_name) when is_app_id(app_id) do
|
||||||
|
case HTTP.post(
|
||||||
|
"/v2/applications/#{app_id}/start-service",
|
||||||
|
Jason.encode!(%{"serviceName" => service_name})
|
||||||
|
) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_service(app_id, image_id: image_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post(
|
||||||
|
"/v2/applications/#{app_id}/start-service",
|
||||||
|
Jason.encode!(%{"imageId" => image_id})
|
||||||
|
) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_service(_app_id, _),
|
||||||
|
do: {:error, "Must specify either a service name or an image id"}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Restart all services in an application.
|
||||||
|
"""
|
||||||
|
@spec restart_services(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||||
|
def restart_services(app_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post("/v2/applications/#{app_id}/restart", "") do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Use this endpoint to purge all user data for a given application id.
|
||||||
|
"""
|
||||||
|
@spec purge(BalenaDevice.app_id()) :: :ok | {:error, term}
|
||||||
|
def purge(app_id) when is_app_id(app_id) do
|
||||||
|
case HTTP.post("/v2/applications/#{app_id}/purge", "") do
|
||||||
|
{:ok, %Response{status_code: 200, body: "OK"}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
This endpoint returns the supervisor version currently running the device api.
|
||||||
|
"""
|
||||||
|
@spec supervisor_version() :: {:ok, String.t()} | {:error, term}
|
||||||
|
def supervisor_version do
|
||||||
|
case HTTP.get("/v2/version") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
version =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("version")
|
||||||
|
|
||||||
|
{:ok, version}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Use this endpoint to match a service name to a container ID.
|
||||||
|
"""
|
||||||
|
@spec container_ids() :: {:ok, map} | {:error, term}
|
||||||
|
def container_ids() do
|
||||||
|
case HTTP.get("/v2/containerId") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
services =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("services")
|
||||||
|
|
||||||
|
{:ok, services}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Use this endpoint to match a service name to a container ID.
|
||||||
|
"""
|
||||||
|
@spec container_id(String.t()) :: {:ok, String.t()} | {:error, term}
|
||||||
|
def container_id(service_name) when is_binary(service_name) do
|
||||||
|
case HTTP.get("/v2/containerId?serviceName=#{service_name}") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
container_id =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("containerId")
|
||||||
|
|
||||||
|
{:ok, container_id}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get the current target state. Note that if a local mode target state has not
|
||||||
|
been set then the apps section of the response will always be empty.
|
||||||
|
|
||||||
|
Only works on devices which are running in local mode.
|
||||||
|
"""
|
||||||
|
@spec local_target_state :: {:ok, map} | {:error, term}
|
||||||
|
def local_target_state do
|
||||||
|
case HTTP.get("/v2/local/target-state") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
state =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("state")
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Set the current target state.
|
||||||
|
"""
|
||||||
|
@spec local_target_state(map) :: :ok | {:error, term}
|
||||||
|
def local_target_state(config) do
|
||||||
|
case HTTP.post("/v2/local/target-state", Jason.encode!(config)) do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get the architecture and device type of the device.
|
||||||
|
"""
|
||||||
|
@spec local_device_info :: {:ok, map} | {:error, term}
|
||||||
|
def local_device_info do
|
||||||
|
case HTTP.get("/v2/local/device-info") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
info =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("info")
|
||||||
|
|
||||||
|
{:ok, info}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Stream local mode application logs from the device.
|
||||||
|
|
||||||
|
This function starts a process which will send logs to the
|
||||||
|
creating processes mailbox as they arrive.
|
||||||
|
"""
|
||||||
|
@spec local_log_stream() :: {:ok, pid} | {:error, term}
|
||||||
|
def local_log_stream,
|
||||||
|
do: GenServer.cast(BalenaDevice.LocalLogStream, {:subscribe, self()})
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get the last returned device name from the balena API. Note that this differs
|
||||||
|
from the `BALENA_DEVICE_NAME_AT_INIT` environment variable provided to
|
||||||
|
containers, as this will not change throughout the runtime of the container,
|
||||||
|
but the endpoint will always return the latest known device name.
|
||||||
|
"""
|
||||||
|
@spec device_name() :: {:ok, String.t()} | {:error, term}
|
||||||
|
def device_name do
|
||||||
|
case HTTP.get("/v2/device/name") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
name =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("deviceName")
|
||||||
|
|
||||||
|
{:ok, name}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieve any device tags from the balena API. Note that this endpoint will not
|
||||||
|
work when the device does not have an available connection to the balena API.
|
||||||
|
"""
|
||||||
|
@spec device_tags() :: {:ok, [map]} | {:error, term}
|
||||||
|
def device_tags do
|
||||||
|
case HTTP.get("/v2/device/tags") do
|
||||||
|
{:ok, %Response{status_code: 200, body: body}} ->
|
||||||
|
tags =
|
||||||
|
body
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("tags")
|
||||||
|
|
||||||
|
{:ok, tags}
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Cleanup volumes with no references
|
||||||
|
|
||||||
|
Added in supervisor version v10.0.0
|
||||||
|
|
||||||
|
Starting with balena-supervisor v10.0.0, volumes which have no references are
|
||||||
|
no longer automatically removed as part of the standard update flow. To
|
||||||
|
cleanup up any orphaned volumes, use this supervisor endpoint.
|
||||||
|
"""
|
||||||
|
@spec cleanup_volumes() :: :ok | {:error, term}
|
||||||
|
def cleanup_volumes do
|
||||||
|
case HTTP.get("/v2/cleanup-volumess") do
|
||||||
|
{:ok, %Response{status_code: 200}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, %Response{body: body, status_code: status}} ->
|
||||||
|
{:error, "Status #{status}: #{inspect(body)}"}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
mix.exs
Normal file
30
mix.exs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
defmodule BalenaDevice.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :balena_device,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.9",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger],
|
||||||
|
mod: {BalenaDevice.Application, []}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
{:httpoison, "~> 1.6"},
|
||||||
|
{:jason, "~> 1.1"},
|
||||||
|
{:dbus, "~> 0.7"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
13
mix.lock
Normal file
13
mix.lock
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
%{
|
||||||
|
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"dbus": {:hex, :dbus, "0.7.0", "71b7660523be6e222a8a4f9ddcbf54ad95638479bd17654975bb751aadbe81de", [:make, :rebar3], [], "hexpm"},
|
||||||
|
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"httpoison": {:hex, :httpoison, "1.6.1", "2ce5bf6e535cd0ab02e905ba8c276580bab80052c5c549f53ddea52d72e81f33", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
|
||||||
|
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
|
||||||
|
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
|
||||||
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
|
||||||
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
|
||||||
|
}
|
4
test/balena_device_test.exs
Normal file
4
test/balena_device_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule BalenaDeviceTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest BalenaDevice
|
||||||
|
end
|
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
Reference in a new issue