From 0239fb66a6b9d15b3afe158a97b54ea1620f989f Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Mon, 11 Dec 2017 20:51:11 -0200 Subject: [PATCH 01/12] Add MotherboardAction.Update --- lib/network/action/network/connection.ex | 2 +- lib/network/internal/network/connection.ex | 4 +- lib/network/model/network/connection.ex | 7 + lib/process/resources.ex | 3 +- lib/server/action/flow/motherboard.ex | 2 +- lib/server/action/flow/motherboard/update.ex | 103 ++++++++ lib/server/action/flow/server.ex | 32 +++ lib/server/action/motherboard.ex | 3 + lib/server/component/specs/specable.ex | 28 +-- lib/server/internal/motherboard.ex | 17 +- lib/server/model/motherboard.ex | 10 +- lib/server/query/motherboard.ex | 2 + test/server/action/motherboard_test.exs | 232 +++++++++++++++++++ test/server/internal/motherboard_test.exs | 50 ++-- test/support/server/component/setup.ex | 8 +- 15 files changed, 449 insertions(+), 54 deletions(-) create mode 100644 lib/server/action/flow/motherboard/update.ex create mode 100644 test/server/action/motherboard_test.exs 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..48a8ad8a 100644 --- a/lib/network/model/network/connection.ex +++ b/lib/network/model/network/connection.ex @@ -74,6 +74,13 @@ defmodule Helix.Network.Model.Network.Connection do |> validate_required(@required_fields) end + 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/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/motherboard/update.ex b/lib/server/action/flow/motherboard/update.ex new file mode 100644 index 00000000..b14bc9eb --- /dev/null +++ b/lib/server/action/flow/motherboard/update.ex @@ -0,0 +1,103 @@ +# 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.Server.Repo, as: ServerRepo + + alias Helix.Server.Query.Component, as: ComponentQuery + + alias Helix.Server.Internal.Motherboard, as: MotherboardInternal + + def update(nil, mobo_data, entity_ncs) do + {:ok, new_mobo} = + MotherboardInternal.setup(mobo_data.mobo, mobo_data.components) + + 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 + + hespawn fn -> + update_network_connections(mobo_data, entity_ncs) + end + + {:ok, new_mobo, []} + end + + 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) + + 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 + Map.has_key?(mobo_data.network_connections, nc.nic_id) -> + nip = mobo_data.network_connections[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 + 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. + {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}] + + # 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 + + 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) + end +end diff --git a/lib/server/action/flow/server.ex b/lib/server/action/flow/server.ex index 1fbb1f88..c6d55005 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -5,6 +5,7 @@ defmodule Helix.Server.Action.Flow.Server do alias Helix.Event alias Helix.Entity.Action.Entity, as: EntityAction alias Helix.Entity.Model.Entity + alias Helix.Server.Action.Motherboard, as: MotherboardAction alias Helix.Server.Action.Server, as: ServerAction alias Helix.Server.Model.Component alias Helix.Server.Model.Server @@ -43,4 +44,35 @@ defmodule Helix.Server.Action.Flow.Server do """ def set_hostname(server, hostname, _relay), do: ServerAction.set_hostname(server, hostname) + + def update_mobo( + server = %Server{}, + cur_mobo_data, + new_mobo_data, + entity_ncs, + relay) + do + new_mobo_id = new_mobo_data.mobo.component_id + + flowing do + with \ + {:ok, new_mobo, events} <- + MotherboardAction.update( + cur_mobo_data, 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} + end + end + end + + defp update_server_mobo(%Server{motherboard_id: mobo_id}, mobo_id), + do: {:ok, mobo_id} + defp update_server_mobo(server, nil), + do: ServerAction.detach(server) + defp update_server_mobo(server, mobo_id), + do: ServerAction.attach(server, mobo_id) end diff --git a/lib/server/action/motherboard.ex b/lib/server/action/motherboard.ex index eb3e5979..6662fdab 100644 --- a/lib/server/action/motherboard.ex +++ b/lib/server/action/motherboard.ex @@ -12,4 +12,7 @@ defmodule Helix.Server.Action.Motherboard do defdelegate unlink(component), to: MotherboardInternal + + defdelegate update(cur_mobo_data, new_mobo_data, entity_ncs), + to: __MODULE__.Update end 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/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/model/motherboard.ex b/lib/server/model/motherboard.ex index 536f9de7..21721196 100644 --- a/lib/server/model/motherboard.ex +++ b/lib/server/model/motherboard.ex @@ -331,10 +331,12 @@ defmodule Helix.Server.Model.Motherboard do @spec by_motherboard(Queryable.t, Motherboard.idt) :: 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/query/motherboard.ex b/lib/server/query/motherboard.ex index 78bbd36e..410048a7 100644 --- a/lib/server/query/motherboard.ex +++ b/lib/server/query/motherboard.ex @@ -30,4 +30,6 @@ defmodule Helix.Server.Query.Motherboard do to: MotherboardInternal defdelegate get_nics(motherboard), to: MotherboardInternal + defdelegate get_rams(motherboard), + to: MotherboardInternal end diff --git a/test/server/action/motherboard_test.exs b/test/server/action/motherboard_test.exs new file mode 100644 index 00000000..6ebb8e58 --- /dev/null +++ b/test/server/action/motherboard_test.exs @@ -0,0 +1,232 @@ +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 = + %{ + old_nic.component_id => {new_nc.network_id, new_nc.ip} + } + + 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.component_id => {nc.network_id, nc.ip} + } + + 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 = + %{ + old_nic.component_id => {nc.network_id, nc.ip} + } + + 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/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/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) From 1d287dab98594b9e7d6e1e4e22afe2dedcdc50de Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Tue, 12 Dec 2017 01:01:36 -0200 Subject: [PATCH 02/12] MotherboardUpdateRequest (check_params) --- lib/server/websocket/channel/server.ex | 18 +++ .../websocket/requests/motherboard_update.ex | 147 +++++++++++++++++ .../requests/motherboard_update_test.exs | 153 ++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 lib/server/websocket/requests/motherboard_update.ex create mode 100644 test/server/websocket/requests/motherboard_update_test.exs diff --git a/lib/server/websocket/channel/server.ex b/lib/server/websocket/channel/server.ex index a32b9853..a5414c8b 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,22 @@ 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"} + } + } + """ + topic "motherboard.update", MotherboardUpdateRequest + @doc """ Updates the server hostname. diff --git a/lib/server/websocket/requests/motherboard_update.ex b/lib/server/websocket/requests/motherboard_update.ex new file mode 100644 index 00000000..403ef782 --- /dev/null +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -0,0 +1,147 @@ +import Helix.Websocket.Request + +request Helix.Server.Websocket.Requests.MotherboardUpdate do + + alias HELL.IPv4 + alias HELL.Utils + alias Helix.Network.Model.Network + alias Helix.Server.Model.Component + alias Helix.Server.Public.Server, as: ServerPublic + + def check_params(request, socket) do + if Enum.empty?(request.unsafe) do + check_detach(request, socket) + else + check_update(request, socket) + end + end + + def check_detach(request, socket) do + with \ + true <- socket.assigns.meta.access_type == :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_type == :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, socket) do + end + + def handle_request(request, socket) do + 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, {ip, network_id}} + end) + + {:ok, ncs} + rescue + _ -> + :bad_ncs + end + end + + # TODO: Move to a SpecableHelper or something like that + 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/test/server/websocket/requests/motherboard_update_test.exs b/test/server/websocket/requests/motherboard_update_test.exs new file mode 100644 index 00000000..3c6e40c4 --- /dev/null +++ b/test/server/websocket/requests/motherboard_update_test.exs @@ -0,0 +1,153 @@ +defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do + + use Helix.Test.Case.Integration + + alias Helix.Websocket.Requestable + alias Helix.Network.Model.Network + alias Helix.Server.Model.Component + alias Helix.Server.Websocket.Requests.MotherboardUpdate, + as: MotherboardUpdateRequest + + alias Helix.Test.Channel.Setup, as: ChannelSetup + + @mock_socket ChannelSetup.mock_server_socket(access_type: :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 == {"1.2.3.4", Network.ID.cast!("::")} + 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 +end From 28055c77d69cbf94e9c7d3a640818e0a90b4723e Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Tue, 12 Dec 2017 03:36:45 -0200 Subject: [PATCH 03/12] Add Henforcers for MotherboardUpdateRequest --- lib/entity/henforcer/entity.ex | 59 ++++ lib/server/henforcer/component.ex | 152 +++++++++ lib/server/henforcer/server.ex | 1 + lib/server/model/motherboard.ex | 2 +- .../websocket/requests/motherboard_update.ex | 2 +- test/entity/henforcer/entity_test.exs | 64 ++++ test/server/henforcer/component_test.exs | 306 ++++++++++++++++++ .../requests/motherboard_update_test.exs | 2 +- test/support/network/setup.ex | 30 ++ 9 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 lib/server/henforcer/component.ex create mode 100644 test/server/henforcer/component_test.exs diff --git a/lib/entity/henforcer/entity.ex b/lib/entity/henforcer/entity.ex index e640c5ee..1db6791c 100644 --- a/lib/entity/henforcer/entity.ex +++ b/lib/entity/henforcer/entity.ex @@ -2,8 +2,12 @@ defmodule Helix.Entity.Henforcer.Entity do import Helix.Henforcer + 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 +67,59 @@ defmodule Helix.Entity.Henforcer.Entity do end |> wrap_relay(%{entity: entity, server: server}) end + + 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 + + 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}) + end end diff --git a/lib/server/henforcer/component.ex b/lib/server/henforcer/component.ex new file mode 100644 index 00000000..c66e54c4 --- /dev/null +++ b/lib/server/henforcer/component.ex @@ -0,0 +1,152 @@ +defmodule Helix.Server.Henforcer.Component do + + import Helix.Henforcer + + alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Model.Component + alias Helix.Server.Model.Motherboard + alias Helix.Server.Query.Component, as: ComponentQuery + + @internet_id NetworkQuery.internet().network_id + + 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 + + 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 + + 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 + + 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 + + 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 + + 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, components -> + 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_acc = + acc + |> put_in([:network_connections], acc_nc ++ [r1.network_connection]) + |> 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), + mobo = r0.component, + + # 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, components), + + # 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])) + else + error -> + error + end + end +end diff --git a/lib/server/henforcer/server.ex b/lib/server/henforcer/server.ex index f2b80d4e..27c974f3 100644 --- a/lib/server/henforcer/server.ex +++ b/lib/server/henforcer/server.ex @@ -19,6 +19,7 @@ defmodule Helix.Server.Henforcer.Server do @doc """ Ensures the requested server exists on the database. """ + # TODO: REVIEW: Why does it accept `Server.t` as input? def server_exists?(server = %Server{}), do: server_exists?(server.server_id) def server_exists?(server_id = %Server.ID{}) do diff --git a/lib/server/model/motherboard.ex b/lib/server/model/motherboard.ex index 21721196..37de3666 100644 --- a/lib/server/model/motherboard.ex +++ b/lib/server/model/motherboard.ex @@ -314,7 +314,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, diff --git a/lib/server/websocket/requests/motherboard_update.ex b/lib/server/websocket/requests/motherboard_update.ex index 403ef782..1fb0bbf0 100644 --- a/lib/server/websocket/requests/motherboard_update.ex +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -78,7 +78,7 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do {:ok, ip} = IPv4.cast(nc["ip"]) {:ok, network_id} = Network.ID.cast(nc["network_id"]) - {nic_id, {ip, network_id}} + {nic_id, {network_id, ip}} end) {:ok, ncs} 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/henforcer/component_test.exs b/test/server/henforcer/component_test.exs new file mode 100644 index 00000000..54e8f2c2 --- /dev/null +++ b/test/server/henforcer/component_test.exs @@ -0,0 +1,306 @@ +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 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.Setup, as: ServerSetup + + @internet_id NetworkHelper.internet_id() + + describe "can_update_mobo?/n" 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 length(relay.owned_components) >= 4 + + # All network_connections passed as a param were added to this relay + assert [new_nc] == relay.network_connections + + # And `entity_network_connections` has all NCs for that entity + assert length(relay.entity_network_connections) == 2 + + assert_relay relay, + [ + :entity, + :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 +end diff --git a/test/server/websocket/requests/motherboard_update_test.exs b/test/server/websocket/requests/motherboard_update_test.exs index 3c6e40c4..7904f92d 100644 --- a/test/server/websocket/requests/motherboard_update_test.exs +++ b/test/server/websocket/requests/motherboard_update_test.exs @@ -40,7 +40,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do [{nic_id, nip}] = req.params.network_connections assert nic_id == Component.ID.cast!("::5") - assert nip == {"1.2.3.4", Network.ID.cast!("::")} + assert nip == {Network.ID.cast!("::"), "1.2.3.4"} end test "handles invalid slot data" do 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 From c2067bbed325b30465953600422ae7fc9da87b2f Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Thu, 14 Dec 2017 03:08:16 -0200 Subject: [PATCH 04/12] 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 } From 7af42f787e6576dcdf8d30fb4d5872034ea2a00c Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Thu, 14 Dec 2017 18:38:45 -0200 Subject: [PATCH 05/12] Support 'detach' action on MotherboardUpdateRequest --- lib/entity/henforcer/entity.ex | 2 +- lib/server/action/flow/server.ex | 12 +++ lib/server/action/motherboard.ex | 3 + lib/server/action/motherboard/update.ex | 21 +++++ lib/server/henforcer/component.ex | 23 ++++- lib/server/public/server.ex | 4 + .../websocket/requests/motherboard_update.ex | 35 +++++++- .../server/topics/motherboard_test.exs | 40 +++++++-- .../requests/motherboard_update_test.exs | 84 ++++++++++++++++++- test/support/server/helper.ex | 2 +- 10 files changed, 210 insertions(+), 16 deletions(-) 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) From 3fecf8df132b2523dda584f03d634be184862fdb Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Fri, 15 Dec 2017 01:30:49 -0200 Subject: [PATCH 06/12] Add HardwareIndex and MotherboardIndex on ServerBootstrap --- lib/network/model/network/connection.ex | 2 +- lib/server/action/flow/server.ex | 2 - lib/server/action/server.ex | 21 ++- lib/server/internal/server.ex | 15 ++- lib/server/model/motherboard.ex | 2 +- lib/server/public/index.ex | 10 +- lib/server/public/index/hardware.ex | 31 +++++ lib/server/public/index/motherboard.ex | 97 ++++++++++++++ lib/server/query/motherboard.ex | 5 + test/server/public/index/motherboard_test.exs | 121 ++++++++++++++++++ test/server/public/index_test.exs | 4 + 11 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 lib/server/public/index/hardware.ex create mode 100644 lib/server/public/index/motherboard.ex create mode 100644 test/server/public/index/motherboard_test.exs diff --git a/lib/network/model/network/connection.ex b/lib/network/model/network/connection.ex index 48a8ad8a..ce1974a3 100644 --- a/lib/network/model/network/connection.ex +++ b/lib/network/model/network/connection.ex @@ -65,7 +65,7 @@ defmodule Helix.Network.Model.Network.Connection do |> validate_required(@required_fields) end - @spec update_nic(t, Component.nic) :: + @spec update_nic(t, Component.nic | nil) :: changeset def update_nic(nc = %__MODULE__{}, nic = %Component{type: :nic}) do nc diff --git a/lib/server/action/flow/server.ex b/lib/server/action/flow/server.ex index 0f0abe3f..5eac690a 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -81,8 +81,6 @@ defmodule Helix.Server.Action.Flow.Server do 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), do: ServerAction.attach(server, mobo_id) 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/internal/server.ex b/lib/server/internal/server.ex index c745d3f8..e63dffc4 100644 --- a/lib/server/internal/server.ex +++ b/lib/server/internal/server.ex @@ -59,15 +59,18 @@ defmodule Helix.Server.Internal.Server do end @spec detach(Server.t) :: - :ok + repo_return 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) :: diff --git a/lib/server/model/motherboard.ex b/lib/server/model/motherboard.ex index 37de3666..b266ab06 100644 --- a/lib/server/model/motherboard.ex +++ b/lib/server/model/motherboard.ex @@ -329,7 +329,7 @@ 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, eager?) def by_motherboard(query, mobo = %Component{type: :mobo}, eager?), diff --git a/lib/server/public/index.ex b/lib/server/public/index.ex index 55cbb2ea..8c68b573 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 } @@ -280,6 +285,7 @@ defmodule Helix.Server.Public.Index do filesystem_index = FileIndex.index(server.server_id) tunnel_index = NetworkIndex.index(server.server_id) process_index = ProcessIndex.index(server.server_id, entity_id) + hardware_index = HardwareIndex.index(server) main_storage_id = server.server_id @@ -291,6 +297,7 @@ defmodule Helix.Server.Public.Index do logs: log_index, main_storage: main_storage_id, storages: filesystem_index, + hardware: hardware_index, processes: process_index, tunnels: tunnel_index } @@ -311,6 +318,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..f7f49874 --- /dev/null +++ b/lib/server/public/index/hardware.ex @@ -0,0 +1,31 @@ +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 + } + + @spec index(Server.t) :: + index + def index(server = %Server{}) do + %{ + motherboard: MotherboardIndex.index(server) + } + end + + @spec render_index(index) :: + rendered_index + 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..87e22eeb --- /dev/null +++ b/lib/server/public/index/motherboard.ex @@ -0,0 +1,97 @@ +defmodule Helix.Server.Public.Index.Motherboard do + + 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 :: term + @type rendered_index :: term + + def index(%Server{motherboard_id: nil}) do + %{ + motherboard_id: nil, + slots: %{}, + 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 + + def render_index(index = %{motherboard_id: nil}), + do: index + + 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 + + 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) + } + + {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 -> + + {slot_id, %{type: to_string(comp_type), component_id: nil}} + end) + |> Enum.into(%{}) + |> Map.merge(acc) + end) + + Map.merge(used_slots, free_slots) + end + + 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/query/motherboard.ex b/lib/server/query/motherboard.ex index 410048a7..e716bd89 100644 --- a/lib/server/query/motherboard.ex +++ b/lib/server/query/motherboard.ex @@ -32,4 +32,9 @@ defmodule Helix.Server.Query.Motherboard do 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/test/server/public/index/motherboard_test.exs b/test/server/public/index/motherboard_test.exs new file mode 100644 index 00000000..89033f06 --- /dev/null +++ b/test/server/public/index/motherboard_test.exs @@ -0,0 +1,121 @@ +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) + assert Enum.empty?(index.slots) + 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 From 5d4faa06dfb6fa60fd29be94f4fb37e6d75e4f3c Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Sat, 16 Dec 2017 01:20:51 -0200 Subject: [PATCH 07/12] Add MotherboardUpdated and MotherboardUpdateFailed events --- lib/event/dispatcher.ex | 2 + lib/henforcer/henforcer.ex | 2 +- lib/server/action/flow/server.ex | 28 +++++- lib/server/action/motherboard/update.ex | 8 -- lib/server/event/motherboard.ex | 90 +++++++++++++++++++ lib/server/public/index.ex | 21 +++-- lib/server/public/index/hardware.ex | 18 +++- test/server/action/server_test.exs | 10 +-- test/server/event/motherboard_test.exs | 34 +++++++ .../server/topics/motherboard_test.exs | 7 +- test/support/event/setup/server.ex | 15 ++++ 11 files changed, 208 insertions(+), 27 deletions(-) create mode 100644 lib/server/event/motherboard.ex create mode 100644 test/server/event/motherboard_test.exs create mode 100644 test/support/event/setup/server.ex 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/server/action/flow/server.ex b/lib/server/action/flow/server.ex index 5eac690a..61351c12 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -11,6 +11,10 @@ defmodule Helix.Server.Action.Flow.Server do 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 + @spec setup(Server.type, Entity.t, Component.mobo, Event.relay) :: {:ok, Server.t} @doc """ @@ -63,18 +67,28 @@ defmodule Helix.Server.Action.Flow.Server do {: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 - def detach_mobo(server = %Server{}, motherboard = %Motherboard{}, _relay) do + 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 @@ -83,4 +97,16 @@ defmodule Helix.Server.Action.Flow.Server do do: {:ok, server} defp update_server_mobo(server, mobo_id), do: ServerAction.attach(server, mobo_id) + + defp emit_motherboard_updated(server, relay) do + server + |> MotherboardUpdatedEvent.new() + |> Event.emit(from: relay) + end + + 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/update.ex b/lib/server/action/motherboard/update.ex index 9af9d3e3..851dd825 100644 --- a/lib/server/action/motherboard/update.ex +++ b/lib/server/action/motherboard/update.ex @@ -6,7 +6,6 @@ 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 @@ -63,13 +62,6 @@ defmodule Helix.Server.Action.Motherboard.Update do end 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) - ncs = mobo_data.network_connections entity_ncs diff --git a/lib/server/event/motherboard.ex b/lib/server/event/motherboard.ex new file mode 100644 index 00000000..56f415cb --- /dev/null +++ b/lib/server/event/motherboard.ex @@ -0,0 +1,90 @@ +defmodule Helix.Server.Event.Motherboard do + + import Helix.Event + + event Updated do + + alias Helix.Server.Model.Server + alias Helix.Server.Public.Index.Hardware, as: HardwareIndex + + event_struct [:server, :index_cache] + + 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 + + def generate_payload(event, %{assigns: %{meta: %{access_type: :local}}}) do + data = HardwareIndex.render_index(event.index_cache) + + {:ok, data} + end + + def generate_payload(event, %{assigns: %{meta: %{access_type: :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 + + 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 + + def generate_payload(event, %{assigns: %{meta: %{access_type: :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/public/index.ex b/lib/server/public/index.ex index 8c68b573..a22ee0fd 100644 --- a/lib/server/public/index.ex +++ b/lib/server/public/index.ex @@ -230,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) @@ -242,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 @@ -261,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) :: @@ -285,7 +292,6 @@ defmodule Helix.Server.Public.Index do filesystem_index = FileIndex.index(server.server_id) tunnel_index = NetworkIndex.index(server.server_id) process_index = ProcessIndex.index(server.server_id, entity_id) - hardware_index = HardwareIndex.index(server) main_storage_id = server.server_id @@ -297,7 +303,6 @@ defmodule Helix.Server.Public.Index do logs: log_index, main_storage: main_storage_id, storages: filesystem_index, - hardware: hardware_index, processes: process_index, tunnels: tunnel_index } diff --git a/lib/server/public/index/hardware.ex b/lib/server/public/index/hardware.ex index f7f49874..156df3f8 100644 --- a/lib/server/public/index/hardware.ex +++ b/lib/server/public/index/hardware.ex @@ -13,16 +13,30 @@ defmodule Helix.Server.Public.Index.Hardware do motherboard: MotherboardIndex.rendered_index | nil } - @spec index(Server.t) :: + @typep access_type :: :local | :remote + + @spec index(Server.t, access_type) :: index - def index(server = %Server{}) do + 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) diff --git a/test/server/action/server_test.exs b/test/server/action/server_test.exs index cc02ab10..40cf2f7b 100644 --- a/test/server/action/server_test.exs +++ b/test/server/action/server_test.exs @@ -42,8 +42,9 @@ defmodule Helix.Server.Action.ServerTest do {server1, _} = ServerSetup.server() {server2, _} = ServerSetup.server() - assert {:error, cs} = ServerAction.attach(server1, server2.motherboard_id) - refute cs.valid? + assert {:error, reason} = + ServerAction.attach(server1, server2.motherboard_id) + assert reason == :internal CacheHelper.sync_test() end @@ -53,9 +54,8 @@ defmodule Helix.Server.Action.ServerTest do {mobo, _} = ComponentSetup.component(type: :mobo) - assert {:error, cs} = ServerAction.attach(server, mobo.component_id) - - refute cs.valid? + assert {:error, reason} = ServerAction.attach(server, mobo.component_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..7ce7222b --- /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_type: :local) + @socket_remote ChannelSetup.mock_server_socket(access_type: :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/websocket/channel/server/topics/motherboard_test.exs b/test/server/websocket/channel/server/topics/motherboard_test.exs index 8e9515d3..e97d672a 100644 --- a/test/server/websocket/channel/server/topics/motherboard_test.exs +++ b/test/server/websocket/channel/server/topics/motherboard_test.exs @@ -4,6 +4,7 @@ defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do 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 @@ -83,7 +84,8 @@ defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do # Empty response. It's async! assert Enum.empty?(response.data) - # TODO: Test received the events + # Client received the MotherboardUpdatedEvent + wait_events [:motherboard_updated] # But the underlying server components were modified!!! motherboard = MotherboardQuery.fetch(server.motherboard_id) @@ -136,7 +138,8 @@ defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do # Empty response. It's async! assert Enum.empty?(response.data) - # TODO: Test received the events + # Client received the MotherboardUpdatedEvent + wait_events [:motherboard_updated] new_server = ServerQuery.fetch(server.server_id) 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 From 4c0844bad3a9a181ae2ee07ad765039fdf614b2f Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Sat, 16 Dec 2017 01:23:02 -0200 Subject: [PATCH 08/12] Rename 'access_type' to 'access' --- lib/process/public/view/process.ex | 4 ++-- lib/server/event/motherboard.ex | 6 +++--- lib/server/public/index/hardware.ex | 4 ++-- lib/server/websocket/channel/server.ex | 2 +- lib/server/websocket/channel/server/join.ex | 10 +++++----- lib/server/websocket/requests/bootstrap.ex | 4 ++-- lib/server/websocket/requests/motherboard_update.ex | 4 ++-- .../websocket/requests/cracker/bruteforce.ex | 2 +- lib/software/websocket/requests/file/download.ex | 2 +- lib/software/websocket/requests/pftp/file/add.ex | 2 +- .../websocket/requests/pftp/file/download.ex | 2 +- lib/software/websocket/requests/pftp/file/remove.ex | 2 +- .../websocket/requests/pftp/server/disable.ex | 2 +- .../websocket/requests/pftp/server/enable.ex | 2 +- test/server/event/motherboard_test.exs | 4 ++-- test/server/websocket/channel/server/join_test.exs | 4 ++-- .../websocket/requests/motherboard_update_test.exs | 12 ++++++------ test/support/channel/setup.ex | 10 +++++----- test/support/process/helper/view.ex | 4 ++-- 19 files changed, 41 insertions(+), 41 deletions(-) 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/server/event/motherboard.ex b/lib/server/event/motherboard.ex index 56f415cb..b10c11c7 100644 --- a/lib/server/event/motherboard.ex +++ b/lib/server/event/motherboard.ex @@ -27,13 +27,13 @@ defmodule Helix.Server.Event.Motherboard do @event :motherboard_updated - def generate_payload(event, %{assigns: %{meta: %{access_type: :local}}}) do + 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_type: :remote}}}) do + def generate_payload(event, %{assigns: %{meta: %{access: :remote}}}) do data = event.index_cache |> HardwareIndex.render_index() @@ -75,7 +75,7 @@ defmodule Helix.Server.Event.Motherboard do @event :motherboard_update_failed - def generate_payload(event, %{assigns: %{meta: %{access_type: :local}}}) do + def generate_payload(event, %{assigns: %{meta: %{access: :local}}}) do data = %{reason: event.reason} {:ok, data} diff --git a/lib/server/public/index/hardware.ex b/lib/server/public/index/hardware.ex index 156df3f8..b80b16df 100644 --- a/lib/server/public/index/hardware.ex +++ b/lib/server/public/index/hardware.ex @@ -13,9 +13,9 @@ defmodule Helix.Server.Public.Index.Hardware do motherboard: MotherboardIndex.rendered_index | nil } - @typep access_type :: :local | :remote + @typep access :: :local | :remote - @spec index(Server.t, access_type) :: + @spec index(Server.t, access) :: index def index(server = %Server{}, :local) do %{ diff --git a/lib/server/websocket/channel/server.ex b/lib/server/websocket/channel/server.ex index a60ccfa8..2a410411 100644 --- a/lib/server/websocket/channel/server.ex +++ b/lib/server/websocket/channel/server.ex @@ -326,7 +326,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 index dec4fcd5..27334387 100644 --- a/lib/server/websocket/requests/motherboard_update.ex +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -22,7 +22,7 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do def check_detach(request, socket) do with \ - true <- socket.assigns.meta.access_type == :local || :bad_src + true <- socket.assigns.meta.access == :local || :bad_src do update_params(request, %{cmd: :detach}, reply: true) else @@ -36,7 +36,7 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do def check_update(request, socket) do with \ - true <- socket.assigns.meta.access_type == :local || :bad_src, + 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"]) 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/test/server/event/motherboard_test.exs b/test/server/event/motherboard_test.exs index 7ce7222b..75be31fe 100644 --- a/test/server/event/motherboard_test.exs +++ b/test/server/event/motherboard_test.exs @@ -7,8 +7,8 @@ defmodule Helix.Server.Event.MotherboardTest do alias Helix.Test.Channel.Setup, as: ChannelSetup alias Helix.Test.Event.Setup, as: EventSetup - @socket_local ChannelSetup.mock_server_socket(access_type: :local) - @socket_remote ChannelSetup.mock_server_socket(access_type: :remote) + @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 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/requests/motherboard_update_test.exs b/test/server/websocket/requests/motherboard_update_test.exs index 189babab..c35bd650 100644 --- a/test/server/websocket/requests/motherboard_update_test.exs +++ b/test/server/websocket/requests/motherboard_update_test.exs @@ -26,7 +26,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do @internet_id NetworkHelper.internet_id() - @mock_socket ChannelSetup.mock_server_socket(access_type: :local) + @mock_socket ChannelSetup.mock_server_socket(access: :local) describe "MotherboardUpdateRequest.check_params" do test "casts the params to internal Helix format" do @@ -215,7 +215,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do ChannelSetup.mock_server_socket( gateway_id: server.server_id, gateway_entity_id: entity.entity_id, - access_type: :local + access: :local ) request = MotherboardUpdateRequest.new(params) @@ -267,7 +267,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do ChannelSetup.mock_server_socket( gateway_id: server.server_id, gateway_entity_id: entity.entity_id, - access_type: :local + access: :local ) request = MotherboardUpdateRequest.new(params) @@ -287,7 +287,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do ChannelSetup.mock_server_socket( gateway_id: server.server_id, gateway_entity_id: entity.entity_id, - access_type: :local + access: :local ) request = MotherboardUpdateRequest.new(params) @@ -354,7 +354,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do ChannelSetup.mock_server_socket( gateway_id: server.server_id, gateway_entity_id: entity.entity_id, - access_type: :local + access: :local ) request = MotherboardUpdateRequest.new(params) @@ -419,7 +419,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do ChannelSetup.mock_server_socket( gateway_id: server.server_id, gateway_entity_id: entity.entity_id, - access_type: :local + access: :local ) request = MotherboardUpdateRequest.new(params) diff --git a/test/support/channel/setup.ex b/test/support/channel/setup.ex index 89806171..bb141bba 100644 --- a/test/support/channel/setup.ex +++ b/test/support/channel/setup.ex @@ -252,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` @@ -273,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 @@ -288,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/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), From 134e684d5a4b0db9a3027ebb337b7814392c6297 Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Sat, 16 Dec 2017 04:31:36 -0200 Subject: [PATCH 09/12] Add typespecs --- lib/entity/henforcer/entity.ex | 32 ++++++++++ lib/server/action/flow/server.ex | 20 +++++++ lib/server/action/motherboard.ex | 4 ++ lib/server/action/motherboard/update.ex | 33 ++++++++++ lib/server/event/motherboard.ex | 7 +++ lib/server/henforcer/component.ex | 80 ++++++++++++++++++++++++- lib/server/model/motherboard.ex | 3 +- lib/server/public/index/motherboard.ex | 55 ++++++++++++++--- lib/server/public/server.ex | 21 ++++++- 9 files changed, 242 insertions(+), 13 deletions(-) diff --git a/lib/entity/henforcer/entity.ex b/lib/entity/henforcer/entity.ex index 074f9f3c..795fbe8c 100644 --- a/lib/entity/henforcer/entity.ex +++ b/lib/entity/henforcer/entity.ex @@ -2,6 +2,7 @@ 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 @@ -68,6 +69,17 @@ defmodule Helix.Entity.Henforcer.Entity do |> 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 def owns_component?(entity_id = %Entity.ID{}, component, owned) do henforce entity_exists?(entity_id) do owns_component?(relay.entity, component, owned) @@ -100,6 +112,26 @@ defmodule Helix.Entity.Henforcer.Entity do ) 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 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) diff --git a/lib/server/action/flow/server.ex b/lib/server/action/flow/server.ex index 61351c12..a5c1ef80 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -5,6 +5,7 @@ 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 @@ -15,6 +16,9 @@ defmodule Helix.Server.Action.Flow.Server do 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 """ @@ -50,6 +54,14 @@ 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 def update_mobo( server = %Server{}, motherboard, @@ -77,6 +89,8 @@ defmodule Helix.Server.Action.Flow.Server do end end + @spec detach_mobo(Server.t, Motherboard.t, Event.relay) :: + detach_mobo_result def detach_mobo(server = %Server{}, motherboard = %Motherboard{}, relay) do flowing do with \ @@ -93,17 +107,23 @@ defmodule Helix.Server.Action.Flow.Server do 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) diff --git a/lib/server/action/motherboard.ex b/lib/server/action/motherboard.ex index f9b76704..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 diff --git a/lib/server/action/motherboard/update.ex b/lib/server/action/motherboard/update.ex index 851dd825..8143ebdd 100644 --- a/lib/server/action/motherboard/update.ex +++ b/lib/server/action/motherboard/update.ex @@ -6,6 +6,7 @@ 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 @@ -14,6 +15,24 @@ defmodule Helix.Server.Action.Motherboard.Update do @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 def detach(motherboard = %Motherboard{}) do MotherboardInternal.unlink_all(motherboard) @@ -32,6 +51,8 @@ defmodule Helix.Server.Action.Motherboard.Update do :ok end + @spec update(Motherboard.t | nil, motherboard_data, [Network.Connection.t]) :: + {:ok, Motherboard.t, [term]} def update(nil, mobo_data, entity_ncs) do {:ok, new_mobo} = MotherboardInternal.setup(mobo_data.mobo, mobo_data.components) @@ -61,6 +82,8 @@ defmodule Helix.Server.Action.Motherboard.Update do {:ok, new_mobo, []} end + @spec update_network_connections(motherboard_data, [Network.Connection.t]) :: + term defp update_network_connections(mobo_data, entity_ncs) do ncs = mobo_data.network_connections @@ -100,12 +123,22 @@ defmodule Helix.Server.Action.Motherboard.Update do |> 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 diff --git a/lib/server/event/motherboard.ex b/lib/server/event/motherboard.ex index b10c11c7..8e8714be 100644 --- a/lib/server/event/motherboard.ex +++ b/lib/server/event/motherboard.ex @@ -9,6 +9,13 @@ defmodule Helix.Server.Event.Motherboard do 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, diff --git a/lib/server/henforcer/component.ex b/lib/server/henforcer/component.ex index 361f682b..6f074c39 100644 --- a/lib/server/henforcer/component.ex +++ b/lib/server/henforcer/component.ex @@ -2,7 +2,9 @@ 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.Henforcer.Server, as: ServerHenforcer alias Helix.Server.Model.Component @@ -13,6 +15,14 @@ defmodule Helix.Server.Henforcer.Component do @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 def component_exists?(component_id = %Component.ID{}) do with component = %Component{} <- ComponentQuery.fetch(component_id) do reply_ok(%{component: component}) @@ -22,6 +32,15 @@ defmodule Helix.Server.Henforcer.Component do 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} + | component_exists_error + + @spec is_motherboard?(Component.t) :: + {true, is_motherboard_relay} + | is_motherboard_error def is_motherboard?(component = %Component{type: :mobo}), do: reply_ok(%{component: component}) def is_motherboard?(%Component{}), @@ -32,6 +51,13 @@ defmodule Helix.Server.Henforcer.Component do 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 def can_link?( mobo = %Component{type: :mobo}, component = %Component{}, @@ -45,6 +71,13 @@ defmodule Helix.Server.Henforcer.Component do end end + @type has_initial_components_relay :: %{} + @type has_initial_components_error :: + {false, {:motherboard, :missing_initial_components}, %{}} + + @spec has_initial_components?([term]) :: + {true, has_initial_components_relay} + | has_initial_components_error def has_initial_components?(components) do if Motherboard.has_required_initial_components?(components) do reply_ok() @@ -53,6 +86,13 @@ defmodule Helix.Server.Henforcer.Component do 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 def has_public_nip?(network_connections) do if Enum.find(network_connections, &(&1.network_id == @internet_id)) do reply_ok() @@ -61,6 +101,37 @@ defmodule Helix.Server.Henforcer.Component do end end + # TODO Merge + @typep mobo_nc :: + %{ + nic_id: Component.id, + network_id: Network.id, + ip: Network.ip, + network_connection: Network.Connection.t + } + + @type can_update_mobo_relay :: + %{ + entity: Entity.t, + mobo: Component.mobo, + components: [term], + owned_components: [Component.t], + network_connections: [mobo_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, [term], [term]) :: + {true, can_update_mobo_relay} + | can_update_mobo_error def can_update_mobo?(entity_id, mobo_id, components, network_connections) do reduce_components = fn mobo -> init = {{true, %{}}, nil} @@ -161,13 +232,19 @@ defmodule Helix.Server.Henforcer.Component do end end + @type can_detach_mobo_relay :: %{server: Server.t, motherboard: Motherboard.t} + @type can_detach_mobo_error :: component_exists_error + + @spec can_detach_mobo?(Server.idt) :: + {true, can_detach_mobo_relay} + | can_detach_mobo_error 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. + # TODO: Mainframe verification, cost analysis (for cooldown) etc. #358 def can_detach_mobo?(server = %Server{}) do with \ {true, _} <- component_exists?(server.motherboard_id) @@ -175,5 +252,6 @@ defmodule Helix.Server.Henforcer.Component do motherboard = MotherboardQuery.fetch(server.motherboard_id) reply_ok(%{motherboard: motherboard}) end + |> wrap_relay(%{server: server}) end end diff --git a/lib/server/model/motherboard.ex b/lib/server/model/motherboard.ex index b266ab06..36a33458 100644 --- a/lib/server/model/motherboard.ex +++ b/lib/server/model/motherboard.ex @@ -45,7 +45,8 @@ defmodule Helix.Server.Model.Motherboard do @type initial_components :: [{Component.pluggable, Component.Mobo.slot_id}] @type required_components :: [Constant.t] - @type slot :: {Component.Mobo.slot_id, Component.t} + @type slot_id :: Component.Mobo.slot_id + @type slot :: {slot_id, Component.t} @type free_slots :: %{Component.type => [Component.Mobo.slot_id]} @type error :: diff --git a/lib/server/public/index/motherboard.ex b/lib/server/public/index/motherboard.ex index 87e22eeb..1fe8c263 100644 --- a/lib/server/public/index/motherboard.ex +++ b/lib/server/public/index/motherboard.ex @@ -1,18 +1,46 @@ 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 :: term - @type rendered_index :: term + @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, - slots: %{}, - network_connections: %{} + network_connections: [] } end @@ -39,8 +67,15 @@ defmodule Helix.Server.Public.Index.Motherboard do } end - def render_index(index = %{motherboard_id: nil}), - do: index + @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 %{ @@ -50,6 +85,8 @@ defmodule Helix.Server.Public.Index.Motherboard do } end + @spec render_slots(Motherboard.t) :: + rendered_slots defp render_slots(motherboard = %Motherboard{}) do used_slots = motherboard.slots @@ -60,7 +97,7 @@ defmodule Helix.Server.Public.Index.Motherboard do component_id: to_string(component.component_id) } - {slot_id, comp_data} + {to_string(slot_id), comp_data} end) |> Enum.into(%{}) @@ -71,7 +108,7 @@ defmodule Helix.Server.Public.Index.Motherboard do free_slots |> Enum.map(fn slot_id -> - {slot_id, %{type: to_string(comp_type), component_id: nil}} + {to_string(slot_id), %{type: to_string(comp_type), component_id: nil}} end) |> Enum.into(%{}) |> Map.merge(acc) @@ -80,6 +117,8 @@ defmodule Helix.Server.Public.Index.Motherboard do 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 -> diff --git a/lib/server/public/server.ex b/lib/server/public/server.ex index ea4c6e49..95d23220 100644 --- a/lib/server/public/server.ex +++ b/lib/server/public/server.ex @@ -6,8 +6,11 @@ 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.Motherboard 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 @@ -40,6 +43,17 @@ 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 def update_mobo(server, {mobo, components, ncs}, entity_ncs, relay) do motherboard = if server.motherboard_id do @@ -58,9 +72,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 + @spec detach_mobo(Server.t, Motherboard.t, Event.relay) :: + ServerFlow.detach_mobo_result + def detach_mobo(server, motherboard, relay), + do: ServerFlow.detach_mobo(server, motherboard, relay) defdelegate set_hostname(server, hostname, relay), to: ServerFlow From 4012e828a3487b78050391a5724db6bbcf65fabc Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Sat, 23 Dec 2017 08:46:13 -0200 Subject: [PATCH 10/12] Add docs --- lib/entity/henforcer/entity.ex | 10 ++ lib/network/model/network/connection.ex | 4 +- lib/server/action/flow/server.ex | 19 +++- lib/server/action/motherboard/update.ex | 23 ++++ lib/server/event/motherboard.ex | 22 ++++ lib/server/henforcer/component.ex | 100 +++++++++++++----- lib/server/henforcer/server.ex | 5 +- lib/server/model/motherboard.ex | 4 +- lib/server/public/server.ex | 24 ++++- lib/server/websocket/channel/server.ex | 30 ++++++ .../websocket/requests/motherboard_update.ex | 7 +- test/server/henforcer/component_test.exs | 36 ++++++- test/server/public/index/motherboard_test.exs | 25 +++-- 13 files changed, 252 insertions(+), 57 deletions(-) diff --git a/lib/entity/henforcer/entity.ex b/lib/entity/henforcer/entity.ex index 795fbe8c..887567bd 100644 --- a/lib/entity/henforcer/entity.ex +++ b/lib/entity/henforcer/entity.ex @@ -80,6 +80,11 @@ defmodule Helix.Entity.Henforcer.Entity do @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) @@ -132,6 +137,11 @@ defmodule Helix.Entity.Henforcer.Entity do @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) diff --git a/lib/network/model/network/connection.ex b/lib/network/model/network/connection.ex index ce1974a3..4d60a253 100644 --- a/lib/network/model/network/connection.ex +++ b/lib/network/model/network/connection.ex @@ -65,7 +65,7 @@ defmodule Helix.Network.Model.Network.Connection do |> validate_required(@required_fields) end - @spec update_nic(t, Component.nic | nil) :: + @spec update_nic(t, Component.nic) :: changeset def update_nic(nc = %__MODULE__{}, nic = %Component{type: :nic}) do nc @@ -74,6 +74,8 @@ 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 diff --git a/lib/server/action/flow/server.ex b/lib/server/action/flow/server.ex index a5c1ef80..7a77fd6c 100644 --- a/lib/server/action/flow/server.ex +++ b/lib/server/action/flow/server.ex @@ -59,9 +59,18 @@ defmodule Helix.Server.Action.Flow.Server do Motherboard.t, MotherboardAction.motherboard_data, entity_ncs :: [Network.Connection.t], - relay :: Event.relay - ) :: + 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, @@ -91,6 +100,12 @@ defmodule Helix.Server.Action.Flow.Server do @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 \ diff --git a/lib/server/action/motherboard/update.ex b/lib/server/action/motherboard/update.ex index 8143ebdd..b9fadf8a 100644 --- a/lib/server/action/motherboard/update.ex +++ b/lib/server/action/motherboard/update.ex @@ -33,9 +33,13 @@ defmodule Helix.Server.Action.Motherboard.Update do @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() @@ -53,10 +57,20 @@ defmodule Helix.Server.Action.Motherboard.Update do @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 @@ -75,6 +89,7 @@ defmodule Helix.Server.Action.Motherboard.Update do MotherboardInternal.setup(mobo_data.mobo, mobo_data.components) end + # Network configuration is asynchronous hespawn fn -> update_network_connections(mobo_data, entity_ncs) end @@ -84,6 +99,14 @@ defmodule Helix.Server.Action.Motherboard.Update do @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 diff --git a/lib/server/event/motherboard.ex b/lib/server/event/motherboard.ex index 8e8714be..4f859e2a 100644 --- a/lib/server/event/motherboard.ex +++ b/lib/server/event/motherboard.ex @@ -3,6 +3,15 @@ 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 @@ -34,6 +43,11 @@ defmodule Helix.Server.Event.Motherboard 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) @@ -55,6 +69,11 @@ defmodule Helix.Server.Event.Motherboard do 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 @@ -82,6 +101,9 @@ defmodule Helix.Server.Event.Motherboard 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} diff --git a/lib/server/henforcer/component.ex b/lib/server/henforcer/component.ex index 6f074c39..e55029ee 100644 --- a/lib/server/henforcer/component.ex +++ b/lib/server/henforcer/component.ex @@ -6,6 +6,7 @@ defmodule Helix.Server.Henforcer.Component do 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 @@ -23,6 +24,9 @@ defmodule Helix.Server.Henforcer.Component do @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}) @@ -36,11 +40,20 @@ defmodule Helix.Server.Henforcer.Component do @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.t) :: + @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{}), @@ -58,6 +71,10 @@ defmodule Helix.Server.Henforcer.Component do @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{}, @@ -75,9 +92,13 @@ defmodule Helix.Server.Henforcer.Component do @type has_initial_components_error :: {false, {:motherboard, :missing_initial_components}, %{}} - @spec has_initial_components?([term]) :: + @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() @@ -93,6 +114,10 @@ defmodule Helix.Server.Henforcer.Component do @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() @@ -101,22 +126,13 @@ defmodule Helix.Server.Henforcer.Component do end end - # TODO Merge - @typep mobo_nc :: - %{ - nic_id: Component.id, - network_id: Network.id, - ip: Network.ip, - network_connection: Network.Connection.t - } - @type can_update_mobo_relay :: %{ entity: Entity.t, mobo: Component.mobo, - components: [term], + components: [MotherboardAction.update_component], owned_components: [Component.t], - network_connections: [mobo_nc], + network_connections: [MotherboardAction.update_nc], entity_network_connections: [Network.Connection.t] } @@ -129,9 +145,20 @@ defmodule Helix.Server.Henforcer.Component do | EntityHenforcer.owns_component_error | EntityHenforcer.owns_nip_error - @spec can_update_mobo?(Entity.id, Component.id, [term], [term]) :: + @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} @@ -232,26 +259,45 @@ defmodule Helix.Server.Henforcer.Component do end end - @type can_detach_mobo_relay :: %{server: Server.t, motherboard: Motherboard.t} - @type can_detach_mobo_error :: component_exists_error + @type can_detach_mobo_relay :: + %{ + server: Server.t, + entity: Entity.t, + mobo: Component.mobo, + motherboard: Motherboard.t, + owned_components: [Component.t] + } - @spec can_detach_mobo?(Server.idt) :: + @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 - def can_detach_mobo?(server_id = %Server.ID{}) do - henforce ServerHenforcer.server_exists?(server_id) do - can_detach_mobo?(relay.server) - end - end - + @doc """ + Ensures `entity_id` can detach mobo from `server_id`. + """ # TODO: Mainframe verification, cost analysis (for cooldown) etc. #358 - def can_detach_mobo?(server = %Server{}) do + def can_detach_mobo?(entity_id, server_id) do with \ - {true, _} <- component_exists?(server.motherboard_id) + {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(%{motherboard: motherboard}) + + reply_ok(relay([r0, r1])) + |> wrap_relay(%{motherboard: motherboard}) end - |> wrap_relay(%{server: server}) end end diff --git a/lib/server/henforcer/server.ex b/lib/server/henforcer/server.ex index 27c974f3..f9e2e7a6 100644 --- a/lib/server/henforcer/server.ex +++ b/lib/server/henforcer/server.ex @@ -13,15 +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. """ - # TODO: REVIEW: Why does it accept `Server.t` as input? - 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/model/motherboard.ex b/lib/server/model/motherboard.ex index 36a33458..b80e285d 100644 --- a/lib/server/model/motherboard.ex +++ b/lib/server/model/motherboard.ex @@ -42,12 +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_id :: Component.Mobo.slot_id @type slot :: {slot_id, Component.t} - @type free_slots :: %{Component.type => [Component.Mobo.slot_id]} + @type free_slots :: %{Component.type => [slot_id]} @type error :: :wrong_slot_type diff --git a/lib/server/public/server.ex b/lib/server/public/server.ex index 95d23220..5bc23f05 100644 --- a/lib/server/public/server.ex +++ b/lib/server/public/server.ex @@ -54,6 +54,18 @@ defmodule Helix.Server.Public.Server do 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 @@ -72,11 +84,15 @@ defmodule Helix.Server.Public.Server do ServerFlow.update_mobo(server, motherboard, mobo_data, entity_ncs, relay) end - @spec detach_mobo(Server.t, Motherboard.t, Event.relay) :: - ServerFlow.detach_mobo_result - def detach_mobo(server, motherboard, relay), - do: ServerFlow.detach_mobo(server, motherboard, relay) + @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/websocket/channel/server.ex b/lib/server/websocket/channel/server.ex index 2a410411..243c0939 100644 --- a/lib/server/websocket/channel/server.ex +++ b/lib/server/websocket/channel/server.ex @@ -127,6 +127,36 @@ 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: diff --git a/lib/server/websocket/requests/motherboard_update.ex b/lib/server/websocket/requests/motherboard_update.ex index 27334387..f0812f21 100644 --- a/lib/server/websocket/requests/motherboard_update.ex +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -65,14 +65,15 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do 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, relay} <- ComponentHenforcer.can_detach_mobo?(gateway_id) + {true, r0} <- ComponentHenforcer.can_detach_mobo?(entity_id, gateway_id) do meta = %{ - server: relay.server, - motherboard: relay.motherboard + server: r0.server, + motherboard: r0.motherboard } update_meta(request, meta, reply: true) diff --git a/test/server/henforcer/component_test.exs b/test/server/henforcer/component_test.exs index 404bc667..fb586004 100644 --- a/test/server/henforcer/component_test.exs +++ b/test/server/henforcer/component_test.exs @@ -8,16 +8,18 @@ defmodule Helix.Server.Henforcer.ComponentTest do 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?/n" do + describe "can_update_mobo?/4" do test "accepts when everything is OK" do {server, %{entity: entity}} = ServerSetup.server() @@ -310,4 +312,36 @@ defmodule Helix.Server.Henforcer.ComponentTest do 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/public/index/motherboard_test.exs b/test/server/public/index/motherboard_test.exs index 89033f06..987bc1d9 100644 --- a/test/server/public/index/motherboard_test.exs +++ b/test/server/public/index/motherboard_test.exs @@ -28,7 +28,6 @@ defmodule Helix.Server.Public.Index.MotherboardTest do refute index.motherboard_id assert Enum.empty?(index.network_connections) - assert Enum.empty?(index.slots) end test "indexes motherboard" do @@ -99,23 +98,23 @@ defmodule Helix.Server.Public.Index.MotherboardTest do # 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" + 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 + 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 From 18d6f895dc97e00e5b3d2e0aa6bec0cbbe91019e Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Sat, 23 Dec 2017 09:22:09 -0200 Subject: [PATCH 11/12] Support 1-step switch of motherboards --- lib/server/internal/server.ex | 20 ++++-- lib/server/model/server.ex | 65 +++++++++---------- lib/server/public/server.ex | 1 - test/server/action/server_test.exs | 22 +++---- .../server/topics/motherboard_test.exs | 61 ++++++++++++++++- 5 files changed, 113 insertions(+), 56 deletions(-) diff --git a/lib/server/internal/server.ex b/lib/server/internal/server.ex index e63dffc4..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 @@ -60,6 +66,11 @@ defmodule Helix.Server.Internal.Server do @spec detach(Server.t) :: 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 result = server @@ -82,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/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/server.ex b/lib/server/public/server.ex index 5bc23f05..f023acee 100644 --- a/lib/server/public/server.ex +++ b/lib/server/public/server.ex @@ -7,7 +7,6 @@ defmodule Helix.Server.Public.Server do alias Helix.Network.Query.Network, as: NetworkQuery alias Helix.Network.Query.Tunnel, as: TunnelQuery alias Helix.Server.Model.Component - alias Helix.Server.Model.Motherboard alias Helix.Server.Model.Server alias Helix.Server.Action.Flow.Server, as: ServerFlow alias Helix.Server.Action.Motherboard, as: MotherboardAction diff --git a/test/server/action/server_test.exs b/test/server/action/server_test.exs index 40cf2f7b..53477be8 100644 --- a/test/server/action/server_test.exs +++ b/test/server/action/server_test.exs @@ -38,23 +38,23 @@ 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, reason} = - ServerAction.attach(server1, server2.motherboard_id) - assert reason == :internal + {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) + test "fails when given motherboard is already attached" do + {server1, _} = ServerSetup.server() + {server2, _} = ServerSetup.server() - assert {:error, reason} = ServerAction.attach(server, mobo.component_id) + assert {:error, reason} = + ServerAction.attach(server1, server2.motherboard_id) assert reason == :internal CacheHelper.sync_test() diff --git a/test/server/websocket/channel/server/topics/motherboard_test.exs b/test/server/websocket/channel/server/topics/motherboard_test.exs index e97d672a..560cdd22 100644 --- a/test/server/websocket/channel/server/topics/motherboard_test.exs +++ b/test/server/websocket/channel/server/topics/motherboard_test.exs @@ -23,7 +23,7 @@ defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do @internet_id NetworkHelper.internet_id() describe "motherboard.update" do - test "updates the components" do + test "updates the components (same motherboard)" do {socket, %{gateway: server, gateway_entity: entity}} = ChannelSetup.join_server(own_server: true) @@ -117,6 +117,65 @@ defmodule Helix.Server.Websocket.Channel.Server.Topics.MotherboardTest do 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) From 308a54fc54a619ff1674c560246b2b58260ee943 Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Sat, 23 Dec 2017 09:28:25 -0200 Subject: [PATCH 12/12] Remove deprecated cache columns --- .../20170713020139_initial_migration.exs | 24 +++++++++---------- ...112419_remove_deprecated_cache_columns.exs | 16 +++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 priv/repo/cache/migrations/20171223112419_remove_deprecated_cache_columns.exs 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