Redo Farmware installation and lookup.
This commit is contained in:
parent
f67590ba76
commit
ff37b75093
130
lib/farmbot/farmware/farmware.ex
Normal file
130
lib/farmbot/farmware/farmware.ex
Normal file
|
@ -0,0 +1,130 @@
|
|||
defmodule Farmbot.Farmware do
|
||||
@moduledoc "Farmware is Farmbot's plugin system."
|
||||
|
||||
defmodule Meta do
|
||||
@moduledoc "Metadata about a Farmware."
|
||||
|
||||
defstruct [:author,:language,:description]
|
||||
end
|
||||
|
||||
defstruct [
|
||||
:name,
|
||||
:version,
|
||||
:min_os_version_major,
|
||||
:url,
|
||||
:zip,
|
||||
:executable,
|
||||
:args,
|
||||
:config,
|
||||
:meta,
|
||||
]
|
||||
|
||||
@doc "Lookup a farmware by it's name."
|
||||
def lookup(name, version \\ nil) do
|
||||
dir = Farmbot.Farmware.Installer.install_root_path
|
||||
with {:ok, all_installed} <- File.ls(dir),
|
||||
true <- name in all_installed,
|
||||
{:ok, versions} <- File.ls(Path.join(dir, name))
|
||||
do
|
||||
[newest | _] = Enum.sort(versions, fn(ver_a, ver_b) ->
|
||||
case Version.compare(ver_a, ver_b) do
|
||||
:eq -> true
|
||||
:gt -> true
|
||||
:lt -> false
|
||||
end
|
||||
end)
|
||||
to_fetch = (version || newest) |> Version.parse!()
|
||||
if "#{to_fetch}" in versions do
|
||||
mani_path = Path.join(Farmbot.Farmware.Installer.install_path(name, to_fetch), "manifest.json")
|
||||
File.read!(mani_path) |> Poison.decode! |> new()
|
||||
else
|
||||
{:error, :no_version}
|
||||
end
|
||||
else
|
||||
false -> {:error, :not_installed}
|
||||
{:error, _} = err -> err
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Creates a new Farmware Struct"
|
||||
def new(map) do
|
||||
with {:ok, name} <- extract_name(map),
|
||||
{:ok, version} <- extract_version(map),
|
||||
{:ok, os_req} <- extract_os_requirement(map),
|
||||
{:ok, url} <- extract_url(map),
|
||||
{:ok, zip} <- extract_zip(map),
|
||||
{:ok, exe} <- extract_exe(map),
|
||||
{:ok, args} <- extract_args(map),
|
||||
{:ok, config} <- extract_config(map),
|
||||
{:ok, meta} <- extract_meta(map)
|
||||
do
|
||||
res = struct(__MODULE__, [name: name,
|
||||
version: version,
|
||||
min_os_version_major: os_req,
|
||||
url: url,
|
||||
zip: zip,
|
||||
executable: exe,
|
||||
args: args,
|
||||
config: config,
|
||||
meta: meta])
|
||||
{:ok, res}
|
||||
else
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_name(%{"package" => name}) when is_binary(name), do: {:ok, name}
|
||||
defp extract_name(_), do: {:error, "bad or missing farmware name"}
|
||||
|
||||
defp extract_version(%{"version" => version}) do
|
||||
case Version.parse(version) do
|
||||
{:ok, _} = res -> res
|
||||
:error -> {:error, "Could not parse version."}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_version(_), do: {:error, "bad or missing farmware version"}
|
||||
|
||||
defp extract_os_requirement(%{"min_os_version_major" => num}) when is_number(num) do
|
||||
{:ok, num}
|
||||
end
|
||||
|
||||
defp extract_os_requirement(_), do: {:error, "bad or missing os requirement"}
|
||||
|
||||
defp extract_zip(%{"zip" => zip}) when is_binary(zip), do: {:ok, zip}
|
||||
defp extract_zip(_), do: {:error, "bad or missing farmware zip url"}
|
||||
|
||||
defp extract_url(%{"url" => url}) when is_binary(url), do: {:ok, url}
|
||||
defp extract_url(_), do: {:error, "bad or missing farmware url"}
|
||||
|
||||
defp extract_exe(%{"executable" => exe}) when is_binary(exe) do
|
||||
case System.find_executable(exe) do
|
||||
nil -> {:error, "#{exe} is not installed"}
|
||||
path -> {:ok, path}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_exe(_), do: {:error, "bad or missing farmware executable"}
|
||||
|
||||
defp extract_args(%{"args" => args}) when is_list(args) do
|
||||
if Enum.all?(args, &is_binary(&1)) do
|
||||
{:ok, args}
|
||||
else
|
||||
{:error, "invalid args"}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_args(_), do: {:error, "bad or missing farmware args"}
|
||||
|
||||
defp extract_config(map) do
|
||||
{:ok, Map.get(map, "config", [])}
|
||||
end
|
||||
|
||||
defp extract_meta(map) do
|
||||
desc = Map.get(map, "description", "no description provided.")
|
||||
lan = Map.get(map, "language", "no language provided.")
|
||||
auth = Map.get(map, "author", "no author provided")
|
||||
{:ok, struct(Meta, [description: desc, language: lan, author: auth])}
|
||||
end
|
||||
|
||||
end
|
8
lib/farmbot/farmware/inspect.ex
Normal file
8
lib/farmbot/farmware/inspect.ex
Normal file
|
@ -0,0 +1,8 @@
|
|||
defimpl Inspect, for: Farmbot.Farmware do
|
||||
def inspect(%{name: name, version: version}, _), do: "#Farmware<#{name}(#{version})>"
|
||||
def inspect(_thing, _), do: "#Farmware<:invalid>"
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Farmbot.Farmware.Meta do
|
||||
def inspect(meta, _), do: "#FarmwareMeta<#{meta.description}>"
|
||||
end
|
136
lib/farmbot/farmware/installer/installer.ex
Normal file
136
lib/farmbot/farmware/installer/installer.ex
Normal file
|
@ -0,0 +1,136 @@
|
|||
defmodule Farmbot.Farmware.Installer do
|
||||
@moduledoc "Install Farmware from a URL."
|
||||
|
||||
alias Farmbot.Farmware
|
||||
alias Farmbot.Farmware.Installer.Repository
|
||||
alias Farmbot.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@current_os_version Mix.Project.config[:version]
|
||||
|
||||
data_path = Application.get_env(:farmbot, :data_path) || raise "No configured data_path."
|
||||
@farmware_install_path Path.join(data_path, "farmware")
|
||||
|
||||
@doc "The root dir of farmware installs."
|
||||
def install_root_path, do: @farmware_install_path
|
||||
|
||||
@doc "Where on the filesystem is this Farmware installed."
|
||||
def install_path(name, %Version{} = fw_version) do
|
||||
Path.join([@farmware_install_path, name, fw_version |> to_string])
|
||||
end
|
||||
|
||||
@doc "Where on the filesystem is this Farmware installed."
|
||||
def install_path(%Farmware{name: name, version: version}) do
|
||||
install_path(name, version)
|
||||
end
|
||||
|
||||
@doc "Enable a repo from a url."
|
||||
def enable_repo(url_or_repo_struct, acc \\ [])
|
||||
|
||||
def enable_repo(url, _acc) when is_binary(url) do
|
||||
Logger.info "Enabling repo from: #{url}"
|
||||
with {:ok, %{status_code: code, body: body}} when code > 199 and code < 300 <- HTTP.get(url),
|
||||
{:ok, json_map} <- Poison.decode(body),
|
||||
{:ok, repo} <- Repository.new(json_map)
|
||||
do
|
||||
case enable_repo(repo, []) do
|
||||
[] ->
|
||||
Logger.info "Successfully enabled repo."
|
||||
:ok
|
||||
list_of_entries ->
|
||||
Logger.error "Failed to enable some entries: #{inspect list_of_entries}"
|
||||
{:error, list_of_entries}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def enable_repo(repo = %Repository{manifests: [entry = %Repository.Entry{manifest_url: manifest_url} | entries]}, acc) do
|
||||
case install(manifest_url) do
|
||||
:ok -> enable_repo(%{repo | manifests: entries}, acc)
|
||||
{:error, _err} -> enable_repo(%{repo | manifests: entries}, [entry | acc])
|
||||
end
|
||||
end
|
||||
|
||||
def enable_repo(%Repository{manifests: []}, acc), do: acc
|
||||
|
||||
@doc "Install a farmware from a URL."
|
||||
def install(url) do
|
||||
Logger.info "Installing farmware from #{url}."
|
||||
with {:ok, %{status_code: code, body: body}} when code > 199 and code < 300 <- HTTP.get(url),
|
||||
{:ok, json_map} <- Poison.decode(body),
|
||||
{:ok, farmware} <- Farmware.new(json_map),
|
||||
:ok <- preflight_checks(farmware) do
|
||||
finish_install(farmware, json_map)
|
||||
else
|
||||
{:error, {name, version, :already_installed}} ->
|
||||
Logger.info "Farmware #{name} - #{version} is already installed."
|
||||
:ok
|
||||
{:ok, %{status_code: code, body: body}} ->
|
||||
Logger.error "Failed to fetch Farmware manifest: #{inspect code}: #{body}"
|
||||
{:error, :bad_http_response}
|
||||
{:error, {:invalid, _, _}} ->
|
||||
Logger.error "Failed to parse json"
|
||||
{:error, :bad_json}
|
||||
{:error, reason} ->
|
||||
Logger.error "Failed to install farmware from #{url}: #{inspect reason}"
|
||||
{:error, reason}
|
||||
|
||||
err ->
|
||||
Logger.error "Unexpected error installing farmware. #{inspect err}"
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
defp preflight_checks(%Farmware{} = fw) do
|
||||
Logger.info "Starting preflight checks for #{inspect fw}"
|
||||
with :ok <- check_version(fw.min_os_version_major),
|
||||
:ok <- check_directory(fw.name, fw.version)
|
||||
do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def check_version(required_os_version) when is_number(required_os_version) do
|
||||
case Version.parse!(@current_os_version).major >= required_os_version do
|
||||
true -> :ok
|
||||
false -> {:error, "does not meet version requirement: #{required_os_version}"}
|
||||
end
|
||||
end
|
||||
|
||||
# sets up directories or returns already_installed.
|
||||
defp check_directory(fw_name, %Version{} = fw_version) do
|
||||
Logger.info "Checking directories for #{fw_name} - #{fw_version}"
|
||||
install_path = install_path(fw_name, fw_version)
|
||||
manifest_path = Path.join(install_path, "manifest.json")
|
||||
if File.exists?(manifest_path) do
|
||||
{:error, {fw_name, fw_version, :already_installed}}
|
||||
else
|
||||
File.mkdir_p(install_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the package and unzips it.
|
||||
defp finish_install(%Farmware{} = fw, json_map) do
|
||||
Logger.info "Finishing install for #{inspect fw}"
|
||||
zip_path = "/tmp/#{fw.name}-#{fw.version}.zip"
|
||||
zip_url = fw.zip
|
||||
with {:ok, ^zip_path} <- HTTP.download_file(zip_url, zip_path),
|
||||
:ok <- unzip(fw, zip_path),
|
||||
{:ok, json} <- Poison.encode(json_map)
|
||||
do
|
||||
manifest_path = Path.join(install_path(fw), "manifest.json")
|
||||
File.write(manifest_path,json)
|
||||
end
|
||||
end
|
||||
|
||||
defp unzip(%Farmware{} = fw, zip_path) do
|
||||
install_dir = install_path(fw)
|
||||
with {:ok, cur_dir} <- File.cwd,
|
||||
:ok <- File.cd(install_dir)
|
||||
do
|
||||
:zip.unzip(zip_path |> to_charlist())
|
||||
File.cd cur_dir
|
||||
end
|
||||
end
|
||||
end
|
21
lib/farmbot/farmware/installer/repository.ex
Normal file
21
lib/farmbot/farmware/installer/repository.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Farmbot.Farmware.Installer.Repository do
|
||||
@moduledoc "A Repository is a way of enabling multiple farmware's at a time."
|
||||
|
||||
defmodule Entry do
|
||||
@moduledoc "An entry in the Repository."
|
||||
defstruct [:name, :manifest_url]
|
||||
end
|
||||
|
||||
defstruct [manifests: []]
|
||||
|
||||
@doc "Turn a list into a Repo."
|
||||
def new(list, acc \\ struct(__MODULE__))
|
||||
|
||||
def new([%{"name" => name, "manifest" => mani_url} | rest], %__MODULE__{manifests: manifests} = acc) do
|
||||
entry = struct(Entry, name: name, manifest_url: mani_url)
|
||||
new(rest, %{acc | manifests: [entry | manifests]})
|
||||
end
|
||||
|
||||
def new([], acc), do: {:ok, acc}
|
||||
|
||||
end
|
3
lib/farmbot/farmware/supervisor.ex
Normal file
3
lib/farmbot/farmware/supervisor.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Farmbot.Farmware.Supervisor do
|
||||
@moduledoc false
|
||||
end
|
Loading…
Reference in a new issue