diff --git a/lib/recycledcloud/opennebula.ex b/lib/recycledcloud/opennebula.ex index daaca2a..9aa0d5b 100644 --- a/lib/recycledcloud/opennebula.ex +++ b/lib/recycledcloud/opennebula.ex @@ -1,10 +1,13 @@ defmodule RecycledCloud.OpenNebula do @moduledoc """ - OpenNebula XML-RPC Interface. + The OpenNebula context and XML-RPC Interface. See http://docs.opennebula.io/5.12/integration/system_interfaces/api.html for details. """ + alias RecycledCloud.OpenNebula.{VM, VMPool} + require Logger + # OpenNebula daemon. @endpoint "/RPC2" @@ -12,33 +15,77 @@ defmodule RecycledCloud.OpenNebula do Application.get_env(:recycledcloud, :opennebula, []) |> Keyword.get(key) end + def get_locations() do + get_opennebula_config(:locations) + |> Enum.map(fn m -> Map.get(m, :name) end) + end + + def get_location_config(name) do + get_opennebula_config(:locations) + |> Enum.find(fn m -> Map.get(m, :name) == name end) + end + ## # Related to XML-RPC calls - defp get_auth_string() do - "#{get_opennebula_config(:user)}:#{get_opennebula_config(:password)}" - end - - defp post!(call, endpoint) do - url = get_opennebula_config(:server) <> endpoint + defp post(url, request) do opts = [] headers = [] - body = call |> XMLRPC.encode! + body = request |> XMLRPC.encode! - response = HTTPoison.post!(url, body, headers, opts).body |> XMLRPC.decode! - case response do - %{fault_code: _, fault_string: err} -> {:error, err} - %{param: [false, err | _]} -> {:error, err} - %{param: [true, result | _]} -> {:ok, result} + query = HTTPoison.post(url, body, headers, opts) + case query do + {:ok, response} -> + case response.body |> XMLRPC.decode! do + %{fault_code: _, fault_string: err} -> {:error, err} + %{param: [false, err | _]} -> {:error, err} + %{param: [true, result | _]} -> {:ok, result} + end + {:error, %HTTPoison.Error{id: nil, reason: reason}} -> + Logger.warn("Error querying OpenNebula: #{inspect(reason)}") + {:error, reason} end end - def query(method, params) do - call = %XMLRPC.MethodCall{ + def query(location, method, params) do + user = Map.get(get_location_config(location), :user) + password = Map.get(get_location_config(location), :password) + auth_string = "#{user}:#{password}" + + request = %XMLRPC.MethodCall{ method_name: method, - params: [get_auth_string() | params] + params: [auth_string | params] } - call |> post!(@endpoint) + address = get_location_config(location) |> Map.get(:xmlrpc_address) + post(address <> @endpoint, request) + end + + ## + # Public / external calls. + + def get_vm(location, id), do: VM.get(id).(location) + + def get_vms_for_user(location, username) do + call = VMPool.get(%{ + filter_flag: :all, + range_start: :infinite, + range_end: :infinite, + state_filter: VM.state_for(:any_except_done), + kv_filter: "" + }) + + case call.(location) do + {:ok, %{VM: vms}} -> + Enum.filter(vms, + fn vm -> List.to_string(Map.get(vm, :UNAME)) == username end) + {:error, _err} -> %{} + end + end + + def get_vms_for_user(username) do + get_locations() + |> Enum.map(fn l -> %{l => get_vms_for_user(l, username)} end) + |> Enum.reduce(%{}, &Map.merge/2) end end diff --git a/lib/recycledcloud/opennebula/vm.ex b/lib/recycledcloud/opennebula/vm.ex index f06f1a6..9cac3ab 100644 --- a/lib/recycledcloud/opennebula/vm.ex +++ b/lib/recycledcloud/opennebula/vm.ex @@ -22,6 +22,68 @@ defmodule RecycledCloud.OpenNebula.VM do {:cloning_failure, 11} ] + @lcm_states [ + {:LCM_INIT , 0}, + {:PROLOG , 1}, + {:BOOT , 2}, + {:RUNNING , 3}, + {:MIGRATE , 4}, + {:SAVE_STOP , 5}, + {:SAVE_SUSPEND , 6}, + {:SAVE_MIGRATE , 7}, + {:PROLOG_MIGRATE , 8}, + {:PROLOG_RESUME , 9}, + {:EPILOG_STOP , 10}, + {:EPILOG , 11}, + {:SHUTDOWN , 12}, + {:CLEANUP_RESUBMIT , 15}, + {:UNKNOWN , 16}, + {:HOTPLUG , 17}, + {:SHUTDOWN_POWEROFF , 18}, + {:BOOT_UNKNOWN , 19}, + {:BOOT_POWEROFF , 20}, + {:BOOT_SUSPENDED , 21}, + {:BOOT_STOPPED , 22}, + {:CLEANUP_DELETE , 23}, + {:HOTPLUG_SNAPSHOT , 24}, + {:HOTPLUG_NIC , 25}, + {:HOTPLUG_SAVEAS , 26}, + {:HOTPLUG_SAVEAS_POWEROFF , 27}, + {:HOTPLUG_SAVEAS_SUSPENDED , 28}, + {:SHUTDOWN_UNDEPLOY , 29}, + {:EPILOG_UNDEPLOY , 30}, + {:PROLOG_UNDEPLOY , 31}, + {:BOOT_UNDEPLOY , 32}, + {:HOTPLUG_PROLOG_POWEROFF , 33}, + {:HOTPLUG_EPILOG_POWEROFF , 34}, + {:BOOT_MIGRATE , 35}, + {:BOOT_FAILURE , 36}, + {:BOOT_MIGRATE_FAILURE , 37}, + {:PROLOG_MIGRATE_FAILURE , 38}, + {:PROLOG_FAILURE , 39}, + {:EPILOG_FAILURE , 40}, + {:EPILOG_STOP_FAILURE , 41}, + {:EPILOG_UNDEPLOY_FAILURE , 42}, + {:PROLOG_MIGRATE_POWEROFF , 43}, + {:PROLOG_MIGRATE_POWEROFF_FAILURE, 44}, + {:PROLOG_MIGRATE_SUSPEND , 45}, + {:PROLOG_MIGRATE_SUSPEND_FAILURE , 46}, + {:BOOT_UNDEPLOY_FAILURE , 47}, + {:BOOT_STOPPED_FAILURE , 48}, + {:PROLOG_RESUME_FAILURE , 49}, + {:PROLOG_UNDEPLOY_FAILURE , 50}, + {:DISK_SNAPSHOT_POWEROFF , 51}, + {:DISK_SNAPSHOT_REVERT_POWEROFF , 52}, + {:DISK_SNAPSHOT_DELETE_POWEROFF , 53}, + {:DISK_SNAPSHOT_SUSPENDED , 54}, + {:DISK_SNAPSHOT_REVERT_SUSPENDED , 55}, + {:DISK_SNAPSHOT_DELETE_SUSPENDED , 56}, + {:DISK_SNAPSHOT , 57}, + {:DISK_SNAPSHOT_DELETE , 59}, + {:PROLOG_MIGRATE_UNKNOWN , 60}, + {:PROLOG_MIGRATE_UNKNOWN_FAILURE , 61}, + ] + @actions [ "terminate-hard", "terminate", @@ -91,17 +153,17 @@ defmodule RecycledCloud.OpenNebula.VM do {:ALIAS_DETACH_ACTION , 47}, ] - def state_for(state) when is_atom(state) do - @states |> Keyword.get(state) - end - - def state_for(state) when is_integer(state) do - case Enum.find(@states, fn {_atom, value} -> value == state end) do + defp find(table, state) when is_integer(state) do + case Enum.find(table, fn {_atom, value} -> value == state end) do {atom, _value} -> atom nil -> nil end end + def state_for(state) when is_atom(state), do: @states |> Keyword.get(state) + def state_for(state) when is_integer(state), do: find(@states, state) + def lcm_state_for(state) when is_integer(state), do: find(@lcm_states, state) + def event_for(event) when is_integer(event) do case Enum.find(@events, fn {_atom, value} -> value == event end) do {atom, _value} -> atom @@ -110,32 +172,24 @@ defmodule RecycledCloud.OpenNebula.VM do end def get(id) do - case ONE.query("one.vm.info", [id]) do - {:ok, raw} -> - data = raw - |> Schema.scan("vm") - |> Schema.map_record - {:ok, data} - {:error, err} -> {:error, err} + fn location -> + case ONE.query(location, "one.vm.info", [id]) do + {:ok, raw} -> + data = raw + |> Schema.scan("vm") + |> Schema.map_record + {:ok, data} + {:error, err} -> {:error, err} + end end end - def get_by_username(username) do - {:ok, %{VM: vms}} = RecycledCloud.OpenNebula.VMPool.get(%{ - filter_flag: :all, - range_start: :infinite, - range_end: :infinite, - state_filter: state_for(:any_except_done), - kv_filter: "" - }) - - Enum.filter(vms, fn vm -> List.to_string(Map.get(vm, :UNAME)) == username end) - end - def execute(vm_id, action) when action in @actions do - case ONE.query("one.vm.action", [action, vm_id]) do - {:ok, _raw} -> :ok - {:error, err} -> {:error, err} + fn location -> + case ONE.query(location, "one.vm.action", [action, vm_id]) do + {:ok, _raw} -> :ok + {:error, err} -> {:error, err} + end end end end diff --git a/lib/recycledcloud/opennebula/vm_pool.ex b/lib/recycledcloud/opennebula/vm_pool.ex index e5d5e40..a33a777 100644 --- a/lib/recycledcloud/opennebula/vm_pool.ex +++ b/lib/recycledcloud/opennebula/vm_pool.ex @@ -40,14 +40,16 @@ defmodule RecycledCloud.OpenNebula.VMPool do kv_filter: kv_filter }) do - params = [filter_flag, range_start, range_end, state, kv_filter] - case ONE.query("one.vmpool.info", params) do - {:ok, raw} -> - data = raw - |> Schema.scan("vm_pool") - |> Schema.map_record - {:ok, data} - {:error, err} -> {:error, err} + fn location -> + params = [filter_flag, range_start, range_end, state, kv_filter] + case ONE.query(location, "one.vmpool.info", params) do + {:ok, raw} -> + data = raw + |> Schema.scan("vm_pool") + |> Schema.map_record + {:ok, data} + {:error, err} -> {:error, err} + end end end end diff --git a/lib/recycledcloud_web/controllers/virtual_machine_hosting_controller.ex b/lib/recycledcloud_web/controllers/virtual_machine_hosting_controller.ex index c9d056e..b38775d 100644 --- a/lib/recycledcloud_web/controllers/virtual_machine_hosting_controller.ex +++ b/lib/recycledcloud_web/controllers/virtual_machine_hosting_controller.ex @@ -1,43 +1,43 @@ defmodule RecycledCloudWeb.VirtualMachineHostingController do use RecycledCloudWeb, :controller - alias RecycledCloud.OpenNebula.VM + alias RecycledCloud.OpenNebula, as: ONE def index(conn, _params) do username = conn.assigns.current_user.username - vms = VM.get_by_username(username) + vms = ONE.get_vms_for_user(username) render(conn, "index.html", vms: vms) end - def start(conn, %{"id" => id}) do - case VM.execute(String.to_integer(id), "resume") do - :ok -> - conn - |> put_flash(:info, "Start request sent to VMM.") - |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) - {:error, err} -> - conn - |> put_flash(:error, "Something went wrong: #{inspect(err)}") - |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) - end - end +# def start(conn, %{"id" => id}) do +# case VM.execute(String.to_integer(id), "resume") do +# :ok -> +# conn +# |> put_flash(:info, "Start request sent to VMM.") +# |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) +# {:error, err} -> +# conn +# |> put_flash(:error, "Something went wrong: #{inspect(err)}") +# |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) +# end +# end +# +# def stop(conn, %{"id" => id}) do +# case VM.execute(String.to_integer(id), "poweroff") do +# :ok -> +# conn +# |> put_flash(:stop, "Stop request sent to VMM.") +# |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) +# {:error, err} -> +# conn +# |> put_flash(:error, "Something went wrong: #{inspect(err)}") +# |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) +# end +# end - def stop(conn, %{"id" => id}) do - case VM.execute(String.to_integer(id), "poweroff") do - :ok -> - conn - |> put_flash(:stop, "Stop request sent to VMM.") - |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) - {:error, err} -> - conn - |> put_flash(:error, "Something went wrong: #{inspect(err)}") - |> redirect(to: Routes.virtual_machine_hosting_path(conn, :index)) - end - end - - def show(conn, %{"id" => id}) do - case VM.get(String.to_integer(id)) do + def show(conn, %{"location" => location, "id" => id}) do + case ONE.get_vm(location, String.to_integer(id)) do {:error, err} -> conn |> put_flash(:error, "Could not fetch VM details: #{inspect(err)}") @@ -46,6 +46,7 @@ defmodule RecycledCloudWeb.VirtualMachineHostingController do owner = vm |> Map.get(:UNAME) |> List.to_string if owner == conn.assigns.current_user.username do conn + |> assign(:location, location) |> assign(:vm, vm) |> render("show.html") else diff --git a/lib/recycledcloud_web/router.ex b/lib/recycledcloud_web/router.ex index f409e64..0f05658 100644 --- a/lib/recycledcloud_web/router.ex +++ b/lib/recycledcloud_web/router.ex @@ -76,9 +76,7 @@ defmodule RecycledCloudWeb.Router do post "/billing/partner/update", BillingController, :update get "/hosting/vm", VirtualMachineHostingController, :index - get "/hosting/vm/:id", VirtualMachineHostingController, :show - get "/hosting/vm/:id/start", VirtualMachineHostingController, :start - get "/hosting/vm/:id/stop", VirtualMachineHostingController, :stop + get "/hosting/vm/:location/:id", VirtualMachineHostingController, :show end scope "/", RecycledCloudWeb do diff --git a/lib/recycledcloud_web/templates/virtual_machine_hosting/index.html.eex b/lib/recycledcloud_web/templates/virtual_machine_hosting/index.html.eex index 9a134ad..c2c4dbf 100644 --- a/lib/recycledcloud_web/templates/virtual_machine_hosting/index.html.eex +++ b/lib/recycledcloud_web/templates/virtual_machine_hosting/index.html.eex @@ -1,36 +1,37 @@
This page list all the Virtual Machines linked to your account. It is -not possible to interect with them yet.
+This page list all the Virtual Machines linked to your account. Note that +SSH keys are not (yet) synced with OpenNebula: you'll have to add your key in +<%= @current_user.username %> (top-right) > Settings > Add SSH Key in +OpenNebula for every location you want to use.
-<%= for location <- ["LNTH"] do %> +<%= for location <- Map.keys(@vms) do %>+ You can access the OpenNebula dashboard with your LDAP credentials at: + + <%= Map.get(ONE.get_location_config(location), :public_address) %> + +
+# | -Name | -State | -Actions | +ID | +Name | +State | +Actions |
---|---|---|---|---|---|---|---|
<%= Map.get(vm, :ID) %> | +<%= location %>#<%= Map.get(vm, :ID) %> | <%= Map.get(vm, :NAME) %> | -<%= VM.state_for(Map.get(vm, :STATE)) %> | +<%= render_state(vm) %> | - <%= case VM.state_for(Map.get(vm, :STATE)) do - :poweroff -> - link "start", to: Routes.virtual_machine_hosting_path(@conn, :start, Map.get(vm, :ID)) - :active -> - link "stop", to: Routes.virtual_machine_hosting_path(@conn, :stop, Map.get(vm, :ID)) - _ -> "" - end %> - - <%= link "show details", to: Routes.virtual_machine_hosting_path(@conn, :show, Map.get(vm, :ID)) %> + <%= link "Show history ยป", to: Routes.virtual_machine_hosting_path(@conn, :show, location, Map.get(vm, :ID)) %> |