diff --git a/lib/entity/henforcer/entity.ex b/lib/entity/henforcer/entity.ex index e640c5ee..887567bd 100644 --- a/lib/entity/henforcer/entity.ex +++ b/lib/entity/henforcer/entity.ex @@ -2,8 +2,13 @@ defmodule Helix.Entity.Henforcer.Entity do import Helix.Henforcer + alias Helix.Network.Model.Network + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Model.Component alias Helix.Server.Model.Server + alias Helix.Server.Henforcer.Component, as: ComponentHenforcer alias Helix.Server.Henforcer.Server, as: ServerHenforcer + alias Helix.Server.Query.Component, as: ComponentQuery alias Helix.Entity.Model.Entity alias Helix.Entity.Query.Entity, as: EntityQuery @@ -63,4 +68,100 @@ defmodule Helix.Entity.Henforcer.Entity do end |> wrap_relay(%{entity: entity, server: server}) end + + @type owns_component_relay :: + %{entity: Entity.t, component: Component.t, owned_components: [Component.t]} + @type owns_component_relay_partial :: owns_component_relay + @type owns_component_error :: + {false, {:component, :not_belongs}, owns_component_relay_partial} + | ComponentHenforcer.component_exists_error + | entity_exists_error + + @spec owns_component?(Entity.idt, Component.idt, [Component.t] | nil) :: + {true, owns_component_relay} + | owns_component_error + @doc """ + Henforces the Entity is the owner of the given component. The third parameter, + `owned`, allows users of this function to pass a previously fetched list of + components owned by the entity (cache). + """ + def owns_component?(entity_id = %Entity.ID{}, component, owned) do + henforce entity_exists?(entity_id) do + owns_component?(relay.entity, component, owned) + end + end + + def owns_component?(entity, component_id = %Component.ID{}, owned) do + henforce(ComponentHenforcer.component_exists?(component_id)) do + owns_component?(entity, relay.component, owned) + end + end + + def owns_component?(entity = %Entity{}, component = %Component{}, nil) do + owned_components = + entity + |> EntityQuery.get_components() + |> Enum.map(&(ComponentQuery.fetch(&1.component_id))) + + owns_component?(entity, component, owned_components) + end + + def owns_component?(entity = %Entity{}, component = %Component{}, owned) do + if component in owned do + reply_ok() + else + reply_error({:component, :not_belongs}) + end + |> wrap_relay( + %{entity: entity, component: component, owned_components: owned} + ) + end + + @type owns_nip_relay :: + %{ + network_connection: Network.Connection.t, + entity: Entity.t, + entity_network_connections: [Network.Connection.t] + } + @type owns_nip_relay_partial :: + %{ + entity: Entity.t, + entity_network_connections: [Network.Connection.t] + } + @type owns_nip_error :: + {false, {:network_connection, :not_belongs}, owns_nip_relay_partial} + | entity_exists_error + + @typep owned_ncs :: [Network.Connection.t] | nil + + @spec owns_nip?(Entity.idt, Network.id, Network.ip, owned_ncs) :: + {true, owns_nip_relay} + | owns_nip_error + @doc """ + Henforces the Entity is the owner of the given NIP (NetworkConnection). The + third parameter, `owned`, allows users of this function to pass a previously + fetched list of NCs owned by the entity (cache). + """ + def owns_nip?(entity_id = %Entity.ID{}, network_id, ip, owned) do + henforce entity_exists?(entity_id) do + owns_nip?(relay.entity, network_id, ip, owned) + end + end + + def owns_nip?(entity = %Entity{}, network_id, ip, nil) do + owned_nips = NetworkQuery.Connection.get_by_entity(entity.entity_id) + + owns_nip?(entity, network_id, ip, owned_nips) + end + + def owns_nip?(entity = %Entity{}, network_id, ip, owned) do + nc = Enum.find(owned, &(&1.network_id == network_id and &1.ip == ip)) + + if nc do + reply_ok(%{network_connection: nc}) + else + reply_error({:network_connection, :not_belongs}) + end + |> wrap_relay(%{entity_network_connections: owned, entity: entity}) + end end diff --git a/lib/event/dispatcher.ex b/lib/event/dispatcher.ex index d057b5d7..8c4bb848 100644 --- a/lib/event/dispatcher.ex +++ b/lib/event/dispatcher.ex @@ -133,6 +133,8 @@ defmodule Helix.Event.Dispatcher do ############################################################################## # All + event ServerEvent.Motherboard.Updated + event ServerEvent.Motherboard.UpdateFailed event ServerEvent.Server.Password.Acquired event ServerEvent.Server.Joined diff --git a/lib/henforcer/henforcer.ex b/lib/henforcer/henforcer.ex index 09be6f07..6896d110 100644 --- a/lib/henforcer/henforcer.ex +++ b/lib/henforcer/henforcer.ex @@ -123,7 +123,6 @@ defmodule Helix.Henforcer do @spec is_b?(x) :: {true, is_b_relay} | is_b_error - | can_d_error def is_b?(x) def is_c?(y) @@ -310,6 +309,7 @@ defmodule Helix.Henforcer do case unquote(henforcer) do {true, sub_relay} -> {true, relay(unquote(relay), sub_relay)} + {false, reason, sub_relay} -> {false, reason, relay(unquote(relay), sub_relay)} end diff --git a/lib/network/action/network/connection.ex b/lib/network/action/network/connection.ex index 3b72878e..31acf7b2 100644 --- a/lib/network/action/network/connection.ex +++ b/lib/network/action/network/connection.ex @@ -39,7 +39,7 @@ defmodule Helix.Network.Action.Network.Connection do end end - @spec update_nic(Network.Connection.t, Component.nic) :: + @spec update_nic(Network.Connection.t, Component.nic | nil) :: {:ok, Network.Connection.t} | {:error, :internal} @doc """ diff --git a/lib/network/internal/network/connection.ex b/lib/network/internal/network/connection.ex index b6f88a2c..27636a30 100644 --- a/lib/network/internal/network/connection.ex +++ b/lib/network/internal/network/connection.ex @@ -56,12 +56,12 @@ defmodule Helix.Network.Internal.Network.Connection do |> Repo.insert() end - @spec update_nic(Network.Connection.t, Component.nic) :: + @spec update_nic(Network.Connection.t, Component.nic | nil) :: repo_result @doc """ Updates the NIC assigned to the NetworkConnection """ - def update_nic(nc = %Network.Connection{}, new_nic = %Component{}) do + def update_nic(nc = %Network.Connection{}, new_nic) do nc |> Network.Connection.update_nic(new_nic) |> Repo.update() diff --git a/lib/network/model/network/connection.ex b/lib/network/model/network/connection.ex index f7866765..4d60a253 100644 --- a/lib/network/model/network/connection.ex +++ b/lib/network/model/network/connection.ex @@ -74,6 +74,15 @@ defmodule Helix.Network.Model.Network.Connection do |> validate_required(@required_fields) end + @spec update_nic(t, nil) :: + changeset + def update_nic(nc = %__MODULE__{}, nil) do + nc + |> change + |> put_change(:nic_id, nil) + |> validate_required(@required_fields) + end + @spec update_ip(t, ip) :: changeset def update_ip(nc = %__MODULE__{}, new_ip) do diff --git a/lib/process/public/view/process.ex b/lib/process/public/view/process.ex index 8c5932a4..e6a3765f 100644 --- a/lib/process/public/view/process.ex +++ b/lib/process/public/view/process.ex @@ -19,7 +19,7 @@ defmodule Helix.Process.Public.View.Process do :full | :partial - @typep process(access_type) :: + @typep process(access) :: %{ process_id: String.t, target_ip: String.t, @@ -28,7 +28,7 @@ defmodule Helix.Process.Public.View.Process do file: file, state: String.t, type: String.t, - access: access_type + access: access } @typep full_access :: diff --git a/lib/process/resources.ex b/lib/process/resources.ex index e81690f4..8317f8c8 100644 --- a/lib/process/resources.ex +++ b/lib/process/resources.ex @@ -99,7 +99,8 @@ defmodule Helix.Process.Resources do they are identical to the resource's initial value. """ def reject_empty(resources) do - Enum.reject(resources, fn {res, val} -> + resources + |> Enum.reject(fn {res, val} -> val == call_resource(res, :initial, []) end) |> Map.new() diff --git a/lib/server/action/flow/motherboard.ex b/lib/server/action/flow/motherboard.ex index 04005e60..9869c5ff 100644 --- a/lib/server/action/flow/motherboard.ex +++ b/lib/server/action/flow/motherboard.ex @@ -72,7 +72,7 @@ defmodule Helix.Server.Action.Flow.Motherboard do [Motherboard.slot] defp map_components_slots(components) do Enum.map(components, fn component -> - {component, Utils.concat_atom(component.type, :_0)} + {component, Utils.concat_atom(component.type, :_1)} end) end diff --git a/lib/server/action/flow/server.ex b/lib/server/action/flow/server.ex index 1fbb1f88..7a77fd6c 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -5,10 +5,20 @@ defmodule Helix.Server.Action.Flow.Server do alias Helix.Event alias Helix.Entity.Action.Entity, as: EntityAction alias Helix.Entity.Model.Entity + alias Helix.Network.Model.Network + alias Helix.Server.Action.Motherboard, as: MotherboardAction alias Helix.Server.Action.Server, as: ServerAction alias Helix.Server.Model.Component + alias Helix.Server.Model.Motherboard alias Helix.Server.Model.Server + alias Helix.Server.Event.Motherboard.Updated, as: MotherboardUpdatedEvent + alias Helix.Server.Event.Motherboard.UpdateFailed, + as: MotherboardUpdateFailedEvent + + @type update_mobo_result :: {:ok, Server.t, Motherboard.t} + @type detach_mobo_result :: {:ok, Server.t} + @spec setup(Server.type, Entity.t, Component.mobo, Event.relay) :: {:ok, Server.t} @doc """ @@ -43,4 +53,95 @@ defmodule Helix.Server.Action.Flow.Server do """ def set_hostname(server, hostname, _relay), do: ServerAction.set_hostname(server, hostname) + + @spec update_mobo( + Server.t, + Motherboard.t, + MotherboardAction.motherboard_data, + entity_ncs :: [Network.Connection.t], + relay :: Event.relay) + :: + update_mobo_result + @doc """ + Updates the server motherboard. + + `new_mobo_data` holds all information about how the new motherboard should + look like. + + Emits `MotherboardUpdatedEvent` in case of success, and + `MotherboardUpdateFailedEvent` otherwise. + """ + def update_mobo( + server = %Server{}, + motherboard, + new_mobo_data, + entity_ncs, + relay) + do + new_mobo_id = new_mobo_data.mobo.component_id + + flowing do + with \ + {:ok, new_motherboard, events} <- + MotherboardAction.update(motherboard, new_mobo_data, entity_ncs), + on_success(fn -> Event.emit(events, from: relay) end), + + {:ok, new_server} <- update_server_mobo(server, new_mobo_id) + do + emit_motherboard_updated(new_server, relay) + + {:ok, new_server, new_motherboard} + else + _ -> + emit_motherboard_update_failed(server, :internal, relay) + end + end + end + + @spec detach_mobo(Server.t, Motherboard.t, Event.relay) :: + detach_mobo_result + @doc """ + Detaches the server motherboard. + + Emits `MotherboardUpdatedEvent` in case of success, and + `MotherboardUpdateFailedEvent` otherwise. + """ + def detach_mobo(server = %Server{}, motherboard = %Motherboard{}, relay) do + flowing do + with \ + :ok <- MotherboardAction.detach(motherboard), + {:ok, new_server} <- ServerAction.detach(server) + do + emit_motherboard_updated(new_server, relay) + + {:ok, new_server} + else + _ -> + emit_motherboard_update_failed(server, :internal, relay) + end + end + end + + @spec update_server_mobo(Server.t, Component.id) :: + {:ok, Server.t} + defp update_server_mobo(server = %Server{motherboard_id: mobo_id}, mobo_id), + do: {:ok, server} + defp update_server_mobo(server, mobo_id), + do: ServerAction.attach(server, mobo_id) + + @spec emit_motherboard_updated(Server.t, Event.relay) :: + term + defp emit_motherboard_updated(server, relay) do + server + |> MotherboardUpdatedEvent.new() + |> Event.emit(from: relay) + end + + @spec emit_motherboard_update_failed(Server.t, term, Event.relay) :: + term + defp emit_motherboard_update_failed(server, reason, relay) do + server + |> MotherboardUpdateFailedEvent.new(reason) + |> Event.emit(from: relay) + end end diff --git a/lib/server/action/motherboard.ex b/lib/server/action/motherboard.ex index eb3e5979..834cdd31 100644 --- a/lib/server/action/motherboard.ex +++ b/lib/server/action/motherboard.ex @@ -2,6 +2,10 @@ defmodule Helix.Server.Action.Motherboard do alias Helix.Server.Internal.Motherboard, as: MotherboardInternal + @type motherboard_data :: __MODULE__.Update.motherboard_data + @type update_component :: __MODULE__.Update.update_component + @type update_nc :: __MODULE__.Update.update_nc + defdelegate setup(mobo, initial_components), to: MotherboardInternal @@ -12,4 +16,10 @@ defmodule Helix.Server.Action.Motherboard do defdelegate unlink(component), to: MotherboardInternal + + defdelegate update(cur_mobo_data, new_mobo_data, entity_ncs), + to: __MODULE__.Update + + defdelegate detach(motherboard), + to: __MODULE__.Update end diff --git a/lib/server/action/motherboard/update.ex b/lib/server/action/motherboard/update.ex new file mode 100644 index 00000000..b9fadf8a --- /dev/null +++ b/lib/server/action/motherboard/update.ex @@ -0,0 +1,179 @@ +defmodule Helix.Server.Action.Motherboard.Update do + + import HELL.Macros + + alias Helix.Network.Action.Network, as: NetworkAction + alias Helix.Network.Model.Network + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Action.Component, as: ComponentAction + alias Helix.Server.Model.Component + alias Helix.Server.Model.Motherboard + alias Helix.Server.Internal.Motherboard, as: MotherboardInternal + alias Helix.Server.Query.Component, as: ComponentQuery + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + alias Helix.Server.Repo, as: ServerRepo + + @internet_id NetworkQuery.internet().network_id + + @type motherboard_data :: + %{ + mobo: Component.mobo, + components: [update_component], + network_connections: [update_nc] + } + + @type update_component :: {Component.pluggable, Motherboard.slot_id} + @type update_nc :: + %{ + nic_id: Component.id, + network_id: Network.id, + ip: Network.ip, + network_connection: Network.Connection.t + } + + @spec detach(Motherboard.t) :: + :ok + @doc """ + Detaches the `motherboard`. + """ + def detach(motherboard = %Motherboard{}) do + MotherboardInternal.unlink_all(motherboard) + + # Network configuration is asynchronous + hespawn fn -> + motherboard + |> MotherboardQuery.get_nics() + |> Enum.each(fn nic -> + nc = NetworkQuery.Connection.fetch_by_nic(nic.component_id) + + if nc do + perform_network_op({:nilify_nic, nc}) + end + end) + end + + :ok + end + + @spec update(Motherboard.t | nil, motherboard_data, [Network.Connection.t]) :: + {:ok, Motherboard.t, [term]} + @doc """ + Updates the motherboard. + + First parameter is the current motherboard. If `nil`, a motherboard is being + attached to a server that was currently without motherboard. + + Updating a motherboard is - as of now - quite naive: we simply unlink all + existing components and then link what was specified by the user. + """ + def update(nil, mobo_data, entity_ncs) do + {:ok, new_mobo} = + MotherboardInternal.setup(mobo_data.mobo, mobo_data.components) + + # Network configuration is asynchronous + hespawn fn -> + update_network_connections(mobo_data, entity_ncs) + end + + {:ok, new_mobo, []} + end + + def update( + motherboard, + mobo_data, + entity_ncs) + do + {:ok, {:ok, new_mobo}} = + ServerRepo.transaction fn -> + MotherboardInternal.unlink_all(motherboard) + MotherboardInternal.setup(mobo_data.mobo, mobo_data.components) + end + + # Network configuration is asynchronous + hespawn fn -> + update_network_connections(mobo_data, entity_ncs) + end + + {:ok, new_mobo, []} + end + + @spec update_network_connections(motherboard_data, [Network.Connection.t]) :: + term + docp """ + Iterates through the player's network connections (NCs), as well as all NCs + assigned to the new motherboard configuration. + + This iteration detects which (if any) NC should be updated, either because it + was previously attached to the motherboard and was removed, or because it was + not previously attached to any mobo but now it is. + """ + defp update_network_connections(mobo_data, entity_ncs) do + ncs = mobo_data.network_connections + + entity_ncs + + # Get the required operations we may have to do on NetworkConnections... + |> Enum.reduce([], fn nc, acc -> + cond do + # The NIC already has an NC attached to it + mobo_nc = has_nic?(ncs, nc.nic_id) -> + # The given NC is the same as before; we don't have to do anything + if nc == mobo_nc.network_connection do + acc + + # There will be a new NC attached to this NIC, so we have to + # remove the previous NC reference to this NIC, as it's no longer + # used. Certainly we'll also have to update the new NC to point + # to this NIC. That's done on another iteration at :set_nic below + else + acc ++ [{:nilify_nic, nc}] + end + + # TODO: What if NIP is in use? Henforce! + # The current NC nic is not in use, but its nip is being assigned. + # This means the NC will start being used, so we need to link it to + # the underlying NIC. + mobo_nc = has_nip?(ncs, nc.network_id, nc.ip) -> + acc ++ [{:set_nic, nc, mobo_nc.nic_id}] + + # This NC is not modified at all by the mobo update + true -> + acc + end + end) + + # Perform those NetworkConnection operations + |> Enum.each(&perform_network_op/1) + end + + @spec has_nic?([update_nc], Component.id) :: + boolean + defp has_nic?(ncs, nic_id), + do: Enum.find(ncs, &(&1.nic_id == nic_id)) + + @spec has_nip?([update_nc], Network.id, Network.ip) :: + boolean + defp has_nip?(ncs, network_id, ip), + do: Enum.find(ncs, &(&1.network_id == network_id and &1.ip == ip)) + + @typep network_op_input :: + {:nilify_nic, Network.Connection.t} + | {:set_nic, Network.Connection.t, Component.id} + + @spec perform_network_op(network_op_input) :: + term + defp perform_network_op({:nilify_nic, nc = %Network.Connection{}}), + do: {:ok, _} = NetworkAction.Connection.update_nic(nc, nil) + defp perform_network_op({:set_nic, nc = %Network.Connection{}, nic_id}) do + nic = ComponentQuery.fetch(nic_id) + + {:ok, _} = NetworkAction.Connection.update_nic(nc, nic) + + # Update the NIC custom + # Note that by default the NIC is assumed to belong to the internet, that's + # why we'll only update it in case it's on a different network. + unless nc.network_id == @internet_id do + ComponentAction.NIC.update_network_id(nic, nc.network_id) + end + end +end diff --git a/lib/server/action/server.ex b/lib/server/action/server.ex index 89301f07..40a3c013 100644 --- a/lib/server/action/server.ex +++ b/lib/server/action/server.ex @@ -41,7 +41,7 @@ defmodule Helix.Server.Action.Server do @spec attach(Server.t, Motherboard.id) :: {:ok, Server.t} - | {:error, Ecto.Changeset.t} + | {:error, :internal} @doc """ Attaches a motherboard to the server @@ -49,18 +49,31 @@ defmodule Helix.Server.Action.Server do are already attached """ def attach(server, motherboard_id) do - ServerInternal.attach(server, motherboard_id) + case ServerInternal.attach(server, motherboard_id) do + {:ok, server} -> + {:ok, server} + + {:error, _} -> + {:error, :internal} + end end @spec detach(Server.t) :: - :ok + {:ok, Server.t} + | {:error, :internal} @doc """ Detaches the motherboard linked to server This function is idempotent """ def detach(server) do - ServerInternal.detach(server) + case ServerInternal.detach(server) do + {:ok, server} -> + {:ok, server} + + {:error, _} -> + {:error, :internal} + end end @spec delete(Server.t) :: diff --git a/lib/server/component/specs/specable.ex b/lib/server/component/specs/specable.ex index cc2184cb..52b2d546 100644 --- a/lib/server/component/specs/specable.ex +++ b/lib/server/component/specs/specable.ex @@ -280,10 +280,10 @@ defmodule Helix.Server.Component.Specable do price: 100, slots: %{ - cpu: %{0 => %{}}, - ram: %{0 => %{}}, - hdd: %{0 => %{}}, - nic: %{0 => %{}}, + cpu: %{1 => %{}}, + ram: %{1 => %{}}, + hdd: %{1 => %{}}, + nic: %{1 => %{}}, usb: %{} } } @@ -295,11 +295,11 @@ defmodule Helix.Server.Component.Specable do price: 200, slots: %{ - cpu: %{0 => %{}, 1 => %{}}, - ram: %{0 => %{}, 1 => %{}}, - hdd: %{0 => %{}}, - nic: %{0 => %{}}, - usb: %{0 => %{}} + cpu: %{1 => %{}, 2 => %{}}, + ram: %{1 => %{}, 2 => %{}}, + hdd: %{1 => %{}}, + nic: %{1 => %{}}, + usb: %{1 => %{}} } } end @@ -310,11 +310,11 @@ defmodule Helix.Server.Component.Specable do price: 999_999_999, slots: %{ - cpu: %{0 => %{}, 1 => %{}, 2 => %{}, 3 => %{}}, - ram: %{0 => %{}, 1 => %{}, 2 => %{}, 3 => %{}}, - hdd: %{0 => %{}, 1 => %{}, 2 => %{}, 3 => %{}}, - nic: %{0 => %{}, 1 => %{}, 2 => %{}, 3 => %{}}, - usb: %{0 => %{}, 1 => %{}, 2 => %{}, 3 => %{}} + cpu: %{1 => %{}, 2 => %{}, 3 => %{}, 4 => %{}}, + ram: %{1 => %{}, 2 => %{}, 3 => %{}, 4 => %{}}, + hdd: %{1 => %{}, 2 => %{}, 3 => %{}, 4 => %{}}, + nic: %{1 => %{}, 2 => %{}, 3 => %{}, 4 => %{}}, + usb: %{1 => %{}, 2 => %{}, 3 => %{}, 4 => %{}} } } end diff --git a/lib/server/event/motherboard.ex b/lib/server/event/motherboard.ex new file mode 100644 index 00000000..4f859e2a --- /dev/null +++ b/lib/server/event/motherboard.ex @@ -0,0 +1,119 @@ +defmodule Helix.Server.Event.Motherboard do + + import Helix.Event + + event Updated do + @moduledoc """ + `MotherboardUpdatedEvent` is fired when the server motherboard has changed + as a result of a player's action. Changes include removal of the mobo + (detach) as well as (un)linking components. + + This data is Notificable, i.e. sent to the Client. The client receives the + new motherboard data through HardwareIndex (same data sent during the + bootstrap step). + """ + + alias Helix.Server.Model.Server + alias Helix.Server.Public.Index.Hardware, as: HardwareIndex + + event_struct [:server, :index_cache] + + @type t :: %__MODULE__{ + server: Server.t, + index_cache: HardwareIndex.index + } + + @spec new(Server.t) :: + t + def new(server = %Server{}) do + %__MODULE__{ + server: server, + + # `index_cache` is a cache of the new server hardware index (bootstrap) + # We save it on the event struct so it is only generated once; + # otherwise it would have to be recalculated to every player joined + # on the server channel. + # We save the full cache (`:local`) and, if the Notificable receiver is + # a remote server, we nilify the `:motherboard` entry + index_cache: HardwareIndex.index(server, :local) + } + end + + notify do + + @event :motherboard_updated + + @doc """ + The player (server channel with `local` access) receives the full hardware + index, while any remote connection receives only remote data (like total + hardware resources). + """ + def generate_payload(event, %{assigns: %{meta: %{access: :local}}}) do + data = HardwareIndex.render_index(event.index_cache) + + {:ok, data} + end + + def generate_payload(event, %{assigns: %{meta: %{access: :remote}}}) do + data = + event.index_cache + |> HardwareIndex.render_index() + |> Map.replace(:motherboard, nil) + + {:ok, data} + end + + def whom_to_notify(event), + do: %{server: event.server.server_id} + end + end + + event UpdateFailed do + @moduledoc """ + `MotherboardUpdateFailedEvent` is fired when the user attempted to update + her motherboard but it failed with `reason`. Client is notified (mostly + because this is an asynchronous step). + """ + + alias Helix.Server.Model.Server + + event_struct [:server_id, :reason] + + @type t :: %__MODULE__{ + server_id: Server.id, + reason: reason + } + + @type reason :: :internal + + @spec new(Server.idt, reason) :: + t + def new(server = %Server{}, reason), + do: new(server.server_id, reason) + def new(server_id = %Server.ID{}, reason) do + %__MODULE__{ + server_id: server_id, + reason: reason + } + end + + notify do + + @event :motherboard_update_failed + + @doc """ + Only the player is notified (server channel with `local` access) + """ + def generate_payload(event, %{assigns: %{meta: %{access: :local}}}) do + data = %{reason: event.reason} + + {:ok, data} + end + def generate_payload(_, _), + do: :noreply + + def whom_to_notify(event), + do: %{server: event.server_id} + end + end +end diff --git a/lib/server/henforcer/component.ex b/lib/server/henforcer/component.ex new file mode 100644 index 00000000..e55029ee --- /dev/null +++ b/lib/server/henforcer/component.ex @@ -0,0 +1,303 @@ +defmodule Helix.Server.Henforcer.Component do + + import Helix.Henforcer + + alias Helix.Entity.Model.Entity + alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Network.Model.Network + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Action.Motherboard, as: MotherboardAction + alias Helix.Server.Henforcer.Server, as: ServerHenforcer + alias Helix.Server.Model.Component + alias Helix.Server.Model.Motherboard + alias Helix.Server.Model.Server + alias Helix.Server.Query.Component, as: ComponentQuery + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + + @internet_id NetworkQuery.internet().network_id + + @type component_exists_relay :: %{component: Component.t} + @type component_exists_relay_partial :: %{} + @type component_exists_error :: + {false, {:component, :not_found}, component_exists_relay_partial} + + @spec component_exists?(Component.id) :: + {true, component_exists_relay} + | component_exists_error + @doc """ + Henforces the requested component exists on the database. + """ + def component_exists?(component_id = %Component.ID{}) do + with component = %Component{} <- ComponentQuery.fetch(component_id) do + reply_ok(%{component: component}) + else + _ -> + reply_error({:component, :not_found}) + end + end + + @type is_motherboard_relay :: %{component: Component.t} + @type is_motherboard_relay_partial :: %{} + @type is_motherboard_error :: + {false, {:component, :not_motherboard}, is_motherboard_relay_partial} + | {false, {:motherboard, :not_attached}, is_motherboard_relay_partial} + | component_exists_error + + @spec is_motherboard?(Component.idt | nil) :: + {true, is_motherboard_relay} + | is_motherboard_error + @doc """ + Checks whether the given component is a motherboard. `nil` is a valid input + when checking against the motherboard_id of a server. + + It does not return `Motherboard.t` as a relay! + """ + def is_motherboard?(nil), + do: reply_error({:motherboard, :not_attached}) + def is_motherboard?(component = %Component{type: :mobo}), + do: reply_ok(%{component: component}) + def is_motherboard?(%Component{}), + do: reply_error({:component, :not_motherboard}) + def is_motherboard?(component_id = %Component.ID{}) do + henforce(component_exists?(component_id)) do + is_motherboard?(relay.component) + end + end + + @type can_link_relay :: %{} + @type can_link_error :: + {false, {:motherboard, :wrong_slot_type | :slot_in_use | :bad_slot}, %{}} + + @spec can_link?(Component.mobo, Component.t, Motherboard.slot_id) :: + {true, can_link_relay} + | can_link_error + @doc """ + Checks whether the given `component` can be linked to the `motherboard` at the + given `slot_id`. + """ + def can_link?( + mobo = %Component{type: :mobo}, + component = %Component{}, + slot_id) + do + with :ok <- Motherboard.check_compatibility(mobo, component, slot_id, []) do + reply_ok() + else + {:error, reason} -> + reply_error({:motherboard, reason}) + end + end + + @type has_initial_components_relay :: %{} + @type has_initial_components_error :: + {false, {:motherboard, :missing_initial_components}, %{}} + + @spec has_initial_components?([MotherboardAction.update_component]) :: + {true, has_initial_components_relay} + | has_initial_components_error + @doc """ + Checks whether the given list of components, supposed to be linked to the + motherboard, matches the minimum required components (`initial_components`). + """ + def has_initial_components?(components) do + if Motherboard.has_required_initial_components?(components) do + reply_ok() + else + reply_error({:motherboard, :missing_initial_components}) + end + end + + @type has_public_nip_relay :: %{} + @type has_public_nip_error :: + {false, {:motherboard, :missing_public_nip}, %{}} + + @spec has_public_nip?([Network.Connection.t]) :: + {true, has_public_nip_relay} + | has_public_nip_error + @doc """ + Checks whether, among the specified NetworkConnection changes, at least one + public NIP/NC is assigned to the motherboard. + """ + def has_public_nip?(network_connections) do + if Enum.find(network_connections, &(&1.network_id == @internet_id)) do + reply_ok() + else + reply_error({:motherboard, :missing_public_nip}) + end + end + + @type can_update_mobo_relay :: + %{ + entity: Entity.t, + mobo: Component.mobo, + components: [MotherboardAction.update_component], + owned_components: [Component.t], + network_connections: [MotherboardAction.update_nc], + entity_network_connections: [Network.Connection.t] + } + + @type can_update_mobo_error :: + component_exists_error + | is_motherboard_error + | can_link_error + | has_initial_components_error + | has_public_nip_error + | EntityHenforcer.owns_component_error + | EntityHenforcer.owns_nip_error + + @spec can_update_mobo?( + Entity.id, + Component.id, + [MotherboardAction.update_component], + [MotherboardAction.update_nc]) + :: + {true, can_update_mobo_relay} + | can_update_mobo_error + @doc """ + Checks whether `entity` can update the `mobo_id` with the given components and + network connections. It checks several underlying conditions, like if all + components exist on the DB, as well as some game mechanics stuff like whether + the mobo will have at least one public NIP assigned to it. + """ + def can_update_mobo?(entity_id, mobo_id, components, network_connections) do + reduce_components = fn mobo -> + init = {{true, %{}}, nil} + + components + |> Enum.reduce_while(init, fn {slot_id, comp_id}, {{true, acc}, cache} -> + with \ + {true, r1} <- component_exists?(comp_id), + component = r1.component, + + {true, r2} <- + EntityHenforcer.owns_component?(entity_id, component, cache), + + {true, _} <- can_link?(mobo, component, slot_id) + do + acc_components = Map.get(acc, :components, []) + + new_acc = + acc + |> put_in([:components], acc_components ++ [{component, slot_id}]) + |> put_in([:entity], r2.entity) + |> put_in([:owned_components], r2.owned_components) + + {:cont, {{true, new_acc}, r2.owned_components}} + else + error -> + {:halt, {error, cache}} + end + end) + |> elem(0) + end + + reduce_network_connections = fn entity -> + init = {{true, %{}}, nil} + + network_connections + |> Enum.reduce_while(init, fn {nic_id, nip}, {{true, acc}, cache} -> + {network_id, ip} = nip + + with \ + {true, r1} <- EntityHenforcer.owns_nip?(entity, network_id, ip, cache) + do + acc_nc = Map.get(acc, :network_connections, []) + + new_entry = + %{ + nic_id: nic_id, + network_id: network_id, + ip: ip, + network_connection: r1.network_connection + } + + new_acc = + acc + |> put_in([:network_connections], acc_nc ++ [new_entry]) + |> put_in( + [:entity_network_connections], r1.entity_network_connections + ) + + {:cont, {{true, new_acc}, r1.entity_network_connections}} + else + error -> + {:halt, {error, cache}} + end + end) + |> elem(0) + end + + with \ + {true, r0} <- component_exists?(mobo_id), + {r0, mobo} = get_and_replace(r0, :component, :mobo), + + # Make sure user is plugging components into a motherboard + {true, _} <- is_motherboard?(mobo), + + # Iterate over components/slots and make the required henforcements + {true, r1} <- reduce_components.(mobo), + components = r1.components, + entity = r1.entity, + owned = r1.owned_components, + + # Ensure mobo belongs to entity + {true, _} <- EntityHenforcer.owns_component?(entity, mobo, owned), + + # Ensure all required initial components are there + {true, _} <- has_initial_components?(components), + + # Iterate over NetworkConnections and make the required henforcements + {true, r2} <- reduce_network_connections.(entity), + + # The mobo must have at least one public NC assigned to it + {true, _} <- has_public_nip?(r2.network_connections) + do + reply_ok(relay([r0, r1, r2])) + else + error -> + error + end + end + + @type can_detach_mobo_relay :: + %{ + server: Server.t, + entity: Entity.t, + mobo: Component.mobo, + motherboard: Motherboard.t, + owned_components: [Component.t] + } + + @type can_detach_mobo_error :: + component_exists_error + | is_motherboard_error + | ServerHenforcer.server_exists_error + | EntityHenforcer.owns_component_error + + @spec can_detach_mobo?(Entity.id, Server.id) :: + {true, can_detach_mobo_relay} + | can_detach_mobo_error + @doc """ + Ensures `entity_id` can detach mobo from `server_id`. + """ + # TODO: Mainframe verification, cost analysis (for cooldown) etc. #358 + def can_detach_mobo?(entity_id, server_id) do + with \ + {true, r0} <- ServerHenforcer.server_exists?(server_id), + server = r0.server, + + # Server must have a motherboard attached to it... + {true, _} <- is_motherboard?(server.motherboard_id), + + # Entity must be the owner of that motherboard + {true, r1} <- + EntityHenforcer.owns_component?(entity_id, server.motherboard_id, nil), + r1 = replace(r1, :component, :mobo) + do + motherboard = MotherboardQuery.fetch(server.motherboard_id) + + reply_ok(relay([r0, r1])) + |> wrap_relay(%{motherboard: motherboard}) + end + end +end diff --git a/lib/server/henforcer/server.ex b/lib/server/henforcer/server.ex index f2b80d4e..f9e2e7a6 100644 --- a/lib/server/henforcer/server.ex +++ b/lib/server/henforcer/server.ex @@ -13,14 +13,12 @@ defmodule Helix.Server.Henforcer.Server do @type server_exists_error :: {false, {:server, :not_found}, server_exists_relay_partial} - @spec server_exists?(Server.idt) :: + @spec server_exists?(Server.id) :: {true, server_exists_relay} | server_exists_error @doc """ Ensures the requested server exists on the database. """ - def server_exists?(server = %Server{}), - do: server_exists?(server.server_id) def server_exists?(server_id = %Server.ID{}) do with server = %Server{} <- ServerQuery.fetch(server_id) do reply_ok(%{server: server}) diff --git a/lib/server/internal/motherboard.ex b/lib/server/internal/motherboard.ex index 59dcfffa..7fc2bc31 100644 --- a/lib/server/internal/motherboard.ex +++ b/lib/server/internal/motherboard.ex @@ -13,7 +13,7 @@ defmodule Helix.Server.Internal.Motherboard do """ def fetch(motherboard_id) do motherboard_id - |> Motherboard.Query.by_motherboard() + |> Motherboard.Query.by_motherboard(eager: true) |> Repo.all() |> Motherboard.format() end @@ -217,7 +217,7 @@ defmodule Helix.Server.Internal.Motherboard do @spec unlink(Component.pluggable) :: :ok @doc """ - Unlinks `component` from `motherboard`. + Unlinks `component` from the motherboard. Notice we are not *updating* any entries. All `unlink` operations are removing data from the `motherboards` table. @@ -230,6 +230,19 @@ defmodule Helix.Server.Internal.Motherboard do :ok end + @spec unlink_all(Motherboard.t) :: + :ok + @doc """ + Unlinks all components from `motherboard`. + """ + def unlink_all(motherboard = %Motherboard{}) do + motherboard.motherboard_id + |> Motherboard.Query.by_motherboard(eager: false) + |> Repo.delete_all() + + :ok + end + @spec get_component(Motherboard.t, Component.type) :: [Component.pluggable] defp get_component(motherboard = %Motherboard{}, component_type) do diff --git a/lib/server/internal/server.ex b/lib/server/internal/server.ex index c745d3f8..b1e5a40c 100644 --- a/lib/server/internal/server.ex +++ b/lib/server/internal/server.ex @@ -40,15 +40,21 @@ defmodule Helix.Server.Internal.Server do def set_hostname(server, hostname) do server |> Server.set_hostname(hostname) - |> update() + |> Repo.update() end @spec attach(Server.t, Motherboard.id) :: repo_return + @doc """ + Updates the `server` motherboard to be `mobo_id`. + + It doesn't matter if the server already has a motherboard attached to it; this + operation will overwrite any existing motherboard. + """ def attach(server, mobo_id) do result = server - |> Server.update_changeset(%{motherboard_id: mobo_id}) + |> Server.attach_motherboard(mobo_id) |> Repo.update() with {:ok, _} <- result do @@ -59,15 +65,23 @@ defmodule Helix.Server.Internal.Server do end @spec detach(Server.t) :: - :ok + repo_return + @doc """ + Detaches the currently attached motherboard from `server` + + It doesn't matter if the server has no motherboard attached to it. + """ def detach(server = %Server{}) do - server - |> Server.detach_motherboard() - |> Repo.update!() + result = + server + |> Server.detach_motherboard() + |> Repo.update() - CacheAction.update_server(server) + with {:ok, _} <- result do + CacheAction.update_server(server) + end - :ok + result end @spec delete(Server.t) :: @@ -79,9 +93,4 @@ defmodule Helix.Server.Internal.Server do :ok end - - @spec update(Server.changeset) :: - repo_return - defp update(changeset), - do: Repo.update(changeset) end diff --git a/lib/server/model/motherboard.ex b/lib/server/model/motherboard.ex index 536f9de7..b80e285d 100644 --- a/lib/server/model/motherboard.ex +++ b/lib/server/model/motherboard.ex @@ -42,11 +42,12 @@ defmodule Helix.Server.Model.Motherboard do net: Component.NIC.custom } - @type initial_components :: [{Component.pluggable, Component.Mobo.slot_id}] + @type initial_components :: [{Component.pluggable, slot_id}] @type required_components :: [Constant.t] - @type slot :: {Component.Mobo.slot_id, Component.t} - @type free_slots :: %{Component.type => [Component.Mobo.slot_id]} + @type slot_id :: Component.Mobo.slot_id + @type slot :: {slot_id, Component.t} + @type free_slots :: %{Component.type => [slot_id]} @type error :: :wrong_slot_type @@ -314,7 +315,7 @@ defmodule Helix.Server.Model.Motherboard do | {:error, :wrong_slot_type} | {:error, :slot_in_use} | {:error, :bad_slot} - defp check_compatibility( + def check_compatibility( mobo = %Component{}, component = %Component{}, slot_id, @@ -329,12 +330,14 @@ defmodule Helix.Server.Model.Motherboard do alias Helix.Server.Model.Component - @spec by_motherboard(Queryable.t, Motherboard.idt) :: + @spec by_motherboard(Queryable.t, Motherboard.idt, [eager: boolean]) :: Queryable.t - def by_motherboard(query \\ Motherboard, motherboard_id) - def by_motherboard(query, mobo = %Component{type: :mobo}), - do: by_motherboard(query, mobo.component_id) - def by_motherboard(query, motherboard_id) do + def by_motherboard(query \\ Motherboard, motherboard_id, eager?) + def by_motherboard(query, mobo = %Component{type: :mobo}, eager?), + do: by_motherboard(query, mobo.component_id, eager?) + def by_motherboard(query, motherboard_id, eager: false), + do: where(query, [m], m.motherboard_id == ^motherboard_id) + def by_motherboard(query, motherboard_id, _) do from entries in query, inner_join: component in assoc(entries, :linked_component), where: entries.motherboard_id == ^to_string(motherboard_id), diff --git a/lib/server/model/server.ex b/lib/server/model/server.ex index f10a15da..7b29decd 100644 --- a/lib/server/model/server.ex +++ b/lib/server/model/server.ex @@ -4,6 +4,7 @@ defmodule Helix.Server.Model.Server do use HELL.ID, field: :server_id, meta: [0x0010] import Ecto.Changeset + import HELL.Ecto.Macros alias Ecto.Changeset alias HELL.Constant @@ -54,7 +55,7 @@ defmodule Helix.Server.Model.Server do end @spec create_changeset(creation_params) :: - Changeset.t + changeset def create_changeset(params) do %__MODULE__{} |> cast(params, @creation_fields) @@ -64,16 +65,6 @@ defmodule Helix.Server.Model.Server do |> validate_inclusion(:type, Server.Type.possible_types()) end - @spec update_changeset(t | Changeset.t, update_params) :: - Changeset.t - def update_changeset(struct, params) do - struct - |> cast(params, []) - |> unique_constraint(:motherboard_id) - |> attach_motherboard(params) - |> validate_required(@required_fields) - end - @spec set_hostname(t, hostname) :: changeset def set_hostname(server, hostname) do @@ -82,38 +73,40 @@ defmodule Helix.Server.Model.Server do |> put_change(:hostname, hostname) end - @spec detach_motherboard(t | Changeset.t) :: - Changeset.t - def detach_motherboard(struct), - do: update_changeset(struct, %{motherboard_id: nil}) - - @spec attach_motherboard(t | Changeset.t, map) :: - Changeset.t - defp attach_motherboard(changeset, params) do - previous = get_field(changeset, :motherboard_id) - changeset = cast(changeset, params, [:motherboard_id]) - next = get_change(changeset, :motherboard_id) - - # Already has motherboard and is trying to override it - if previous && next do - add_error(changeset, :motherboard_id, "is already set") - else - changeset - end + @spec update_motherboard(t, Motherboard.id | nil) :: + changeset + defp update_motherboard(server, mobo_id) do + server + |> change() + |> unique_constraint(:motherboard_id) + |> put_change(:motherboard_id, mobo_id) + |> validate_required(@required_fields) end - @spec generate_password(Changeset.t) :: - Changeset.t + @spec attach_motherboard(t, Motherboard.id) :: + changeset + @doc """ + Assigns `new_mobo_id` to the Server model + """ + def attach_motherboard(server, new_mobo_id), + do: update_motherboard(server, new_mobo_id) + + @spec detach_motherboard(t) :: + changeset + @doc """ + Removes the `motherboard_id` field from the Server model. + """ + def detach_motherboard(server), + do: update_motherboard(server, nil) + + @spec generate_password(changeset) :: + changeset defp generate_password(changeset), do: put_change(changeset, :password, Password.generate(:server)) - defmodule Query do - - import Ecto.Query + query do - alias Ecto.Queryable alias Helix.Server.Model.Motherboard - alias Helix.Server.Model.Server @spec by_id(Queryable.t, Server.idtb) :: Queryable.t diff --git a/lib/server/public/index.ex b/lib/server/public/index.ex index 55cbb2ea..a22ee0fd 100644 --- a/lib/server/public/index.ex +++ b/lib/server/public/index.ex @@ -14,6 +14,7 @@ defmodule Helix.Server.Public.Index do alias Helix.Software.Model.Storage alias Helix.Software.Public.Index, as: FileIndex alias Helix.Server.Model.Server + alias Helix.Server.Public.Index.Hardware, as: HardwareIndex alias Helix.Server.Query.Server, as: ServerQuery @type index :: @@ -72,6 +73,7 @@ defmodule Helix.Server.Public.Index do logs: LogIndex.index, main_storage: Storage.id, storages: FileIndex.index, + hardware: HardwareIndex.index, processes: ProcessIndex.index, tunnels: NetworkIndex.index, } @@ -84,6 +86,7 @@ defmodule Helix.Server.Public.Index do logs: LogIndex.rendered_index, main_storage: String.t, storages: FileIndex.rendered_index, + hardware: HardwareIndex.rendered_index, processes: ProcessIndex.index, tunnels: NetworkIndex.rendered_index, } @@ -93,7 +96,8 @@ defmodule Helix.Server.Public.Index do nips: [Network.nip], logs: LogIndex.index, main_storage: Storage.id, - storages: FileIndex.rendered_index, + storages: FileIndex.index, + hardware: HardwareIndex.index, processes: ProcessIndex.index, tunnels: NetworkIndex.index } @@ -104,6 +108,7 @@ defmodule Helix.Server.Public.Index do logs: LogIndex.rendered_index, main_storage: String.t, storages: FileIndex.rendered_index, + hardware: HardwareIndex.rendered_index, processes: ProcessIndex.index, tunnels: NetworkIndex.rendered_index } @@ -225,7 +230,8 @@ defmodule Helix.Server.Public.Index do def gateway(server = %Server{}, entity_id) do index = %{ password: server.password, - name: server.hostname + name: server.hostname, + hardware: HardwareIndex.index(server, :local) } Map.merge(server_common(server, entity_id), index) @@ -237,10 +243,11 @@ defmodule Helix.Server.Public.Index do Renderer for `gateway/2` """ def render_gateway(server) do - partial = %{ - password: server.password, - name: server.name - } + partial = + %{ + password: server.password, + name: server.name + } Map.merge(partial, render_server_common(server)) end @@ -256,7 +263,12 @@ defmodule Helix.Server.Public.Index do - Resync client data with `bootstrap` request """ def remote(server = %Server{}, entity_id) do - server_common(server, entity_id) + index = + %{ + hardware: HardwareIndex.index(server, :remote) + } + + Map.merge(server_common(server, entity_id), index) end @spec render_remote(remote) :: @@ -311,6 +323,7 @@ defmodule Helix.Server.Public.Index do logs: LogIndex.render_index(server.logs), main_storage: server.main_storage |> to_string(), storages: FileIndex.render_index(server.storages), + hardware: HardwareIndex.render_index(server.hardware), processes: server.processes, tunnels: NetworkIndex.render_index(server.tunnels) } diff --git a/lib/server/public/index/hardware.ex b/lib/server/public/index/hardware.ex new file mode 100644 index 00000000..b80b16df --- /dev/null +++ b/lib/server/public/index/hardware.ex @@ -0,0 +1,45 @@ +defmodule Helix.Server.Public.Index.Hardware do + + alias Helix.Server.Model.Server + alias Helix.Server.Public.Index.Motherboard, as: MotherboardIndex + + @type index :: + %{ + motherboard: MotherboardIndex.index | nil + } + + @type rendered_index :: + %{ + motherboard: MotherboardIndex.rendered_index | nil + } + + @typep access :: :local | :remote + + @spec index(Server.t, access) :: + index + def index(server = %Server{}, :local) do + %{ + motherboard: MotherboardIndex.index(server) + } + end + + def index(%Server{}, :remote) do + %{ + motherboard: nil + } + end + + @spec render_index(index) :: + rendered_index + def render_index(%{motherboard: nil}) do + %{ + motherboard: nil + } + end + + def render_index(index) do + %{ + motherboard: MotherboardIndex.render_index(index.motherboard) + } + end +end diff --git a/lib/server/public/index/motherboard.ex b/lib/server/public/index/motherboard.ex new file mode 100644 index 00000000..1fe8c263 --- /dev/null +++ b/lib/server/public/index/motherboard.ex @@ -0,0 +1,136 @@ +defmodule Helix.Server.Public.Index.Motherboard do + + alias Helix.Network.Model.Network + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Model.Motherboard + alias Helix.Server.Model.Server + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + + @type index :: + %{ + motherboard_id: Motherboard.id | nil, + network_connections: [Network.Connection.t] + } + + @type rendered_index :: + %{ + motherboard_id: String.t, + slots: rendered_slots, + network_connections: rendered_network_connections + } + + @typep rendered_slots :: + %{ + slot_id :: String.t => %{ + type: String.t, + component_id: String.t + } + } + + @typep rendered_network_connections :: + %{ + nic_id :: String.t => %{ + network_id: String.t, + ip: String.t + } + } + + @spec index(Server.t) :: + index + def index(%Server{motherboard_id: nil}) do + %{ + motherboard_id: nil, + network_connections: [] + } + end + + def index(server = %Server{}) do + motherboard = MotherboardQuery.fetch(server.motherboard_id) + + network_connections = + motherboard + |> MotherboardQuery.get_nics() + |> Enum.reduce([], fn nic, acc -> + + if nic do + nc = NetworkQuery.Connection.fetch_by_nic(nic) + + acc ++ [nc] + else + acc + end + end) + + %{ + motherboard: motherboard, + network_connections: network_connections, + } + end + + @spec render_index(Server.t) :: + rendered_index + def render_index(%{motherboard_id: nil}) do + %{ + motherboard_id: nil, + slots: %{}, + network_connections: %{} + } + end + + def render_index(index) do + %{ + motherboard_id: to_string(index.motherboard.motherboard_id), + slots: render_slots(index.motherboard), + network_connections: render_network_connections(index.network_connections) + } + end + + @spec render_slots(Motherboard.t) :: + rendered_slots + defp render_slots(motherboard = %Motherboard{}) do + used_slots = + motherboard.slots + |> Enum.map(fn {slot_id, component} -> + comp_data = + %{ + type: to_string(component.type), + component_id: to_string(component.component_id) + } + + {to_string(slot_id), comp_data} + end) + |> Enum.into(%{}) + + free_slots = + motherboard + |> MotherboardQuery.get_free_slots() + |> Enum.reduce(%{}, fn {comp_type, free_slots}, acc -> + free_slots + |> Enum.map(fn slot_id -> + + {to_string(slot_id), %{type: to_string(comp_type), component_id: nil}} + end) + |> Enum.into(%{}) + |> Map.merge(acc) + end) + + Map.merge(used_slots, free_slots) + end + + @spec render_network_connections([Network.Connection.t]) :: + rendered_network_connections + defp render_network_connections(network_connections) do + network_connections + |> Enum.reduce(%{}, fn nc, acc -> + client_nip = + %{ + network_id: to_string(nc.network_id), + ip: nc.ip + } + + %{} + |> Map.put(to_string(nc.nic_id), client_nip) + |> Map.merge(acc) + end) + end +end diff --git a/lib/server/public/server.ex b/lib/server/public/server.ex index 4171aed4..f023acee 100644 --- a/lib/server/public/server.ex +++ b/lib/server/public/server.ex @@ -6,9 +6,12 @@ defmodule Helix.Server.Public.Server do alias Helix.Network.Model.Tunnel alias Helix.Network.Query.Network, as: NetworkQuery alias Helix.Network.Query.Tunnel, as: TunnelQuery + alias Helix.Server.Model.Component alias Helix.Server.Model.Server alias Helix.Server.Action.Flow.Server, as: ServerFlow + alias Helix.Server.Action.Motherboard, as: MotherboardAction alias Helix.Server.Public.Index, as: ServerIndex + alias Helix.Server.Query.Motherboard, as: MotherboardQuery @spec connect_to_server(Server.id, Server.id, [Server.id]) :: {:ok, Tunnel.t} @@ -39,6 +42,56 @@ defmodule Helix.Server.Public.Server do end end + @spec update_mobo( + Server.t, + { + Component.mobo, + [MotherboardAction.update_component], + [MotherboardAction.update_nc] + }, + [Network.Connection.t], + Event.relay) + :: + ServerFlow.update_mobo_result + @doc """ + Updates the server motherboard. + + - `mobo` points to the (potentially) new motherboard component. + - `components` is a list of the (potentially) new components linked to the + motherboard. + - `ncs` is a list of the (potentially) new NCs assigned to its NICs. + + Notice `components` and `ncs` are no ordinary lists. The former also includes + the `slot_id` that component is supposed to be linked to, and the latter + includes the `nic_id` that should be assigned the network connection (NC). + """ + def update_mobo(server, {mobo, components, ncs}, entity_ncs, relay) do + motherboard = + if server.motherboard_id do + MotherboardQuery.fetch(server.motherboard_id) + else + nil + end + + mobo_data = + %{ + mobo: mobo, + components: components, + network_connections: ncs + } + + ServerFlow.update_mobo(server, motherboard, mobo_data, entity_ncs, relay) + end + + @doc """ + Detaches the server motherboard. + """ + defdelegate detach_mobo(server, motherboard, relay), + to: ServerFlow + + @doc """ + Sets the server hostname. + """ defdelegate set_hostname(server, hostname, relay), to: ServerFlow diff --git a/lib/server/query/motherboard.ex b/lib/server/query/motherboard.ex index 78bbd36e..e716bd89 100644 --- a/lib/server/query/motherboard.ex +++ b/lib/server/query/motherboard.ex @@ -30,4 +30,11 @@ defmodule Helix.Server.Query.Motherboard do to: MotherboardInternal defdelegate get_nics(motherboard), to: MotherboardInternal + defdelegate get_rams(motherboard), + to: MotherboardInternal + + defdelegate get_free_slots(motherboard), + to: MotherboardInternal + defdelegate get_free_slots(mobo, motherboard), + to: MotherboardInternal end diff --git a/lib/server/websocket/channel/server.ex b/lib/server/websocket/channel/server.ex index a32b9853..243c0939 100644 --- a/lib/server/websocket/channel/server.ex +++ b/lib/server/websocket/channel/server.ex @@ -34,6 +34,8 @@ channel Helix.Server.Websocket.Channel.Server do alias Helix.Server.Websocket.Requests.Bootstrap, as: BootstrapRequest alias Helix.Server.Websocket.Requests.Config.Check, as: ConfigCheckRequest alias Helix.Server.Websocket.Requests.Config.Set, as: ConfigSetRequest + alias Helix.Server.Websocket.Requests.MotherboardUpdate, + as: MotherboardUpdateRequest alias Helix.Server.Websocket.Requests.SetHostname, as: SetHostnameRequest @doc """ @@ -124,6 +126,57 @@ channel Helix.Server.Websocket.Channel.Server do """ topic "config.check", ConfigCheckRequest + @doc """ + Updates the player's motherboard. May be used to attach, detach or update the + mobo components. + + Params (detach): + - *cmd: "detach" + + Params (update): + - *motherboard_id: ID of the motherboard selected by the player. + - *slots: Map with the mobo `slot_id` as key and the component selected for + such slot. Empty slots may be ignored or set as `nil`. + - *network_connections: Map with the `nic_id` as key and the nip selected for + such nic. Non-assigned NICs may be ignored. + + Example: + %{ + "motherboard_id" => "::1", + "slots" => %{ + "cpu_1" => "::f", + "ram_1" => nil, + }, + "network_connections" => %{ + "::5" => %{ + "network_id" => "::", + "ip" => "1.2.3.4" + } + } + } + + All components (including the mobo) and the NIPs must belong to the player. + + Errors: + + Henforcer: + - component_not_found: One of the specified components were not found + - motherboard_wrong_slot_type: Buraco errado + - motherboard_bad_slot: Specified invalid slot ID + - component_not_belongs: One of the components do not belong to the player + - motherboard_missing_initial_components: So large it's self explanatory + - network_connection_not_belongs: One of the NCs do not belong to the player + - motherboard_missing_public_nip: Mobos must have at least one public NIP + - component_not_motherboard: Wrong tool for the job + + Input validation: + - bad_slot_data: slot data (input) is invalid + - bad_network_connections: network connections data (input) is invalid + - bad_src: this request may only be run on `local` channels + + base errors + """ + topic "motherboard.update", MotherboardUpdateRequest + @doc """ Updates the server hostname. @@ -303,7 +356,7 @@ channel Helix.Server.Websocket.Channel.Server do ServerWebsocketChannelState. """ def terminate(_reason, socket) do - if socket.assigns.meta.access_type == :remote do + if socket.assigns.meta.access == :remote do entity_id = socket.assigns.gateway.entity_id server_id = socket.assigns.destination.server_id counter = socket.assigns.meta.counter diff --git a/lib/server/websocket/channel/server/join.ex b/lib/server/websocket/channel/server/join.ex index 294bedd9..ee5aaa0b 100644 --- a/lib/server/websocket/channel/server/join.ex +++ b/lib/server/websocket/channel/server/join.ex @@ -40,14 +40,14 @@ join Helix.Server.Websocket.Channel.Server.Join do method. """ def check_params(request = %ServerJoin{type: nil}, socket) do - access_type = + access = if request.unsafe["gateway_ip"] do :remote else :local end - %{request| type: access_type} + %{request| type: access} |> check_params(socket) end @@ -186,13 +186,13 @@ join Helix.Server.Websocket.Channel.Server.Join do defp build_meta(%ServerJoin{type: :local}) do %{ - access_type: :local + access: :local } end defp build_meta(request = %ServerJoin{type: :remote}) do %{ - access_type: :remote, + access: :remote, counter: request.meta.counter, network_id: request.params.network_id } @@ -216,7 +216,7 @@ join Helix.Server.Websocket.Channel.Server.Join do socket = socket - |> assign.(:access_type, :local) + |> assign.(:access, :local) |> assign.(:gateway, gateway_data) |> assign.(:destination, gateway_data) |> assign.(:meta, build_meta(request)) diff --git a/lib/server/websocket/requests/bootstrap.ex b/lib/server/websocket/requests/bootstrap.ex index ff3f65b5..7cf61c71 100644 --- a/lib/server/websocket/requests/bootstrap.ex +++ b/lib/server/websocket/requests/bootstrap.ex @@ -25,7 +25,7 @@ request Helix.Server.Websocket.Requests.Bootstrap do server = ServerQuery.fetch(server_id) bootstrap = - if socket.assigns.meta.access_type == :local do + if socket.assigns.meta.access == :local do ServerPublic.bootstrap_gateway(server, entity_id) else ServerPublic.bootstrap_remote(server, entity_id) @@ -36,7 +36,7 @@ request Helix.Server.Websocket.Requests.Bootstrap do render(request, socket) do data = - if socket.assigns.meta.access_type == :local do + if socket.assigns.meta.access == :local do ServerPublic.render_bootstrap_gateway(request.meta.bootstrap) else ServerPublic.render_bootstrap_remote(request.meta.bootstrap) diff --git a/lib/server/websocket/requests/motherboard_update.ex b/lib/server/websocket/requests/motherboard_update.ex new file mode 100644 index 00000000..f0812f21 --- /dev/null +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -0,0 +1,221 @@ +import Helix.Websocket.Request + +request Helix.Server.Websocket.Requests.MotherboardUpdate do + + import HELL.Macros + + alias HELL.IPv4 + alias HELL.Utils + alias Helix.Network.Model.Network + alias Helix.Server.Henforcer.Component, as: ComponentHenforcer + alias Helix.Server.Henforcer.Server, as: ServerHenforcer + alias Helix.Server.Model.Component + alias Helix.Server.Public.Server, as: ServerPublic + + def check_params(request, socket) do + if request.unsafe["cmd"] == "detach" do + check_detach(request, socket) + else + check_update(request, socket) + end + end + + def check_detach(request, socket) do + with \ + true <- socket.assigns.meta.access == :local || :bad_src + do + update_params(request, %{cmd: :detach}, reply: true) + else + :bad_src -> + reply_error(request, "bad_src") + + _ -> + bad_request(request) + end + end + + def check_update(request, socket) do + with \ + true <- socket.assigns.meta.access == :local || :bad_src, + {:ok, mobo_id} <- Component.ID.cast(request.unsafe["motherboard_id"]), + {:ok, slots} <- cast_slots(request.unsafe["slots"]), + {:ok, ncs} <- cast_ncs(request.unsafe["network_connections"]) + do + params = %{ + slots: slots, + network_connections: ncs, + mobo_id: mobo_id, + cmd: :update + } + + update_params(request, params, reply: true) + else + :bad_src -> + reply_error(request, "bad_src") + + :bad_slots -> + reply_error(request, "bad_slot_data") + + :bad_ncs -> + reply_error(request, "bad_network_connections") + + _ -> + bad_request(request) + end + end + + def check_permissions(request = %{params: %{cmd: :detach}}, socket) do + entity_id = socket.assigns.gateway.entity_id + gateway_id = socket.assigns.gateway.server_id + + with \ + {true, r0} <- ComponentHenforcer.can_detach_mobo?(entity_id, gateway_id) + do + meta = %{ + server: r0.server, + motherboard: r0.motherboard + } + + update_meta(request, meta, reply: true) + else + {false, reason, _} -> + reply_error(request, reason) + end + + end + + def check_permissions(request = %{params: %{cmd: :update}}, socket) do + gateway_id = socket.assigns.gateway.server_id + entity_id = socket.assigns.gateway.entity_id + mobo_id = request.params.mobo_id + slots = request.params.slots + ncs = request.params.network_connections + + with \ + {true, r0} <- ServerHenforcer.server_exists?(gateway_id), + {true, r1} <- + ComponentHenforcer.can_update_mobo?(entity_id, mobo_id, slots, ncs) + do + meta = %{ + server: r0.server, + mobo: r1.mobo, + components: r1.components, + owned_components: r1.owned_components, + network_connections: r1.network_connections, + entity_network_connections: r1.entity_network_connections + } + + update_meta(request, meta, reply: true) + else + {false, reason, _} -> + reply_error(request, reason) + end + end + + def handle_request(request = %{params: %{cmd: :detach}}, _socket) do + server = request.meta.server + motherboard = request.meta.motherboard + relay = request.relay + + hespawn fn -> + ServerPublic.detach_mobo(server, motherboard, relay) + end + + reply_ok(request) + end + + def handle_request(request = %{params: %{cmd: :update}}, _socket) do + server = request.meta.server + mobo = request.meta.mobo + components = request.meta.components + ncs = request.meta.network_connections + entity_ncs = request.meta.entity_network_connections + relay = request.relay + + # Updates mobo asynchronously + hespawn fn -> + ServerPublic.update_mobo( + server, {mobo, components, ncs}, entity_ncs, relay + ) + end + + reply_ok(request) + end + + render_empty() + + defp cast_ncs(nil), + do: :bad_ncs + defp cast_ncs(network_connections) do + try do + ncs = + Enum.map(network_connections, fn {nic_id, nc} -> + {:ok, nic_id} = Component.ID.cast(nic_id) + {:ok, ip} = IPv4.cast(nc["ip"]) + {:ok, network_id} = Network.ID.cast(nc["network_id"]) + + {nic_id, {network_id, ip}} + end) + + {:ok, ncs} + rescue + _ -> + :bad_ncs + end + end + + defp cast_slots(nil), + do: :bad_slots + defp cast_slots(slots) do + try do + slots = + slots + |> Enum.map(fn {slot_id, component_id} -> + component_id = + if component_id do + Component.ID.cast!(component_id) + else + nil + end + + {:ok, slot_id} = cast_slot_id(slot_id) + + {slot_id, component_id} + end) + |> Enum.into(%{}) + + {:ok, slots} + rescue + _ -> + :bad_slots + end + end + + defp cast_slot_id("cpu_" <> id), + do: concat_slot(:cpu, id) + defp cast_slot_id("ram_" <> id), + do: concat_slot(:ram, id) + defp cast_slot_id("hdd_" <> id), + do: concat_slot(:hdd, id) + defp cast_slot_id("nic_" <> id), + do: concat_slot(:nic, id) + defp cast_slot_id("usb_" <> id), + do: concat_slot(:usb, id) + defp cast_slot_id(_), + do: :error + + defp concat_slot(component, id) do + case Integer.parse(id) do + {_, ""} -> + slot_id = + component + |> Utils.concat_atom("_") + |> Utils.concat_atom(id) + + {:ok, slot_id} + + _ -> + :error + end + end +end diff --git a/lib/software/websocket/requests/cracker/bruteforce.ex b/lib/software/websocket/requests/cracker/bruteforce.ex index a5f2f88f..04b4d930 100644 --- a/lib/software/websocket/requests/cracker/bruteforce.ex +++ b/lib/software/websocket/requests/cracker/bruteforce.ex @@ -17,7 +17,7 @@ request Helix.Software.Websocket.Requests.Cracker.Bruteforce do Network.ID.cast(request.unsafe["network_id"]), true <- IPv4.valid?(request.unsafe["ip"]), {:ok, bounces} = cast_bounces(request.unsafe["bounces"]), - true <- socket.assigns.meta.access_type == :local || :bad_attack_src + true <- socket.assigns.meta.access == :local || :bad_attack_src do params = %{ bounces: bounces, diff --git a/lib/software/websocket/requests/file/download.ex b/lib/software/websocket/requests/file/download.ex index 81863047..d4da6c0b 100644 --- a/lib/software/websocket/requests/file/download.ex +++ b/lib/software/websocket/requests/file/download.ex @@ -23,7 +23,7 @@ request Helix.Software.Websocket.Requests.File.Download do end with \ - true <- socket.assigns.meta.access_type == :remote || :bad_access, + true <- socket.assigns.meta.access == :remote || :bad_access, {:ok, file_id} <- File.ID.cast(request.unsafe["file_id"]), {:ok, storage_id} <- Storage.ID.cast(unsafe_storage_id) do diff --git a/lib/software/websocket/requests/pftp/file/add.ex b/lib/software/websocket/requests/pftp/file/add.ex index 8025a083..5efc3375 100644 --- a/lib/software/websocket/requests/pftp/file/add.ex +++ b/lib/software/websocket/requests/pftp/file/add.ex @@ -15,7 +15,7 @@ request Helix.Software.Websocket.Requests.PFTP.File.Add do """ def check_params(request, socket) do with \ - true <- socket.assigns.meta.access_type == :local || :not_local, + true <- socket.assigns.meta.access == :local || :not_local, {:ok, file_id} <- File.ID.cast(request.unsafe["file_id"]) do params = %{ diff --git a/lib/software/websocket/requests/pftp/file/download.ex b/lib/software/websocket/requests/pftp/file/download.ex index eff3801b..b1fc04f1 100644 --- a/lib/software/websocket/requests/pftp/file/download.ex +++ b/lib/software/websocket/requests/pftp/file/download.ex @@ -18,7 +18,7 @@ request Helix.Software.Websocket.Requests.PFTP.File.Download do end with \ - true <- socket.assigns.meta.access_type == :local || :not_local, + true <- socket.assigns.meta.access == :local || :not_local, {:ok, file_id} <- File.ID.cast(request.unsafe["file_id"]), {:ok, network_id, ip} <- validate_nip(request.unsafe["network_id"], request.unsafe["ip"]), diff --git a/lib/software/websocket/requests/pftp/file/remove.ex b/lib/software/websocket/requests/pftp/file/remove.ex index 6cf91629..810dc885 100644 --- a/lib/software/websocket/requests/pftp/file/remove.ex +++ b/lib/software/websocket/requests/pftp/file/remove.ex @@ -16,7 +16,7 @@ request Helix.Software.Websocket.Requests.PFTP.File.Remove do """ def check_params(request, socket) do with \ - true <- socket.assigns.meta.access_type == :local || :not_local, + true <- socket.assigns.meta.access == :local || :not_local, {:ok, file_id} <- File.ID.cast(request.unsafe["file_id"]) do params = %{ diff --git a/lib/software/websocket/requests/pftp/server/disable.ex b/lib/software/websocket/requests/pftp/server/disable.ex index db32c408..070b9eb3 100644 --- a/lib/software/websocket/requests/pftp/server/disable.ex +++ b/lib/software/websocket/requests/pftp/server/disable.ex @@ -14,7 +14,7 @@ request Helix.Software.Websocket.Requests.PFTP.Server.Disable do local socket. """ def check_params(request, socket) do - if socket.assigns.meta.access_type == :local do + if socket.assigns.meta.access == :local do reply_ok(request) else reply_error(request, "pftp_must_be_local") diff --git a/lib/software/websocket/requests/pftp/server/enable.ex b/lib/software/websocket/requests/pftp/server/enable.ex index a0630cb2..5e99cde7 100644 --- a/lib/software/websocket/requests/pftp/server/enable.ex +++ b/lib/software/websocket/requests/pftp/server/enable.ex @@ -14,7 +14,7 @@ request Helix.Software.Websocket.Requests.PFTP.Server.Enable do local socket. """ def check_params(request, socket) do - if socket.assigns.meta.access_type == :local do + if socket.assigns.meta.access == :local do reply_ok(request) else reply_error(request, "pftp_must_be_local") diff --git a/priv/repo/cache/migrations/20170713020139_initial_migration.exs b/priv/repo/cache/migrations/20170713020139_initial_migration.exs index ce758801..c6377a4c 100644 --- a/priv/repo/cache/migrations/20170713020139_initial_migration.exs +++ b/priv/repo/cache/migrations/20170713020139_initial_migration.exs @@ -5,16 +5,16 @@ defmodule Helix.Cache.Repo.Migrations.InitialMigration do create table(:server_cache, primary_key: false) do add :server_id, :inet, primary_key: true - add :entity_id, :inet - add :motherboard_id, :inet + add :entity_id, :inet # removed + add :motherboard_id, :inet # removed add :networks, {:array, :json} add :storages, {:array, :inet} - add :resources, :map - add :components, {:array, :inet} + add :resources, :map # removed + add :components, {:array, :inet} # removed add :expiration_date, :utc_datetime end - create index(:server_cache, [:entity_id]) - create index(:server_cache, [:motherboard_id]) + create index(:server_cache, [:entity_id]) # removed + create index(:server_cache, [:motherboard_id]) # removed create index(:server_cache, [:expiration_date]) create table(:storage_cache, primary_key: false) do @@ -32,12 +32,12 @@ defmodule Helix.Cache.Repo.Migrations.InitialMigration do end create index(:network_cache, [:expiration_date]) - create table(:component_cache, primary_key: false) do - add :component_id, :inet, primary_key: true - add :motherboard_id, :inet - add :expiration_date, :utc_datetime - end - create index(:component_cache, [:expiration_date]) + create table(:component_cache, primary_key: false) do # removed + add :component_id, :inet, primary_key: true # removed + add :motherboard_id, :inet # removed + add :expiration_date, :utc_datetime # removed + end # removed + create index(:component_cache, [:expiration_date]) # removed create table(:web_cache, primary_key: false) do add :network_id, :inet, primary_key: true diff --git a/priv/repo/cache/migrations/20171223112419_remove_deprecated_cache_columns.exs b/priv/repo/cache/migrations/20171223112419_remove_deprecated_cache_columns.exs new file mode 100644 index 00000000..82a82b63 --- /dev/null +++ b/priv/repo/cache/migrations/20171223112419_remove_deprecated_cache_columns.exs @@ -0,0 +1,16 @@ +defmodule Helix.Cache.Repo.Migrations.RemoveDeprecatedCacheColumns do + use Ecto.Migration + + def change do + drop index(:server_cache, [:entity_id]) + drop index(:server_cache, [:motherboard_id]) + alter table(:server_cache, primary_key: false) do + remove :entity_id + remove :motherboard_id + remove :resources + remove :components + end + + drop table(:component_cache) + end +end diff --git a/test/entity/henforcer/entity_test.exs b/test/entity/henforcer/entity_test.exs index 14a6fd00..a03c4a84 100644 --- a/test/entity/henforcer/entity_test.exs +++ b/test/entity/henforcer/entity_test.exs @@ -7,9 +7,14 @@ defmodule Helix.Entity.Henforcer.EntityTest do alias Helix.Entity.Model.Entity alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + alias Helix.Test.Server.Helper, as: ServerHelper alias Helix.Test.Server.Setup, as: ServerSetup alias Helix.Test.Entity.Setup, as: EntitySetup + @internet_id NetworkHelper.internet_id() + describe "entity_exists?/1" do test "accepts when entity exists" do {entity, _} = EntitySetup.entity() @@ -50,4 +55,63 @@ defmodule Helix.Entity.Henforcer.EntityTest do assert reason == {:server, :not_belongs} end end + + describe "owns_component?/3" do + test "accepts when entity owns the component" do + {server, %{entity: entity}} = ServerSetup.server() + + assert {true, relay} = + EntityHenforcer.owns_component?( + entity.entity_id, server.motherboard_id, nil + ) + + assert relay.component.component_id == server.motherboard_id + assert relay.entity == entity + assert length(relay.owned_components) >= 4 + + assert_relay relay, [:component, :entity, :owned_components] + end + + test "rejects when entity does not own the component" do + {entity, _} = EntitySetup.entity() + {component, _} = ComponentSetup.component() + + assert {false, reason, relay} = + EntityHenforcer.owns_component?(entity, component, nil) + + assert reason == {:component, :not_belongs} + + assert_relay relay, [:component, :entity, :owned_components] + end + end + + describe "owns_nip?/4" do + test "accepts when entity owns the nip" do + {server, %{entity: entity}} = ServerSetup.server() + + %{ip: ip, network_id: network_id} = ServerHelper.get_nip(server) + + assert {true, relay} = + EntityHenforcer.owns_nip?(entity.entity_id, network_id, ip, nil) + + assert relay.entity == entity + assert relay.network_connection.network_id == network_id + assert relay.network_connection.ip == ip + assert length(relay.entity_network_connections) == 1 + + assert_relay relay, + [:entity, :network_connection, :entity_network_connections] + end + + test "rejects when entity doesn't own the nip" do + {entity, _} = EntitySetup.entity() + + assert {false, reason, _} = + EntityHenforcer.owns_nip?( + entity.entity_id, @internet_id, "1.2.3.4", nil + ) + + assert reason == {:network_connection, :not_belongs} + end + end end diff --git a/test/server/action/flow/server_test.exs b/test/server/action/flow/server_test.exs index b0df3f71..409a3cfb 100644 --- a/test/server/action/flow/server_test.exs +++ b/test/server/action/flow/server_test.exs @@ -2,10 +2,14 @@ defmodule Helix.Server.Action.Flow.ServerTest do use Helix.Test.Case.Integration + alias Helix.Network.Query.Network, as: NetworkQuery alias Helix.Server.Action.Flow.Motherboard, as: MotherboardFlow alias Helix.Server.Action.Flow.Server, as: ServerFlow + alias Helix.Server.Query.Component, as: ComponentQuery + alias Helix.Server.Query.Motherboard, as: MotherboardQuery alias Helix.Test.Entity.Setup, as: EntitySetup + alias Helix.Test.Server.Setup, as: ServerSetup @relay nil @@ -22,4 +26,59 @@ defmodule Helix.Server.Action.Flow.ServerTest do assert server.motherboard_id == mobo.component_id end end + + describe "update_mobo/5" do + test "motherboard is updated (same one)" do + {server, %{entity: entity}} = ServerSetup.server() + + # Fetch current motherboard data + mobo = ComponentQuery.fetch(server.motherboard_id) + motherboard = MotherboardQuery.fetch(server.motherboard_id) + + # Get current motherboard components + [cpu] = MotherboardQuery.get_cpus(motherboard) + [ram] = MotherboardQuery.get_rams(motherboard) + [hdd] = MotherboardQuery.get_hdds(motherboard) + [nic] = MotherboardQuery.get_nics(motherboard) + + # Get current NetworkConnection assigned to `old_nic` + nc = NetworkQuery.Connection.fetch_by_nic(nic) + + # Specify the mobo components (desired state) + new_components = [ + {cpu, :cpu_1}, + {ram, :ram_1}, + {hdd, :hdd_1}, + {nic, :nic_1}, + ] + + new_network_connections = + %{ + nic_id: nic.component_id, + network_id: nc.network_id, + ip: nc.ip, + network_connection: nc + } + + mobo_data = + %{ + mobo: mobo, + components: new_components, + network_connections: [new_network_connections] + } + + entity_ncs = NetworkQuery.Connection.get_by_entity(entity.entity_id) + + assert {:ok, new_server, new_motherboard} = + ServerFlow.update_mobo( + server, motherboard, mobo_data, entity_ncs, @relay + ) + + # This is funny (and perhaps I'm tired). We've just updated the mobo with + # the exact same components and NC as before. So, it must be identical to + # the original values + assert new_server == server + assert new_motherboard == motherboard + end + end end diff --git a/test/server/action/motherboard_test.exs b/test/server/action/motherboard_test.exs new file mode 100644 index 00000000..345d5a59 --- /dev/null +++ b/test/server/action/motherboard_test.exs @@ -0,0 +1,241 @@ +defmodule Helix.Server.Action.Motherboardtest do + + use Helix.Test.Case.Integration + + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Action.Motherboard, as: MotherboardAction + alias Helix.Server.Query.Component, as: ComponentQuery + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + alias Helix.Server.Query.Server, as: ServerQuery + + alias Helix.Test.Network.Setup, as: NetworkSetup + alias Helix.Test.Server.Helper, as: ServerHelper + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + + describe "update/5" do + test "updates the mobo components" do + {server, %{entity: entity}} = ServerSetup.server() + + # Upgrade the test motherboard to one that supports multiple slots + ServerHelper.update_server_mobo(server, :mobo_999) + + # Create `new_cpu`, `new_ram` and `new_nic`. `new_cpu` will replace the + # server's current CPU, while `new_ram` will be added alongside the + # current one. Same applies to `new_nic`, which will be added to + {new_cpu, _} = ComponentSetup.component(type: :cpu) + {new_ram, _} = ComponentSetup.component(type: :ram) + {new_nic, _} = ComponentSetup.component(type: :nic) + + # Also create a new NetworkConnection that will replace the current one + {new_nc, _} = + NetworkSetup.Connection.connection(entity_id: entity.entity_id) + + # Fetch current motherboard data + mobo = ComponentQuery.fetch(server.motherboard_id) + motherboard = MotherboardQuery.fetch(server.motherboard_id) + + # Get current motherboard components + [_old_cpu] = MotherboardQuery.get_cpus(motherboard) + [old_ram] = MotherboardQuery.get_rams(motherboard) + [old_hdd] = MotherboardQuery.get_hdds(motherboard) + [old_nic] = MotherboardQuery.get_nics(motherboard) + + # Get current NetworkConnection assigned to `old_nic` + _old_nc = NetworkQuery.Connection.fetch_by_nic(old_nic) + + # Specify the mobo components (desired state) + new_components = [ + {new_cpu, :cpu_1}, + {old_ram, :ram_1}, + {new_ram, :ram_2}, + {old_hdd, :hdd_1}, + {old_nic, :nic_1}, + {new_nic, :nic_2}, + ] + + # Notice that we are setting the `new_nc` to `old_nic`, and we did not + # pass any information about `new_nic`. Therefore, `new_nic` will have no + # NetworkConnection assigned to it, and `old_nc` will be replaced. + new_network_connections = + %{ + nic_id: old_nic.component_id, + network_id: new_nc.network_id, + ip: new_nc.ip, + network_connection: new_nc + } + + mobo_data = + %{ + mobo: mobo, + components: new_components, + network_connections: [new_network_connections] + } + + entity_ncs = NetworkQuery.Connection.get_by_entity(entity.entity_id) + + # Update + assert {:ok, new_motherboard, _events} = + MotherboardAction.update(motherboard, mobo_data, entity_ncs) + + assert [cpu] = MotherboardQuery.get_cpus(new_motherboard) + assert [ram1, ram2] = MotherboardQuery.get_rams(new_motherboard) + assert [hdd] = MotherboardQuery.get_hdds(new_motherboard) + assert [nic1, nic2] = MotherboardQuery.get_nics(new_motherboard) + + # The CPU got replaced + assert cpu.component_id == new_cpu.component_id + + # A new RAM component was linked to the mobo + assert ram1.component_id == old_ram.component_id + assert ram2.component_id == new_ram.component_id + + # Same goes for NIC + assert nic1.component_id == old_nic.component_id + assert nic2.component_id == new_nic.component_id + + # But HDD is still the same old love + assert hdd.component_id == old_hdd.component_id + + # OK, if we reached here then the Mobo got updated just fine. Now let's + # see if the NetworkConnections were updated too + + # The new NetworkConnection (`new_nc`) was assigned to `old_nic` + current_nc = NetworkQuery.Connection.fetch_by_nic(old_nic) + + assert current_nc.ip == new_nc.ip + assert current_nc.network_id == new_nc.network_id + assert current_nc.nic_id == old_nic.component_id + + # New nic has no NetworkConnection assigned to it + refute NetworkQuery.Connection.fetch_by_nic(new_nic) + end + + test "updates the mobo (on a server that had no mobo)" do + {server, %{entity: entity}} = ServerSetup.server() + + # Note: we are "cheating" - we are directly setting the server mobo to nil + # This will work for this test, but won't invalidate cache, or the current + # mobo NC, for instance. + ServerHelper.update_server_mobo(server, nil) + + # Server has no mobo + server = ServerQuery.fetch(server.server_id) + refute server.motherboard_id + + # Generate brand new components + {mobo, _} = ComponentSetup.component(type: :mobo) + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + # Generate NetworkConnection + {nc, _} = + NetworkSetup.Connection.connection(entity_id: entity.entity_id) + + # Declare mobo data + new_components = [ + {cpu, :cpu_1}, + {ram, :ram_1}, + {hdd, :hdd_1}, + {nic, :nic_1}, + ] + + new_network_connections = + %{ + nic_id: nic.component_id, + network_id: nc.network_id, + ip: nc.ip, + network_connection: nc + } + + mobo_data = + %{ + mobo: mobo, + components: new_components, + network_connections: [new_network_connections] + } + + entity_ncs = NetworkQuery.Connection.get_by_entity(entity.entity_id) + + # Update + assert {:ok, new_motherboard, _events} = + MotherboardAction.update(nil, mobo_data, entity_ncs) + + assert new_motherboard.motherboard_id == mobo.component_id + + # `new_components` were linked to the mobo + assert new_motherboard.slots.cpu_1 == cpu + assert new_motherboard.slots.ram_1 == ram + assert new_motherboard.slots.hdd_1 == hdd + assert new_motherboard.slots.nic_1 == nic + + # `nc` was assigned to `nic` + current_nc = NetworkQuery.Connection.fetch_by_nic(nic) + + assert current_nc.ip == nc.ip + assert current_nc.network_id == nc.network_id + assert current_nc.nic_id == nic.component_id + end + + test "updates the mobo (on a server that had a different mobo)" do + {server, %{entity: entity}} = ServerSetup.server() + + # Create the new mobo that we'll replace + {new_mobo, _} = ComponentSetup.component(type: :mobo) + + # Fetch current motherboard data + motherboard = MotherboardQuery.fetch(server.motherboard_id) + + # Get current motherboard components + [old_cpu] = MotherboardQuery.get_cpus(motherboard) + [old_ram] = MotherboardQuery.get_rams(motherboard) + [old_hdd] = MotherboardQuery.get_hdds(motherboard) + [old_nic] = MotherboardQuery.get_nics(motherboard) + + # Get current NetworkConnection assigned to `old_nic` + nc = NetworkQuery.Connection.fetch_by_nic(old_nic) + + # We'll add to the new mobo the previous mobo's components + new_components = [ + {old_cpu, :cpu_1}, + {old_ram, :ram_1}, + {old_hdd, :hdd_1}, + {old_nic, :nic_1} + ] + + # We are setting to the old nic (on a new mobo) the old NC + new_network_connections = + %{ + nic_id: old_nic.component_id, + network_id: nc.network_id, + ip: nc.ip, + network_connection: nc + } + + mobo_data = + %{ + mobo: new_mobo, + components: new_components, + network_connections: [new_network_connections] + } + + entity_ncs = NetworkQuery.Connection.get_by_entity(entity.entity_id) + + # Update + assert {:ok, new_motherboard, _events} = + MotherboardAction.update(motherboard, mobo_data, entity_ncs) + + # The motherboard was changed to `new_mobo` + assert new_motherboard.motherboard_id == new_mobo.component_id + + # But it has the same slots (components) as the previous one + assert new_motherboard.slots == motherboard.slots + + # And it has the same NC as before (on the same NIC) + current_nc = NetworkQuery.Connection.fetch_by_nic(old_nic) + assert current_nc == nc + end + end +end diff --git a/test/server/action/server_test.exs b/test/server/action/server_test.exs index cc02ab10..53477be8 100644 --- a/test/server/action/server_test.exs +++ b/test/server/action/server_test.exs @@ -38,24 +38,24 @@ defmodule Helix.Server.Action.ServerTest do CacheHelper.sync_test() end - test "fails when given motherboard is already attached" do - {server1, _} = ServerSetup.server() - {server2, _} = ServerSetup.server() + test "succeeds when server already has a motherboard" do + {server, _} = ServerSetup.server() - assert {:error, cs} = ServerAction.attach(server1, server2.motherboard_id) - refute cs.valid? + {mobo, _} = ComponentSetup.component(type: :mobo) + + assert {:ok, new_server} = ServerAction.attach(server, mobo.component_id) + assert new_server.motherboard_id == mobo.component_id CacheHelper.sync_test() end - test "fails when server already has a motherboard" do - {server, _} = ServerSetup.server() - - {mobo, _} = ComponentSetup.component(type: :mobo) - - assert {:error, cs} = ServerAction.attach(server, mobo.component_id) + test "fails when given motherboard is already attached" do + {server1, _} = ServerSetup.server() + {server2, _} = ServerSetup.server() - refute cs.valid? + assert {:error, reason} = + ServerAction.attach(server1, server2.motherboard_id) + assert reason == :internal CacheHelper.sync_test() end diff --git a/test/server/event/motherboard_test.exs b/test/server/event/motherboard_test.exs new file mode 100644 index 00000000..75be31fe --- /dev/null +++ b/test/server/event/motherboard_test.exs @@ -0,0 +1,34 @@ +defmodule Helix.Server.Event.MotherboardTest do + + use Helix.Test.Case.Integration + + alias Helix.Event.Notificable + + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Event.Setup, as: EventSetup + + @socket_local ChannelSetup.mock_server_socket(access: :local) + @socket_remote ChannelSetup.mock_server_socket(access: :remote) + + describe "MotherboardUpdatedEvent.generate_payload/2" do + test "generates full hardware index on gateway (local)" do + event = EventSetup.Server.motherboard_updated() + + assert {:ok, data} = Notificable.generate_payload(event, @socket_local) + + # Returns full data about the motherboard + assert data.motherboard.motherboard_id + assert data.motherboard.slots + assert data.motherboard.network_connections + end + + test "generates partial hardware index on endpoint (remote)" do + event = EventSetup.Server.motherboard_updated() + + assert {:ok, data} = Notificable.generate_payload(event, @socket_remote) + + # Does not return information about the motherboard + refute data.motherboard + end + end +end diff --git a/test/server/henforcer/component_test.exs b/test/server/henforcer/component_test.exs new file mode 100644 index 00000000..fb586004 --- /dev/null +++ b/test/server/henforcer/component_test.exs @@ -0,0 +1,347 @@ +defmodule Helix.Server.Henforcer.ComponentTest do + + use Helix.Test.Case.Integration + + import Helix.Test.Henforcer.Macros + + alias Helix.Entity.Action.Entity, as: EntityAction + alias Helix.Network.Action.Network, as: NetworkAction + alias Helix.Server.Henforcer.Component, as: ComponentHenforcer + alias Helix.Server.Model.Component + alias Helix.Server.Query.Server, as: ServerQuery + + alias HELL.TestHelper.Random + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Network.Setup, as: NetworkSetup + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + alias Helix.Test.Server.Helper, as: ServerHelper + alias Helix.Test.Server.Setup, as: ServerSetup + + @internet_id NetworkHelper.internet_id() + + describe "can_update_mobo?/4" do + test "accepts when everything is OK" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + # Create a new NC + {:ok, new_nc} = + NetworkAction.Connection.create(@internet_id, Random.ipv4(), entity) + + nc = %{nic.component_id => {new_nc.network_id, new_nc.ip}} + + assert {true, relay} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, nc + ) + + # Assigned the components (`Component.t`) to the corresponding slot + Enum.each(relay.components, fn {component, slot_id} -> + cond do + slot_id == :cpu_1 -> + assert component == cpu + + slot_id == :ram_1 -> + assert component == ram + + slot_id == :nic_1 -> + assert component == nic + + slot_id == :hdd_1 -> + assert component == hdd + end + end) + + assert relay.entity == entity + assert relay.mobo.component_id == server.motherboard_id + assert length(relay.owned_components) >= 4 + + # All network_connections passed as a param were added to this relay + assert [mobo_nc] = relay.network_connections + + assert mobo_nc.nic_id == nic.component_id + assert mobo_nc.network_id == new_nc.network_id + assert mobo_nc.ip == new_nc.ip + assert mobo_nc.network_connection == new_nc + + # And `entity_network_connections` has all NCs for that entity + assert length(relay.entity_network_connections) == 2 + + assert_relay relay, + [ + :entity, + :mobo, + :components, + :owned_components, + :network_connections, + :entity_network_connections + ] + end + + test "rejects when component does not exist" do + {server, %{entity: entity}} = ServerSetup.server() + + components = [{:cpu_1, Component.ID.generate()}] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, %{} + ) + + assert reason == {:component, :not_found} + end + + test "rejects when invalid component slots are used" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + # C1 has components on the wrong slots (RAM on CPU and versa-vice) + c1 = [ + {:cpu_1, ram.component_id}, {:ram_1, cpu.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason1, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, c1, %{} + ) + + assert reason1 == {:motherboard, :wrong_slot_type} + + # C2 has the components on the right slots but it uses an invalid one + c2 = [ + {:cpu_999, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason2, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, c2, %{} + ) + + assert reason2 == {:motherboard, :bad_slot} + end + + test "rejects when components does not belong to entity" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, %{} + ) + + assert reason == {:component, :not_belongs} + end + + test "rejects when entity does not own the motherboard" do + {_, %{entity: entity}} = ServerSetup.server() + + {bad_mobo, _} = ComponentSetup.component(type: :mobo) + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, bad_mobo.component_id, components, %{} + ) + + assert reason == {:component, :not_belongs} + end + + test "rejects when update does not match the initial components" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + # Missing `hdd` + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id} + ] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, %{} + ) + + assert reason == {:motherboard, :missing_initial_components} + end + + test "rejects when given motherboard_id is not a motherboard (!!!)" do + {_, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + # Using CPU as my motherboard... because why not + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, cpu.component_id, components, %{} + ) + + assert reason == {:component, :not_motherboard} + end + + test "rejects when invalid NIP was assigned to the mobo" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + # I'm assign `Random.ipv4()` as my NC... but it doesn't belong to me! + nc = %{nic.component_id => {@internet_id, Random.ipv4()}} + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, nc + ) + + assert reason == {:network_connection, :not_belongs} + end + + test "rejects when no public NIC was assigned to the mobo" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + {network, _} = NetworkSetup.network() + + # Create a new NC on a random network + {:ok, new_nc} = + NetworkAction.Connection.create(network, Random.ipv4(), entity) + + # Assign `new_nc` to the Mobo. Valid, but it has no public IP !!11! + nc = %{nic.component_id => {new_nc.network_id, new_nc.ip}} + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, nc + ) + + assert reason == {:motherboard, :missing_public_nip} + end + end + + describe "can_detach_mobo?/2" do + test "accepts when everything is a-ok" do + {server, %{entity: entity}} = ServerSetup.server() + + assert {true, relay} = + ComponentHenforcer.can_detach_mobo?(entity.entity_id, server.server_id) + + assert relay.server == server + assert relay.entity == entity + assert relay.mobo.component_id == server.motherboard_id + assert relay.motherboard.motherboard_id == server.motherboard_id + + assert_relay relay, + [:server, :entity, :mobo, :motherboard, :owned_components] + end + + test "rejects when server has no mobo" do + {server, %{entity: entity}} = ServerSetup.server() + + # Remove mobo + ServerHelper.update_server_mobo(server, nil) + + # Look mah, no mobo! + server = ServerQuery.fetch(server.server_id) + refute server.motherboard_id + + assert {false, reason, _} = + ComponentHenforcer.can_detach_mobo?(entity.entity_id, server.server_id) + assert reason == {:motherboard, :not_attached} + end + end +end diff --git a/test/server/internal/motherboard_test.exs b/test/server/internal/motherboard_test.exs index 988fe83c..1aee882d 100644 --- a/test/server/internal/motherboard_test.exs +++ b/test/server/internal/motherboard_test.exs @@ -20,7 +20,7 @@ defmodule Helix.Server.Internal.MotherboardTest do assert motherboard == gen_motherboard # Ran `Component.format` - assert motherboard.slots.hdd_0.custom.iops == hdd.custom.iops + assert motherboard.slots.hdd_1.custom.iops == hdd.custom.iops end test "returns nil if not found" do @@ -65,10 +65,10 @@ defmodule Helix.Server.Internal.MotherboardTest do {ram, _} = ComponentSetup.component(type: :ram) {nic, _} = ComponentSetup.nic(ulk: 20, dlk: 21, network_id: @internet_id) - assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, cpu, :cpu_1) - assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, hdd, :hdd_1) - assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, nic, :nic_1) - assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, ram, :ram_1) + assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, cpu, :cpu_2) + assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, hdd, :hdd_2) + assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, nic, :nic_2) + assert {:ok, _} = MotherboardInternal.link(motherboard, mobo, ram, :ram_2) motherboard = MotherboardInternal.fetch(mobo.component_id) new_res = MotherboardInternal.get_resources(motherboard) @@ -88,7 +88,7 @@ defmodule Helix.Server.Internal.MotherboardTest do net2 = "::f" |> Network.ID.cast!() {nic2, _} = ComponentSetup.nic(dlk: 1, ulk: 2, network_id: net2) - MotherboardInternal.link(motherboard, mobo, nic2, :nic_2) + MotherboardInternal.link(motherboard, mobo, nic2, :nic_3) # Let's fetch again... motherboard = MotherboardInternal.fetch(mobo.component_id) @@ -121,10 +121,10 @@ defmodule Helix.Server.Internal.MotherboardTest do initial_components = [ - {cpu, :cpu_0}, - {hdd, :hdd_0}, - {nic, :nic_0}, - {ram, :ram_0} + {cpu, :cpu_1}, + {hdd, :hdd_1}, + {nic, :nic_1}, + {ram, :ram_1} ] assert {:ok, motherboard} = @@ -134,16 +134,16 @@ defmodule Helix.Server.Internal.MotherboardTest do Enum.each(motherboard.slots, fn {slot_id, component} -> case slot_id do - :cpu_0 -> + :cpu_1 -> assert component.component_id == cpu.component_id - :hdd_0 -> + :hdd_1 -> assert component.component_id == hdd.component_id - :nic_0 -> + :nic_1 -> assert component.component_id == nic.component_id - :ram_0 -> + :ram_1 -> assert component.component_id == ram.component_id end end) @@ -158,8 +158,8 @@ defmodule Helix.Server.Internal.MotherboardTest do nic: nic } = ComponentSetup.mobo_components() - i0 = [{hdd, :cpu_0}, {cpu, :hdd_0}, {nic, :nic_0}, {ram, :ram_0}] - i1 = [{cpu, :cpu_0}, {hdd, :hdd_9}, {nic, :nic_0}, {ram, :ram_0}] + i0 = [{hdd, :cpu_1}, {cpu, :hdd_1}, {nic, :nic_1}, {ram, :ram_1}] + i1 = [{cpu, :cpu_1}, {hdd, :hdd_9}, {nic, :nic_1}, {ram, :ram_1}] assert {:error, reason} = MotherboardInternal.setup(mobo, i0) assert reason == :wrong_slot_type @@ -172,7 +172,7 @@ defmodule Helix.Server.Internal.MotherboardTest do %{mobo: mobo, cpu: cpu} = ComponentSetup.mobo_components() # Missing hdd, ram, nic... - initial_components = [{cpu, :cpu_0}] + initial_components = [{cpu, :cpu_1}] assert {:error, reason} = MotherboardInternal.setup(mobo, initial_components) @@ -190,10 +190,10 @@ defmodule Helix.Server.Internal.MotherboardTest do initial_components = [ - {mobo, :cpu_0}, - {hdd, :hdd_0}, - {nic, :nic_0}, - {ram, :ram_0} + {mobo, :cpu_1}, + {hdd, :hdd_1}, + {nic, :nic_1}, + {ram, :ram_1} ] assert {:error, reason} = @@ -210,11 +210,11 @@ defmodule Helix.Server.Internal.MotherboardTest do motherboard = MotherboardInternal.fetch(mobo.component_id) assert {:ok, entry} = - MotherboardInternal.link(motherboard, mobo, cpu, :cpu_1) + MotherboardInternal.link(motherboard, mobo, cpu, :cpu_2) assert entry.motherboard_id == mobo.component_id assert entry.linked_component_id == cpu.component_id - assert entry.slot_id == :cpu_1 + assert entry.slot_id == :cpu_2 new_motherboard = MotherboardInternal.fetch(mobo.component_id) @@ -247,7 +247,7 @@ defmodule Helix.Server.Internal.MotherboardTest do motherboard = MotherboardInternal.fetch(mobo.component_id) assert {:error, reason} = - MotherboardInternal.link(motherboard, mobo, cpu, :cpu_0) + MotherboardInternal.link(motherboard, mobo, cpu, :cpu_1) assert reason == :slot_in_use end end @@ -266,7 +266,7 @@ defmodule Helix.Server.Internal.MotherboardTest do new_motherboard = MotherboardInternal.fetch(mobo.component_id) refute motherboard == new_motherboard - refute Map.has_key?(new_motherboard.slots, :hdd_0) + refute Map.has_key?(new_motherboard.slots, :hdd_1) # The new motherboard has one less component attached to it assert length(new_motherboard.slots |> Map.to_list()) == diff --git a/test/server/public/index/motherboard_test.exs b/test/server/public/index/motherboard_test.exs new file mode 100644 index 00000000..987bc1d9 --- /dev/null +++ b/test/server/public/index/motherboard_test.exs @@ -0,0 +1,120 @@ +defmodule Helix.Server.Public.Index.MotherboardTest do + + use Helix.Test.Case.Integration + + alias Helix.Server.Public.Index.Motherboard, as: MotherboardIndex + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + alias Helix.Server.Query.Server, as: ServerQuery + + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Server.Helper, as: ServerHelper + alias Helix.Test.Server.Setup, as: ServerSetup + + @internet_id NetworkHelper.internet_id() + @internet_id_str to_string(@internet_id) + + describe "index/1" do + test "indexes empty motherboard" do + {server, _} = ServerSetup.server() + + # Remove mobo + ServerHelper.update_server_mobo(server, nil) + + # Look mah, no mobo + server = ServerQuery.fetch(server.server_id) + refute server.motherboard_id + + index = MotherboardIndex.index(server) + + refute index.motherboard_id + assert Enum.empty?(index.network_connections) + end + + test "indexes motherboard" do + {server, _} = ServerSetup.server() + + index = MotherboardIndex.index(server) + + assert index.motherboard == MotherboardQuery.fetch(server.motherboard_id) + assert [nc] = index.network_connections + + assert nc.network_id == @internet_id + assert nc.ip == ServerHelper.get_ip(server) + assert nc.nic_id == index.motherboard.slots.nic_1.component_id + end + end + + describe "render_index/1" do + test "renders empty motherboard" do + {server, _} = ServerSetup.server() + + # Remove mobo + ServerHelper.update_server_mobo(server, nil) + + # Look mah, no mobo + server = ServerQuery.fetch(server.server_id) + refute server.motherboard_id + + rendered = + server + |> MotherboardIndex.index() + |> MotherboardIndex.render_index() + + refute rendered.motherboard_id + assert Enum.empty?(rendered.network_connections) + assert Enum.empty?(rendered.slots) + end + + test "renders the motherboard index" do + {server, _} = ServerSetup.server() + + ServerHelper.update_server_mobo(server, :mobo_999) + + motherboard = MotherboardQuery.fetch(server.motherboard_id) + + [cpu] = MotherboardQuery.get_cpus(motherboard) + [hdd] = MotherboardQuery.get_hdds(motherboard) + [ram] = MotherboardQuery.get_rams(motherboard) + [nic] = MotherboardQuery.get_nics(motherboard) + + ip = ServerHelper.get_ip(server) + + rendered = + server + |> MotherboardIndex.index() + |> MotherboardIndex.render_index() + + assert rendered.motherboard_id == to_string(server.motherboard_id) + + # NC data is valid + assert ncs = rendered.network_connections + assert Map.has_key?(ncs, to_string(nic.component_id)) + + assert %{ + network_id: @internet_id_str, + ip: ip + } == ncs[to_string(nic.component_id)] + + # Slot data is valid + slots = rendered.slots + + assert rendered.slots["cpu_1"].component_id == to_string(cpu.component_id) + assert rendered.slots["cpu_1"].type == "cpu" + assert rendered.slots["hdd_1"].component_id == to_string(hdd.component_id) + assert rendered.slots["hdd_1"].type == "hdd" + assert rendered.slots["ram_1"].component_id == to_string(ram.component_id) + assert rendered.slots["ram_1"].type == "ram" + assert rendered.slots["nic_1"].component_id == to_string(nic.component_id) + assert rendered.slots["nic_1"].type == "nic" + + # Returned all slots (available and free slots) + assert map_size(slots) > 10 + + # Available slots have an empty `component_id` + refute rendered.slots["cpu_2"].component_id + refute rendered.slots["ram_2"].component_id + refute rendered.slots["hdd_2"].component_id + refute rendered.slots["nic_2"].component_id + end + end +end diff --git a/test/server/public/index_test.exs b/test/server/public/index_test.exs index ea6462b3..83b30243 100644 --- a/test/server/public/index_test.exs +++ b/test/server/public/index_test.exs @@ -186,6 +186,7 @@ defmodule Helix.Server.Public.IndexTest do # Info retrieved from sub-Indexes assert gateway.main_storage assert gateway.storages + assert gateway.hardware assert gateway.logs assert gateway.processes assert gateway.tunnels @@ -211,6 +212,7 @@ defmodule Helix.Server.Public.IndexTest do assert rendered.main_storage assert rendered.storages + assert rendered.hardware assert rendered.logs assert rendered.processes assert rendered.tunnels @@ -235,6 +237,7 @@ defmodule Helix.Server.Public.IndexTest do # Info retrieved from sub-Indexes assert remote.main_storage assert remote.storages + assert remote.hardware assert remote.logs assert remote.processes assert remote.tunnels @@ -258,6 +261,7 @@ defmodule Helix.Server.Public.IndexTest do assert rendered.main_storage assert rendered.storages + assert rendered.hardware assert rendered.logs assert rendered.processes assert rendered.tunnels diff --git a/test/server/websocket/channel/server/join_test.exs b/test/server/websocket/channel/server/join_test.exs index a02c0df5..eb2be674 100644 --- a/test/server/websocket/channel/server/join_test.exs +++ b/test/server/websocket/channel/server/join_test.exs @@ -33,7 +33,7 @@ defmodule Helix.Server.Websocket.Channel.Server.JoinTest do assert new_socket.assigns.gateway.entity_id == entity_id # metadata is valid - assert new_socket.assigns.meta.access_type == :local + assert new_socket.assigns.meta.access == :local # Does not have assigns exclusive to remote joins refute Map.has_key?(new_socket.assigns.gateway, :ip) @@ -168,7 +168,7 @@ defmodule Helix.Server.Websocket.Channel.Server.JoinTest do assert new_socket.assigns.destination.entity_id == destination_entity_id # Metadata is correct - assert new_socket.assigns.meta.access_type == :remote + assert new_socket.assigns.meta.access == :remote assert new_socket.assigns.meta.network_id == @internet_id # Other stuff diff --git a/test/server/websocket/channel/server/topics/motherboard_test.exs b/test/server/websocket/channel/server/topics/motherboard_test.exs new file mode 100644 index 00000000..560cdd22 --- /dev/null +++ b/test/server/websocket/channel/server/topics/motherboard_test.exs @@ -0,0 +1,212 @@ +defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + import Helix.Test.Macros + import Helix.Test.Channel.Macros + + alias Helix.Entity.Action.Entity, as: EntityAction + alias Helix.Network.Action.Network, as: NetworkAction + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + alias Helix.Server.Query.Server, as: ServerQuery + + alias HELL.TestHelper.Random + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Server.Helper, as: ServerHelper + + alias Helix.Test.Network.Setup, as: NetworkSetup + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + + @internet_id NetworkHelper.internet_id() + + describe "motherboard.update" do + test "updates the components (same motherboard)" do + {socket, %{gateway: server, gateway_entity: entity}} = + ChannelSetup.join_server(own_server: true) + + # Let's modify the server mobo to support multiple NICs + ServerHelper.update_server_mobo(server, :mobo_999) + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {hdd, _} = ComponentSetup.component(type: :hdd) + {nic1, _} = ComponentSetup.component(type: :nic) + {nic2, _} = ComponentSetup.component(type: :nic) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, hdd) + EntityAction.link_component(entity, nic1) + EntityAction.link_component(entity, nic2) + + # We'll assign two NCs to this mobo, one public (required) and another + # custom network + {network, _} = NetworkSetup.network() + + # Create a new NC + {:ok, nc_internet} = + NetworkAction.Connection.create(@internet_id, Random.ipv4(), entity) + + {:ok, nc_custom} = + NetworkAction.Connection.create(network, Random.ipv4(), entity) + + params = + %{ + "motherboard_id" => to_string(server.motherboard_id), + "slots" => %{ + "cpu_1" => to_string(cpu.component_id), + "ram_1" => to_string(ram.component_id), + "hdd_1" => to_string(hdd.component_id), + "nic_1" => to_string(nic1.component_id), + "nic_2" => to_string(nic2.component_id) + }, + "network_connections" => %{ + to_string(nic1.component_id) => %{ + "ip" => nc_internet.ip, + "network_id" => to_string(nc_internet.network_id) + }, + to_string(nic2.component_id) => %{ + "ip" => nc_custom.ip, + "network_id" => to_string(nc_custom.network_id) + } + } + } + + # Request the update + ref = push socket, "motherboard.update", params + + # It worked! + assert_reply ref, :ok, response, timeout(:slow) + + # Empty response. It's async! + assert Enum.empty?(response.data) + + # Client received the MotherboardUpdatedEvent + wait_events [:motherboard_updated] + + # But the underlying server components were modified!!! + motherboard = MotherboardQuery.fetch(server.motherboard_id) + + # See? Components are the ones we've just created + assert motherboard.slots.cpu_1 == cpu + assert motherboard.slots.ram_1 == ram + assert motherboard.slots.hdd_1 == hdd + assert motherboard.slots.nic_1 == nic1 + + # nic2 is also identical, but the component `custom` changed to point to + # the underlying network_id. Hence, we'll ignore it for this assertion + assert_map motherboard.slots.nic_2, nic2, skip: :custom + + # And the NetworkConnection must have also changed (nic1) + mobo_nc1 = NetworkQuery.Connection.fetch_by_nic(nic1.component_id) + + assert mobo_nc1.network_id == nc_internet.network_id + assert mobo_nc1.ip == nc_internet.ip + + assert motherboard.slots.nic_1.custom.network_id == nc_internet.network_id + + # NC for nic2 was updated too + mobo_nc2 = NetworkQuery.Connection.fetch_by_nic(nic2.component_id) + + assert mobo_nc2.network_id == nc_custom.network_id + assert mobo_nc2.ip == nc_custom.ip + + assert motherboard.slots.nic_2.custom.network_id == nc_custom.network_id + end + + test "updates the mobo (1-step switch to a new mobo)" do + {socket, %{gateway: server, gateway_entity: entity}} = + ChannelSetup.join_server(own_server: true) + + # This will be our new mobo + {new_mobo, _} = ComponentSetup.component(type: :mobo) + EntityAction.link_component(entity, new_mobo) + + # Get current motherboard components + motherboard = MotherboardQuery.fetch(server.motherboard_id) + [old_cpu] = MotherboardQuery.get_cpus(motherboard) + [old_ram] = MotherboardQuery.get_rams(motherboard) + [old_hdd] = MotherboardQuery.get_hdds(motherboard) + [old_nic] = MotherboardQuery.get_nics(motherboard) + + # Get current NetworkConnection assigned to `old_nic` + old_nc = NetworkQuery.Connection.fetch_by_nic(old_nic) + + params = + %{ + "motherboard_id" => to_string(new_mobo.component_id), + "slots" => %{ + "cpu_1" => to_string(old_cpu.component_id), + "ram_1" => to_string(old_ram.component_id), + "hdd_1" => to_string(old_hdd.component_id), + "nic_1" => to_string(old_nic.component_id), + }, + "network_connections" => %{ + to_string(old_nic.component_id) => %{ + "ip" => old_nc.ip, + "network_id" => to_string(old_nc.network_id) + }, + } + } + + # Request the update + ref = push socket, "motherboard.update", params + assert_reply ref, :ok, _, timeout(:slow) + + # Wait for completion + wait_events [:motherboard_updated] + + # Let's make sure the new motherboard is the one we've just requested + motherboard = MotherboardQuery.fetch(new_mobo.component_id) + + # Components are the same as before + assert motherboard.slots.cpu_1 == old_cpu + assert motherboard.slots.ram_1 == old_ram + assert motherboard.slots.hdd_1 == old_hdd + assert motherboard.slots.nic_1 == old_nic + + # Previous server mobo_id no longer exists + refute MotherboardQuery.fetch(server.motherboard_id) + + # And the NetworkConnection is also the same + mobo_nc = NetworkQuery.Connection.fetch_by_nic(old_nic.component_id) + assert mobo_nc == old_nc + end + + test "detaches the mobo (and unlinks the underlying components)" do + {socket, %{gateway: server}} = ChannelSetup.join_server(own_server: true) + + # Get current NC (used for later verification) + %{ip: ip, network_id: network_id} = ServerHelper.get_nip(server) + cur_nc = NetworkQuery.Connection.fetch(network_id, ip) + + # It is attached to a NIC + nic_id = cur_nc.nic_id + assert nic_id + + params = %{"cmd" => "detach"} + + ref = push socket, "motherboard.update", params + + # It worked! + assert_reply ref, :ok, response, timeout(:slow) + + # Empty response. It's async! + assert Enum.empty?(response.data) + + # Client received the MotherboardUpdatedEvent + wait_events [:motherboard_updated] + + new_server = ServerQuery.fetch(server.server_id) + + # Motherboard is gone! + refute new_server.motherboard_id + + # And so are all the components linked to it + refute MotherboardQuery.fetch(server.motherboard_id) + end + end +end diff --git a/test/server/websocket/requests/motherboard_update_test.exs b/test/server/websocket/requests/motherboard_update_test.exs new file mode 100644 index 00000000..c35bd650 --- /dev/null +++ b/test/server/websocket/requests/motherboard_update_test.exs @@ -0,0 +1,456 @@ +defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do + + use Helix.Test.Case.Integration + + import Helix.Test.Macros + + alias Helix.Websocket.Requestable + alias Helix.Entity.Action.Entity, as: EntityAction + alias Helix.Network.Action.Network, as: NetworkAction + alias Helix.Network.Model.Network + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Model.Component + alias Helix.Server.Query.Component, as: ComponentQuery + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Server.Websocket.Requests.MotherboardUpdate, + as: MotherboardUpdateRequest + + alias HELL.TestHelper.Random + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Network.Setup, as: NetworkSetup + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + alias Helix.Test.Server.Helper, as: ServerHelper + alias Helix.Test.Server.Setup, as: ServerSetup + + @internet_id NetworkHelper.internet_id() + + @mock_socket ChannelSetup.mock_server_socket(access: :local) + + describe "MotherboardUpdateRequest.check_params" do + test "casts the params to internal Helix format" do + params = + %{ + "motherboard_id" => "::1", + "slots" => %{ + "cpu_1" => "::f", + "ram_1" => nil, + }, + "network_connections" => %{ + "::5" => %{ + "network_id" => "::", + "ip" => "1.2.3.4" + } + } + } + + req = MotherboardUpdateRequest.new(params) + assert {:ok, req} = Requestable.check_params(req, @mock_socket) + + assert req.params.cmd == :update + assert req.params.mobo_id == Component.ID.cast!(params["motherboard_id"]) + assert req.params.slots.cpu_1 == Component.ID.cast!("::f") + refute req.params.slots.ram_1 + + [{nic_id, nip}] = req.params.network_connections + + assert nic_id == Component.ID.cast!("::5") + assert nip == {Network.ID.cast!("::"), "1.2.3.4"} + end + + test "infers players want to detach mobo when params are empty" do + params = %{"cmd" => "detach"} + + req = MotherboardUpdateRequest.new(params) + assert {:ok, req} = Requestable.check_params(req, @mock_socket) + + assert req.params.cmd == :detach + end + + test "handles invalid slot data" do + base_params = %{"motherboard_id" => "::"} + + # Invalid component type `notexists` + p1 = + %{ + "slots" => %{"notexists_50" => "::"} + } |> Map.merge(base_params) + + # Invalid slot `abc` + p2 = + %{ + "slots" => %{"cpu_abc" => nil} + } |> Map.merge(base_params) + + # Invalid component ID `wtf` + p3 = + %{ + "slots" => %{"cpu_1" => "wtf"} + } |> Map.merge(base_params) + + # Empty slots + p4 = base_params + + req1 = MotherboardUpdateRequest.new(p1) + req2 = MotherboardUpdateRequest.new(p2) + req3 = MotherboardUpdateRequest.new(p3) + req4 = MotherboardUpdateRequest.new(p4) + + assert {:error, %{message: reason1}, _} = + Requestable.check_params(req1, @mock_socket) + assert {:error, %{message: reason2}, _} = + Requestable.check_params(req2, @mock_socket) + assert {:error, %{message: reason3}, _} = + Requestable.check_params(req3, @mock_socket) + assert {:error, %{message: reason4}, _} = + Requestable.check_params(req4, @mock_socket) + + assert reason1 == "bad_slot_data" + assert reason2 == reason1 + assert reason3 == reason2 + assert reason4 == reason3 + end + + test "handles invalid network connections" do + base_params = + %{ + "motherboard_id" => "::f", + "slots" => %{"cpu_1" => "::1"}, + } + + # Empty NCs + p1 = base_params + + # Invalid NIC ID + p2 = + %{ + "network_connections" => %{ + "invalid_component" => %{ + "ip" => "1.2.3.4", + "network_id" => "::" + } + } + } |> Map.merge(base_params) + + # Invalid IP + p3 = + %{ + "network_connections" => %{ + "::f" => %{ + "ip" => "abc", + "network_id" => "::" + } + } + } |> Map.merge(base_params) + + # Invalid network ID + p4 = + %{ + "network_connections" => %{ + "::f" => %{ + "ip" => "127.0.0.1", + "network_id" => "invalid" + } + } + } |> Map.merge(base_params) + + req1 = MotherboardUpdateRequest.new(p1) + req2 = MotherboardUpdateRequest.new(p2) + req3 = MotherboardUpdateRequest.new(p3) + req4 = MotherboardUpdateRequest.new(p4) + + assert {:error, %{message: reason1}, _} = + Requestable.check_params(req1, @mock_socket) + assert {:error, %{message: reason2}, _} = + Requestable.check_params(req2, @mock_socket) + assert {:error, %{message: reason3}, _} = + Requestable.check_params(req3, @mock_socket) + assert {:error, %{message: reason4}, _} = + Requestable.check_params(req4, @mock_socket) + + assert reason1 == "bad_network_connections" + assert reason2 == reason1 + assert reason3 == reason2 + assert reason4 == reason3 + end + end + + describe "MotherboardUpdateRequest.check_permissions" do + test "accepts when data is valid (update)" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + # Create a new NC + {:ok, new_nc} = + NetworkAction.Connection.create(@internet_id, Random.ipv4(), entity) + + params = + %{ + "motherboard_id" => to_string(server.motherboard_id), + "slots" => %{ + "cpu_1" => to_string(cpu.component_id), + "ram_1" => to_string(ram.component_id), + "hdd_1" => to_string(hdd.component_id), + "nic_1" => to_string(nic.component_id) + }, + "network_connections" => %{ + to_string(nic.component_id) => %{ + "ip" => new_nc.ip, + "network_id" => to_string(new_nc.network_id) + } + } + } + + socket = + ChannelSetup.mock_server_socket( + gateway_id: server.server_id, + gateway_entity_id: entity.entity_id, + access: :local + ) + + request = MotherboardUpdateRequest.new(params) + assert {:ok, request} = Requestable.check_params(request, socket) + assert {:ok, request} = Requestable.check_permissions(request, socket) + + assert request.meta.components + assert request.meta.owned_components + assert request.meta.network_connections + assert request.meta.entity_network_connections + end + + test "rejects when something is wrong (update)" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + # Create a new NC + {:ok, new_nc} = + NetworkAction.Connection.create(@internet_id, Random.ipv4(), entity) + + # Note: for a full test of the validation see `ComponentHenforcerTest` + params = + %{ + "motherboard_id" => to_string(server.motherboard_id), + "slots" => %{ + "cpu_1" => to_string(cpu.component_id), + "ram_1" => to_string(ram.component_id), + "hdd_1" => to_string(hdd.component_id), + "nic_1" => to_string(nic.component_id) + }, + "network_connections" => %{ + to_string(nic.component_id) => %{ + "ip" => Random.ipv4(), # This IP does not belong to me!!11! + "network_id" => to_string(new_nc.network_id) + } + } + } + + socket = + ChannelSetup.mock_server_socket( + gateway_id: server.server_id, + gateway_entity_id: entity.entity_id, + access: :local + ) + + request = MotherboardUpdateRequest.new(params) + assert {:ok, request} = Requestable.check_params(request, socket) + assert {:error, %{message: reason}, _} = + Requestable.check_permissions(request, socket) + + assert reason == "network_connection_not_belongs" + end + + test "accepts when data is valid (detach)" do + {server, %{entity: entity}} = ServerSetup.server() + + params = %{"cmd" => "detach"} + + socket = + ChannelSetup.mock_server_socket( + gateway_id: server.server_id, + gateway_entity_id: entity.entity_id, + access: :local + ) + + request = MotherboardUpdateRequest.new(params) + assert {:ok, request} = Requestable.check_params(request, socket) + assert {:ok, request} = Requestable.check_permissions(request, socket) + + assert request.meta.server == server + end + end + + describe "MotherboardUpdateRequest.handle_request" do + test "updates the motherboard" do + {server, %{entity: entity}} = ServerSetup.server() + + # Let's modify the server mobo to support multiple NICs + ServerHelper.update_server_mobo(server, :mobo_999) + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {hdd, _} = ComponentSetup.component(type: :hdd) + {nic1, _} = ComponentSetup.component(type: :nic) + {nic2, _} = ComponentSetup.component(type: :nic) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, hdd) + EntityAction.link_component(entity, nic1) + EntityAction.link_component(entity, nic2) + + # We'll assign two NCs to this mobo, one public (required) and another + # custom network + {network, _} = NetworkSetup.network() + + # Create a new NC + {:ok, nc_internet} = + NetworkAction.Connection.create(@internet_id, Random.ipv4(), entity) + + {:ok, nc_custom} = + NetworkAction.Connection.create(network, Random.ipv4(), entity) + + params = + %{ + "motherboard_id" => to_string(server.motherboard_id), + "slots" => %{ + "cpu_1" => to_string(cpu.component_id), + "ram_1" => to_string(ram.component_id), + "hdd_1" => to_string(hdd.component_id), + "nic_1" => to_string(nic1.component_id), + "nic_2" => to_string(nic2.component_id) + }, + "network_connections" => %{ + to_string(nic1.component_id) => %{ + "ip" => nc_internet.ip, + "network_id" => to_string(nc_internet.network_id) + }, + to_string(nic2.component_id) => %{ + "ip" => nc_custom.ip, + "network_id" => to_string(nc_custom.network_id) + } + } + } + + socket = + ChannelSetup.mock_server_socket( + gateway_id: server.server_id, + gateway_entity_id: entity.entity_id, + access: :local + ) + + request = MotherboardUpdateRequest.new(params) + assert {:ok, request} = Requestable.check_params(request, socket) + assert {:ok, request} = Requestable.check_permissions(request, socket) + assert {:ok, _request} = Requestable.handle_request(request, socket) + + # Since updating the motherboard is asynchronous, we won't receive any + # information on the `request` returned at `handle_request/2`, and as such + # we'll proceed to render the empty request. + # However, the server mobo must have changed: + + # The new server is identical to the previous one, since we did not change + # the motherboard itself + new_server = ServerQuery.fetch(server.server_id) + assert new_server == server + + # The components linked to the mobo have changed too! + motherboard = MotherboardQuery.fetch(new_server.motherboard_id) + + # See? Components are the ones we've just created + assert motherboard.slots.cpu_1 == cpu + assert motherboard.slots.ram_1 == ram + assert motherboard.slots.hdd_1 == hdd + assert motherboard.slots.nic_1 == nic1 + + # nic2 is also identical, but the component `custom` changed to point to + # the underlying network_id. Hence, we'll ignore it for this assertion + assert_map motherboard.slots.nic_2, nic2, skip: :custom + + # And the NetworkConnection must have also changed (nic1) + mobo_nc1 = NetworkQuery.Connection.fetch_by_nic(nic1.component_id) + + assert mobo_nc1.network_id == nc_internet.network_id + assert mobo_nc1.ip == nc_internet.ip + + assert motherboard.slots.nic_1.custom.network_id == nc_internet.network_id + + # NC for nic2 was updated too + mobo_nc2 = NetworkQuery.Connection.fetch_by_nic(nic2.component_id) + + assert mobo_nc2.network_id == nc_custom.network_id + assert mobo_nc2.ip == nc_custom.ip + + assert motherboard.slots.nic_2.custom.network_id == nc_custom.network_id + end + + test "detaches the motherboard" do + {server, %{entity: entity}} = ServerSetup.server() + + # Get current NC (used for later verification) + %{ip: ip, network_id: network_id} = ServerHelper.get_nip(server) + cur_nc = NetworkQuery.Connection.fetch(network_id, ip) + + # It is attached to a NIC + nic_id = cur_nc.nic_id + assert nic_id + + params = %{"cmd" => "detach"} + + socket = + ChannelSetup.mock_server_socket( + gateway_id: server.server_id, + gateway_entity_id: entity.entity_id, + access: :local + ) + + request = MotherboardUpdateRequest.new(params) + assert {:ok, request} = Requestable.check_params(request, socket) + assert {:ok, request} = Requestable.check_permissions(request, socket) + assert {:ok, _request} = Requestable.handle_request(request, socket) + + # Detaching is asynchronous, so we don't care about the returned value of + # `handle_request/2`. Now we must make sure that the mobo was detached. + + new_server = ServerQuery.fetch(server.server_id) + + # Motherboard is gone! + refute new_server.motherboard_id + + # And so are all the components linked to it + refute MotherboardQuery.fetch(server.motherboard_id) + + # Underlying components still exist (but they are not linked to any mobo) + assert ComponentQuery.fetch(nic_id) + + # Old NIC points to no NC (i.e. no NCs are assigned to the NIC) + refute NetworkQuery.Connection.fetch_by_nic(nic_id) + + # Old NIP still exists - but it's unused + new_nc = NetworkQuery.Connection.fetch(network_id, ip) + + assert new_nc.network_id == network_id + assert new_nc.ip == ip + assert new_nc.entity_id == entity.entity_id + refute new_nc.nic_id + end + end +end diff --git a/test/support/channel/setup.ex b/test/support/channel/setup.ex index 2287b4a3..bb141bba 100644 --- a/test/support/channel/setup.ex +++ b/test/support/channel/setup.ex @@ -7,6 +7,7 @@ defmodule Helix.Test.Channel.Setup do alias Helix.Account.Query.Account, as: AccountQuery alias Helix.Account.Websocket.Channel.Account, as: AccountChannel alias Helix.Entity.Model.Entity + alias Helix.Entity.Query.Entity, as: EntityQuery alias Helix.Network.Model.Network alias Helix.Server.Model.Server alias Helix.Server.Query.Server, as: ServerQuery @@ -130,10 +131,12 @@ defmodule Helix.Test.Channel.Setup do Related: Account.t, \ gateway :: Server.t, \ - destination :: Server.t | nil, \ - destination_files :: [SoftwareSetup.file] | nil, \ + gateway_entity :: Entity.t \ gateway_files :: [SoftwareSetup.file] | nil, \ gateway_ip :: Network.ip, \ + destination :: Server.t | nil, \ + destination_entity :: Entity.t | nil \ + destination_files :: [SoftwareSetup.file] | nil, \ destination_ip :: Network.ip | nil """ def join_server(opts \\ []) do @@ -169,6 +172,7 @@ defmodule Helix.Test.Channel.Setup do gateway_related = %{ account: account, gateway: gateway, + gateway_entity: EntityQuery.fetch(socket.assigns.gateway.entity_id), gateway_ip: join.gateway_ip, gateway_files: gateway_files, } @@ -180,8 +184,12 @@ defmodule Helix.Test.Channel.Setup do destination_files = generate_files(opts[:destination_files], destination.server_id) + destination_entity = + EntityQuery.fetch(socket.assigns.destination.entity_id) + %{ destination: destination, + destination_entity: destination_entity, destination_ip: join.destination_ip, destination_files: destination_files } @@ -244,7 +252,7 @@ defmodule Helix.Test.Channel.Setup do - destination_ip - destination_entity_id - network_id - - access_type: Inferred if not set + - access: Inferred if not set - own_server: Force socket to represent own server channel. Defaults to false. - counter: Defaults to 0. - connect_opts: Opts that will be relayed to the `mock_connection_socket` @@ -265,10 +273,10 @@ defmodule Helix.Test.Channel.Setup do {server_id, server_ip, entity_id} end - access_type = + access = cond do - opts[:access_type] -> - opts[:access_type] + opts[:access] -> + opts[:access] gateway_id == destination_id -> :local @@ -280,7 +288,7 @@ defmodule Helix.Test.Channel.Setup do network_id = Keyword.get(opts, :network_id, Network.ID.generate()) counter = Keyword.get(opts, :counter, 0) meta = %{ - access_type: access_type, + access: access, network_id: network_id, counter: counter } diff --git a/test/support/event/setup/server.ex b/test/support/event/setup/server.ex new file mode 100644 index 00000000..6c9113df --- /dev/null +++ b/test/support/event/setup/server.ex @@ -0,0 +1,15 @@ +defmodule Helix.Test.Event.Setup.Server do + + alias Helix.Server.Model.Server + + alias Helix.Server.Event.Motherboard.Updated, as: MotherboardUpdatedEvent + + alias Helix.Test.Server.Setup, as: ServerSetup + + def motherboard_updated(server = %Server{}), + do: MotherboardUpdatedEvent.new(server) + def motherboard_updated do + {server, _} = ServerSetup.server() + motherboard_updated(server) + end +end diff --git a/test/support/network/setup.ex b/test/support/network/setup.ex index 3478c61f..63e7bdeb 100644 --- a/test/support/network/setup.ex +++ b/test/support/network/setup.ex @@ -1,7 +1,9 @@ defmodule Helix.Test.Network.Setup do + alias Ecto.Changeset alias Helix.Server.Model.Server alias Helix.Network.Model.Connection + alias Helix.Network.Model.Network alias Helix.Network.Model.Tunnel alias Helix.Network.Query.Tunnel, as: TunnelQuery alias Helix.Network.Repo, as: NetworkRepo @@ -125,4 +127,32 @@ defmodule Helix.Test.Network.Setup do {connection, %{tunnel: tunnel}} end + + @doc """ + See doc on `fake_network/1` + """ + def network(opts \\ []) do + {_, related = %{changeset: changeset}} = fake_network(opts) + {:ok, inserted} = NetworkRepo.insert(changeset) + {inserted, related} + end + + @doc """ + - network_id: specify network id. Defaults to random one + - name: Specify network name. Defaults to + """ + def fake_network(opts \\ []) do + network_id = Keyword.get(opts, :network_id, Network.ID.generate()) + name = Keyword.get(opts, :name, "LAN") + + network = + %Network{ + network_id: network_id, + name: name + } + + related = %{changeset: Changeset.change(network)} + + {network, related} + end end diff --git a/test/support/process/helper/view.ex b/test/support/process/helper/view.ex index 8f970b42..b98307ab 100644 --- a/test/support/process/helper/view.ex +++ b/test/support/process/helper/view.ex @@ -30,8 +30,8 @@ defmodule Helix.Test.Process.View.Helper do If `data` is expected to be empty, simply omit its parameter. """ - def assert_keys(rendered, access_type), - do: assert_keys(rendered, access_type, &empty_data_function/1) + def assert_keys(rendered, access), + do: assert_keys(rendered, access, &empty_data_function/1) def assert_keys(rendered, :full, data_function), do: check_view(rendered, :full, &pview_access_full/0, data_function) def assert_keys(rendered, :partial, data_function), diff --git a/test/support/server/component/setup.ex b/test/support/server/component/setup.ex index cf20436e..ca57413e 100644 --- a/test/support/server/component/setup.ex +++ b/test/support/server/component/setup.ex @@ -88,10 +88,10 @@ defmodule Helix.Test.Server.Component.Setup do initial_components = [ - {cpu, :cpu_0}, - {hdd, :hdd_0}, - {nic, :nic_0}, - {ram, :ram_0} + {cpu, :cpu_1}, + {hdd, :hdd_1}, + {nic, :nic_1}, + {ram, :ram_1} ] {:ok, entries} = MotherboardInternal.setup(mobo, initial_components) diff --git a/test/support/server/helper.ex b/test/support/server/helper.ex index 5f135cd3..9b25dae4 100644 --- a/test/support/server/helper.ex +++ b/test/support/server/helper.ex @@ -28,7 +28,7 @@ defmodule Helix.Test.Server.Helper do def get_nip(server = %Server{}), do: get_nip(server.server_id) def get_nip(server_id = %Server.ID{}), - do: get_all_nips(server_id) |> List.first() + do: get_all_nips(server_id) |> List.first() def get_all_nips(server = %Server{}), do: get_all_nips(server.server_id)