508 lines
14 KiB
Elixir
508 lines
14 KiB
Elixir
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).
|
|
"""
|
|
|
|
# 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.
|
|
"""
|
|
@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: BalenaDevice.LocalLogStream.start_link(receiver: 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
|
|
|
|
@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
|