From f069b82f57a31b645858015b0f4604680a5bdb58 Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Sat, 23 Dec 2017 08:46:13 -0200 Subject: [PATCH] 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