From 6ed5c695b41375db7687c81a203660c42d0a164f Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Thu, 14 Dec 2017 03:08:16 -0200 Subject: [PATCH] Add handle_request for MotherboardUpdateRequest --- lib/server/action/flow/server.ex | 14 +- .../action/{flow => }/motherboard/update.ex | 49 ++-- lib/server/henforcer/component.ex | 16 +- lib/server/public/server.ex | 19 ++ lib/server/websocket/channel/server.ex | 29 ++- .../websocket/requests/motherboard_update.ex | 50 +++- test/server/action/flow/server_test.exs | 59 +++++ test/server/action/motherboard_test.exs | 21 +- test/server/henforcer/component_test.exs | 9 +- .../server/topics/motherboard_test.exs | 126 ++++++++++ .../requests/motherboard_update_test.exs | 223 ++++++++++++++++++ test/support/channel/setup.ex | 12 +- 12 files changed, 569 insertions(+), 58 deletions(-) rename lib/server/action/{flow => }/motherboard/update.ex (71%) create mode 100644 test/server/websocket/channel/server/topics/motherboard_test.exs diff --git a/lib/server/action/flow/server.ex b/lib/server/action/flow/server.ex index c6d55005..db3c8c7a 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -47,7 +47,7 @@ defmodule Helix.Server.Action.Flow.Server do def update_mobo( server = %Server{}, - cur_mobo_data, + motherboard, new_mobo_data, entity_ncs, relay) @@ -56,21 +56,19 @@ defmodule Helix.Server.Action.Flow.Server do flowing do with \ - {:ok, new_mobo, events} <- - MotherboardAction.update( - cur_mobo_data, new_mobo_data, entity_ncs - ), + {: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 - {:ok, new_server, new_mobo} + {:ok, new_server, new_motherboard} end end end - defp update_server_mobo(%Server{motherboard_id: mobo_id}, mobo_id), - do: {:ok, mobo_id} + defp update_server_mobo(server = %Server{motherboard_id: mobo_id}, mobo_id), + do: {:ok, server} defp update_server_mobo(server, nil), do: ServerAction.detach(server) defp update_server_mobo(server, mobo_id), diff --git a/lib/server/action/flow/motherboard/update.ex b/lib/server/action/motherboard/update.ex similarity index 71% rename from lib/server/action/flow/motherboard/update.ex rename to lib/server/action/motherboard/update.ex index b14bc9eb..da5c7f31 100644 --- a/lib/server/action/flow/motherboard/update.ex +++ b/lib/server/action/motherboard/update.ex @@ -1,15 +1,16 @@ -# TODO: Move to motherboard action (outside flow) 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.Internal.Motherboard, as: MotherboardInternal + alias Helix.Server.Query.Component, as: ComponentQuery alias Helix.Server.Repo, as: ServerRepo - alias Helix.Server.Query.Component, as: ComponentQuery - - alias Helix.Server.Internal.Motherboard, as: MotherboardInternal + @internet_id NetworkQuery.internet().network_id def update(nil, mobo_data, entity_ncs) do {:ok, new_mobo} = @@ -43,10 +44,12 @@ defmodule Helix.Server.Action.Motherboard.Update do defp update_network_connections(mobo_data, entity_ncs) do # Secondary index used to figure out whether a specific NIP was specified on # `mobo_data.network_connections`. - assigned_nips = - Enum.reduce(mobo_data.network_connections, [], fn {_, nip}, acc -> - acc ++ [nip] - end) + # assigned_nips = + # Enum.reduce(mobo_data.network_connections, [], fn {_, nip}, acc -> + # acc ++ [nip] + # end) + + ncs = mobo_data.network_connections entity_ncs @@ -54,11 +57,9 @@ defmodule Helix.Server.Action.Motherboard.Update do |> Enum.reduce([], fn nc, acc -> cond do # The NIC already has an NC attached to it - Map.has_key?(mobo_data.network_connections, nc.nic_id) -> - nip = mobo_data.network_connections[nc.nic_id] - + 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.network_id, nc.ip} == nip do + if nc == mobo_nc.network_connection do acc # There will be a new NC attached to this NIC, so we have to @@ -73,19 +74,12 @@ defmodule Helix.Server.Action.Motherboard.Update do # 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. - {nc.network_id, nc.ip} in assigned_nips -> - {nic_id, _} = - mobo_data.network_connections - |> Enum.find(fn {_, {network_id, ip}} -> - network_id == nc.network_id and ip == nc.ip - end) - - acc ++ [{:set_nic, nc, nic_id}] + 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) @@ -93,11 +87,24 @@ defmodule Helix.Server.Action.Motherboard.Update do |> Enum.each(&perform_network_op/1) end + defp has_nic?(ncs, nic_id), + do: Enum.find(ncs, &(&1.nic_id == nic_id)) + + defp has_nip?(ncs, network_id, ip), + do: Enum.find(ncs, &(&1.network_id == network_id and &1.ip == ip)) + 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/henforcer/component.ex b/lib/server/henforcer/component.ex index c66e54c4..fb314c83 100644 --- a/lib/server/henforcer/component.ex +++ b/lib/server/henforcer/component.ex @@ -94,7 +94,7 @@ defmodule Helix.Server.Henforcer.Component do init = {{true, %{}}, nil} network_connections - |> Enum.reduce_while(init, fn {_nic_id, nip}, {{true, acc}, cache} -> + |> Enum.reduce_while(init, fn {nic_id, nip}, {{true, acc}, cache} -> {network_id, ip} = nip with \ @@ -102,9 +102,17 @@ defmodule Helix.Server.Henforcer.Component do 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 ++ [r1.network_connection]) + |> put_in([:network_connections], acc_nc ++ [new_entry]) |> put_in( [:entity_network_connections], r1.entity_network_connections ) @@ -120,7 +128,7 @@ defmodule Helix.Server.Henforcer.Component do with \ {true, r0} <- component_exists?(mobo_id), - mobo = r0.component, + {r0, mobo} = get_and_replace(r0, :component, :mobo), # Make sure user is plugging components into a motherboard {true, _} <- is_motherboard?(mobo), @@ -143,7 +151,7 @@ defmodule Helix.Server.Henforcer.Component do # The mobo must have at least one public NC assigned to it {true, _} <- has_public_nip?(r2.network_connections) do - reply_ok(relay([r1, r2])) + reply_ok(relay([r0, r1, r2])) else error -> error diff --git a/lib/server/public/server.ex b/lib/server/public/server.ex index 4171aed4..2937e202 100644 --- a/lib/server/public/server.ex +++ b/lib/server/public/server.ex @@ -9,6 +9,7 @@ defmodule Helix.Server.Public.Server do alias Helix.Server.Model.Server alias Helix.Server.Action.Flow.Server, as: ServerFlow 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 +40,24 @@ defmodule Helix.Server.Public.Server do end end + 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 + defdelegate set_hostname(server, hostname, relay), to: ServerFlow diff --git a/lib/server/websocket/channel/server.ex b/lib/server/websocket/channel/server.ex index a5414c8b..a60ccfa8 100644 --- a/lib/server/websocket/channel/server.ex +++ b/lib/server/websocket/channel/server.ex @@ -127,18 +127,23 @@ channel Helix.Server.Websocket.Channel.Server do topic "config.check", ConfigCheckRequest @doc """ - {} => mobo.detach - - \/ update - { - "motherboard_id" : "motherboard_id" - , "slots" : - { "slot_id_a" : "component_id" , "slot_id_b" : null} - , "network_connections" : - { "component_id" : - { "ip" : "ip" , "network_id" : "network_id"} - } - } + 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 diff --git a/lib/server/websocket/requests/motherboard_update.ex b/lib/server/websocket/requests/motherboard_update.ex index 1fb0bbf0..5ce8c190 100644 --- a/lib/server/websocket/requests/motherboard_update.ex +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -2,9 +2,13 @@ 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 @@ -60,10 +64,50 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do end end - def check_permissions(request, socket) do + 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, socket) do + 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() @@ -88,7 +132,6 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do end end - # TODO: Move to a SpecableHelper or something like that defp cast_slots(nil), do: :bad_slots defp cast_slots(slots) do @@ -143,5 +186,4 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do :error 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 index 6ebb8e58..345d5a59 100644 --- a/test/server/action/motherboard_test.exs +++ b/test/server/action/motherboard_test.exs @@ -59,14 +59,17 @@ defmodule Helix.Server.Action.Motherboardtest do # NetworkConnection assigned to it, and `old_nc` will be replaced. new_network_connections = %{ - old_nic.component_id => {new_nc.network_id, new_nc.ip} + 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 + network_connections: [new_network_connections] } entity_ncs = NetworkQuery.Connection.get_by_entity(entity.entity_id) @@ -141,14 +144,17 @@ defmodule Helix.Server.Action.Motherboardtest do new_network_connections = %{ - nic.component_id => {nc.network_id, nc.ip} + 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 + network_connections: [new_network_connections] } entity_ncs = NetworkQuery.Connection.get_by_entity(entity.entity_id) @@ -202,14 +208,17 @@ defmodule Helix.Server.Action.Motherboardtest do # We are setting to the old nic (on a new mobo) the old NC new_network_connections = %{ - old_nic.component_id => {nc.network_id, nc.ip} + 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 + network_connections: [new_network_connections] } entity_ncs = NetworkQuery.Connection.get_by_entity(entity.entity_id) diff --git a/test/server/henforcer/component_test.exs b/test/server/henforcer/component_test.exs index 54e8f2c2..404bc667 100644 --- a/test/server/henforcer/component_test.exs +++ b/test/server/henforcer/component_test.exs @@ -65,10 +65,16 @@ defmodule Helix.Server.Henforcer.ComponentTest do 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 [new_nc] == relay.network_connections + 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 @@ -76,6 +82,7 @@ defmodule Helix.Server.Henforcer.ComponentTest do assert_relay relay, [ :entity, + :mobo, :components, :owned_components, :network_connections, 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..c297feba --- /dev/null +++ b/test/server/websocket/channel/server/topics/motherboard_test.exs @@ -0,0 +1,126 @@ +defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do + + use Helix.Test.Case.Integration + + 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 + {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) + + # TODO: Test received the events + + # 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 + end +end diff --git a/test/server/websocket/requests/motherboard_update_test.exs b/test/server/websocket/requests/motherboard_update_test.exs index 7904f92d..78b688c2 100644 --- a/test/server/websocket/requests/motherboard_update_test.exs +++ b/test/server/websocket/requests/motherboard_update_test.exs @@ -2,13 +2,28 @@ 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.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_type: :local) @@ -150,4 +165,212 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do assert reason4 == reason3 end end + + describe "MotherboardUpdateRequest.check_permissions" do + test "accepts when data is valid" 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_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.components + assert request.meta.owned_components + assert request.meta.network_connections + assert request.meta.entity_network_connections + end + + test "rejects when something is wrong" 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_type: :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 + 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_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) + + # 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 + end end diff --git a/test/support/channel/setup.ex b/test/support/channel/setup.ex index 2287b4a3..89806171 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 }