diff --git a/lib/entity/henforcer/entity.ex b/lib/entity/henforcer/entity.ex index 1db6791c..074f9f3c 100644 --- a/lib/entity/henforcer/entity.ex +++ b/lib/entity/henforcer/entity.ex @@ -120,6 +120,6 @@ defmodule Helix.Entity.Henforcer.Entity do else reply_error({:network_connection, :not_belongs}) end - |> wrap_relay(%{entity_network_connections: owned}) + |> wrap_relay(%{entity_network_connections: owned, entity: entity}) end end diff --git a/lib/server/action/flow/server.ex b/lib/server/action/flow/server.ex index db3c8c7a..0f0abe3f 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -8,6 +8,7 @@ defmodule Helix.Server.Action.Flow.Server do 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 @spec setup(Server.type, Entity.t, Component.mobo, Event.relay) :: @@ -67,6 +68,17 @@ defmodule Helix.Server.Action.Flow.Server do end end + def detach_mobo(server = %Server{}, motherboard = %Motherboard{}, _relay) do + flowing do + with \ + :ok <- MotherboardAction.detach(motherboard), + {:ok, new_server} <- ServerAction.detach(server) + do + {:ok, new_server} + end + end + end + defp update_server_mobo(server = %Server{motherboard_id: mobo_id}, mobo_id), do: {:ok, server} defp update_server_mobo(server, nil), diff --git a/lib/server/action/motherboard.ex b/lib/server/action/motherboard.ex index 6662fdab..f9b76704 100644 --- a/lib/server/action/motherboard.ex +++ b/lib/server/action/motherboard.ex @@ -15,4 +15,7 @@ defmodule Helix.Server.Action.Motherboard do 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 index da5c7f31..9af9d3e3 100644 --- a/lib/server/action/motherboard/update.ex +++ b/lib/server/action/motherboard/update.ex @@ -6,12 +6,33 @@ defmodule Helix.Server.Action.Motherboard.Update do 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 + def detach(motherboard = %Motherboard{}) do + MotherboardInternal.unlink_all(motherboard) + + 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 + def update(nil, mobo_data, entity_ncs) do {:ok, new_mobo} = MotherboardInternal.setup(mobo_data.mobo, mobo_data.components) diff --git a/lib/server/henforcer/component.ex b/lib/server/henforcer/component.ex index fb314c83..361f682b 100644 --- a/lib/server/henforcer/component.ex +++ b/lib/server/henforcer/component.ex @@ -4,9 +4,12 @@ defmodule Helix.Server.Henforcer.Component do alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer alias Helix.Network.Query.Network, as: NetworkQuery + 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 @@ -90,7 +93,7 @@ defmodule Helix.Server.Henforcer.Component do |> elem(0) end - reduce_network_connections = fn entity, components -> + reduce_network_connections = fn entity -> init = {{true, %{}}, nil} network_connections @@ -146,7 +149,7 @@ defmodule Helix.Server.Henforcer.Component do {true, _} <- has_initial_components?(components), # Iterate over NetworkConnections and make the required henforcements - {true, r2} <- reduce_network_connections.(entity, components), + {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) @@ -157,4 +160,20 @@ defmodule Helix.Server.Henforcer.Component do error end end + + def can_detach_mobo?(server_id = %Server.ID{}) do + henforce ServerHenforcer.server_exists?(server_id) do + can_detach_mobo?(relay.server) + end + end + + # TODO: Mainframe verification, cost analysis (for cooldown) etc. + def can_detach_mobo?(server = %Server{}) do + with \ + {true, _} <- component_exists?(server.motherboard_id) + do + motherboard = MotherboardQuery.fetch(server.motherboard_id) + reply_ok(%{motherboard: motherboard}) + end + end end diff --git a/lib/server/public/server.ex b/lib/server/public/server.ex index 2937e202..ea4c6e49 100644 --- a/lib/server/public/server.ex +++ b/lib/server/public/server.ex @@ -58,6 +58,10 @@ defmodule Helix.Server.Public.Server do ServerFlow.update_mobo(server, motherboard, mobo_data, entity_ncs, relay) end + def detach_mobo(server, motherboard, relay) do + ServerFlow.detach_mobo(server, motherboard, relay) + end + defdelegate set_hostname(server, hostname, relay), to: ServerFlow diff --git a/lib/server/websocket/requests/motherboard_update.ex b/lib/server/websocket/requests/motherboard_update.ex index 5ce8c190..dec4fcd5 100644 --- a/lib/server/websocket/requests/motherboard_update.ex +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -13,7 +13,7 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do alias Helix.Server.Public.Server, as: ServerPublic def check_params(request, socket) do - if Enum.empty?(request.unsafe) do + if request.unsafe["cmd"] == "detach" do check_detach(request, socket) else check_update(request, socket) @@ -64,6 +64,25 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do end end + def check_permissions(request = %{params: %{cmd: :detach}}, socket) do + gateway_id = socket.assigns.gateway.server_id + + with \ + {true, relay} <- ComponentHenforcer.can_detach_mobo?(gateway_id) + do + meta = %{ + server: relay.server, + motherboard: relay.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 @@ -92,7 +111,19 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do end end - def handle_request(request = %{params: %{cmd: :update}}, socket) do + 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 diff --git a/test/server/websocket/channel/server/topics/motherboard_test.exs b/test/server/websocket/channel/server/topics/motherboard_test.exs index c297feba..8e9515d3 100644 --- a/test/server/websocket/channel/server/topics/motherboard_test.exs +++ b/test/server/websocket/channel/server/topics/motherboard_test.exs @@ -5,29 +5,21 @@ defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do import Phoenix.ChannelTest 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.Motherboard, as: MotherboardQuery alias Helix.Server.Query.Server, as: ServerQuery alias HELL.TestHelper.Random - alias Helix.Test.Cache.Helper, as: CacheHelper alias Helix.Test.Channel.Setup, as: ChannelSetup alias Helix.Test.Network.Helper, as: NetworkHelper - alias Helix.Test.Software.Setup, as: SoftwareSetup - alias Helix.Test.Universe.NPC.Helper, as: NPCHelper alias Helix.Test.Server.Helper, as: ServerHelper - alias Helix.Test.Server.Setup, as: ServerSetup alias Helix.Test.Network.Setup, as: NetworkSetup alias Helix.Test.Server.Component.Setup, as: ComponentSetup @internet_id NetworkHelper.internet_id() - @internet_str to_string(@internet_id) describe "motherboard.update" do test "updates the components" do @@ -122,5 +114,37 @@ defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do assert motherboard.slots.nic_2.custom.network_id == nc_custom.network_id 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) + + # TODO: Test received the events + + 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 index 78b688c2..189babab 100644 --- a/test/server/websocket/requests/motherboard_update_test.exs +++ b/test/server/websocket/requests/motherboard_update_test.exs @@ -10,6 +10,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do 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, @@ -58,6 +59,15 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do 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" => "::"} @@ -167,7 +177,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do end describe "MotherboardUpdateRequest.check_permissions" do - test "accepts when data is valid" do + test "accepts when data is valid (update)" do {server, %{entity: entity}} = ServerSetup.server() {cpu, _} = ComponentSetup.component(type: :cpu) @@ -218,7 +228,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do assert request.meta.entity_network_connections end - test "rejects when something is wrong" do + test "rejects when something is wrong (update)" do {server, %{entity: entity}} = ServerSetup.server() {cpu, _} = ComponentSetup.component(type: :cpu) @@ -267,6 +277,25 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do 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_type: :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 @@ -372,5 +401,56 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do 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_type: :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/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)