Redo Farmware installation and lookup.

This commit is contained in:
Connor Rigby 2017-10-31 11:18:49 -07:00
parent f67590ba76
commit ff37b75093
5 changed files with 298 additions and 0 deletions

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,3 @@
defmodule Farmbot.Farmware.Supervisor do
@moduledoc false
end