From 4ddc8afbb33a2d53f39d5636215235e058b90372 Mon Sep 17 00:00:00 2001 From: Nelson Kopliku Date: Mon, 29 Apr 2024 15:03:38 +0200 Subject: [PATCH] Track software update discoveries (#2540) * Add a DiscoveryResult schema in order to keep track of software updates discovery results * Store software updates discovery results on successful discovery * Add tracking of both successful and failing software updates discoveries * Integrate clearup of tracked software updates discoveries * Properly track discoveries failing due to authentication issues --- ...oftware_updates_discovery_event_handler.ex | 13 +- lib/trento/software_updates/discovery.ex | 135 +++++-- .../discovery/discovery_result.ex | 22 ++ ...eate_software_updates_discovery_result.exs | 15 + test/support/factory.ex | 18 + ...e_updates_discovery_event_handler_test.exs | 144 ++++--- .../software_updates/discovery_test.exs | 369 +++++++++++++++--- 7 files changed, 572 insertions(+), 144 deletions(-) create mode 100644 lib/trento/software_updates/discovery/discovery_result.ex create mode 100644 priv/repo/migrations/20240422105737_create_software_updates_discovery_result.exs diff --git a/lib/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler.ex b/lib/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler.ex index 1a9f643c1a..61d623e90a 100644 --- a/lib/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler.ex +++ b/lib/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler.ex @@ -8,7 +8,10 @@ defmodule Trento.Infrastructure.Commanded.EventHandlers.SoftwareUpdatesDiscovery application: Trento.Commanded, name: "software_updates_discovery_event_handler" - alias Trento.Hosts.Events.SoftwareUpdatesDiscoveryRequested + alias Trento.Hosts.Events.{ + SoftwareUpdatesDiscoveryCleared, + SoftwareUpdatesDiscoveryRequested + } alias Trento.SoftwareUpdates.Discovery @@ -23,4 +26,12 @@ defmodule Trento.Infrastructure.Commanded.EventHandlers.SoftwareUpdatesDiscovery :ok end + + def handle( + %SoftwareUpdatesDiscoveryCleared{ + host_id: host_id + }, + _ + ), + do: Discovery.clear_tracked_discovery_result(host_id) end diff --git a/lib/trento/software_updates/discovery.ex b/lib/trento/software_updates/discovery.ex index d1148653e6..cd017abe1b 100644 --- a/lib/trento/software_updates/discovery.ex +++ b/lib/trento/software_updates/discovery.ex @@ -3,6 +3,12 @@ defmodule Trento.SoftwareUpdates.Discovery do Software updates integration service """ + import Ecto.Query + + alias Trento.Repo + + alias Ecto.Multi + alias Trento.Hosts alias Trento.Hosts.Commands.{ @@ -11,6 +17,7 @@ defmodule Trento.SoftwareUpdates.Discovery do } alias Trento.Hosts.Projections.HostReadModel + alias Trento.SoftwareUpdates.Discovery.DiscoveryResult require Trento.SoftwareUpdates.Enums.SoftwareUpdatesHealth, as: SoftwareUpdatesHealth require Trento.SoftwareUpdates.Enums.AdvisoryType, as: AdvisoryType @@ -49,12 +56,12 @@ defmodule Trento.SoftwareUpdates.Discovery do {:error, error} -> {:error, host_id, error} - {:ok, _, _, _} = success -> + {:ok, _, _, _, _} = success -> success end end) |> Enum.split_with(fn - {:ok, _, _, _} -> true + {:ok, _, _, _, _} -> true _ -> false end)} end @@ -74,8 +81,14 @@ defmodule Trento.SoftwareUpdates.Discovery do :ok end + @spec clear_tracked_discovery_result(String.t()) :: :ok + def clear_tracked_discovery_result(host_id) do + Repo.delete_all(from d in DiscoveryResult, where: d.host_id == ^host_id) + :ok + end + @spec discover_host_software_updates(String.t(), String.t()) :: - {:ok, String.t(), String.t(), any()} | {:error, any()} + {:ok, String.t(), String.t(), any(), any()} | {:error, any()} def discover_host_software_updates(host_id, nil) do Logger.info("Host #{host_id} does not have an fqdn. Skipping software updates discovery") {:error, :host_without_fqdn} @@ -84,57 +97,111 @@ defmodule Trento.SoftwareUpdates.Discovery do def discover_host_software_updates(host_id, fully_qualified_domain_name) do with {:ok, system_id} <- get_system_id(fully_qualified_domain_name), {:ok, relevant_patches} <- get_relevant_patches(system_id), - :ok <- - host_id - |> build_discovery_completion_command(relevant_patches) - |> commanded().dispatch() do - {:ok, host_id, system_id, relevant_patches} + {:ok, upgradable_packages} <- get_upgradable_packages(system_id), + {:ok, _} <- + finalize_successful_discovery( + host_id, + system_id, + relevant_patches, + upgradable_packages + ) do + {:ok, host_id, system_id, relevant_patches, upgradable_packages} else {:error, :settings_not_configured} -> {:error, :settings_not_configured} - {:error, discovery_error} = error -> + {:error, _} = error -> Logger.error( "An error occurred during software updates discovery for host #{host_id}: #{inspect(error)}" ) - commanded().dispatch( - CompleteSoftwareUpdatesDiscovery.new!(%{ - host_id: host_id, - health: SoftwareUpdatesHealth.unknown() - }) - ) + finalize_failed_discovery(host_id, error) - {:error, discovery_error} + error end end defp discover_host_software_updates(_, _, {:error, :settings_not_configured} = error), do: error - defp discover_host_software_updates(host_id, _, {:error, error}) do - commanded().dispatch( - CompleteSoftwareUpdatesDiscovery.new!(%{ - host_id: host_id, - health: SoftwareUpdatesHealth.unknown() - }) - ) - - {:error, error} + defp discover_host_software_updates(host_id, _, {:error, _} = error) do + finalize_failed_discovery(host_id, error) + error end defp discover_host_software_updates(host_id, fully_qualified_domain_name, _), do: discover_host_software_updates(host_id, fully_qualified_domain_name) - defp build_discovery_completion_command(host_id, relevant_patches), - do: - CompleteSoftwareUpdatesDiscovery.new!(%{ - host_id: host_id, - health: - relevant_patches - |> track_relevant_patches - |> compute_software_updates_discovery_health - }) + defp finalize_failed_discovery(host_id, {:error, reason}) do + %DiscoveryResult{} + |> DiscoveryResult.changeset(%{ + host_id: host_id, + system_id: nil, + relevant_patches: nil, + upgradable_packages: nil, + failure_reason: Atom.to_string(reason) + }) + |> finalize_discovery(host_id, SoftwareUpdatesHealth.unknown()) + end + + defp finalize_successful_discovery(host_id, system_id, relevant_patches, upgradable_packages) do + %DiscoveryResult{} + |> DiscoveryResult.changeset(%{ + host_id: host_id, + system_id: "#{system_id}", + relevant_patches: relevant_patches, + upgradable_packages: upgradable_packages + }) + |> finalize_discovery( + host_id, + relevant_patches + |> track_relevant_patches + |> compute_software_updates_discovery_health + ) + end + + defp finalize_discovery(discovery_result, host_id, discovered_health) do + transaction_result = + Multi.new() + |> Multi.insert(:insert, discovery_result, + conflict_target: :host_id, + on_conflict: :replace_all + ) + |> Multi.run(:command_dispatching, fn _, _ -> + dispatch_completion_command(host_id, discovered_health) + end) + |> Repo.transaction() + + case transaction_result do + {:ok, _} = success -> + success + + {:error, :command_dispatching, dispatching_error, _} -> + {:error, dispatching_error} + + {:error, _} = error -> + Logger.error( + "Error while finalizing software updates discovery for host #{host_id}, error: #{inspect(error)}" + ) + + error + end + end + + defp dispatch_completion_command(host_id, discovered_health) do + case %{ + host_id: host_id, + health: discovered_health + } + |> CompleteSoftwareUpdatesDiscovery.new!() + |> commanded().dispatch() do + :ok -> + {:ok, :dispatched} + + {:error, _} = error -> + error + end + end defp track_relevant_patches(relevant_patches), do: diff --git a/lib/trento/software_updates/discovery/discovery_result.ex b/lib/trento/software_updates/discovery/discovery_result.ex new file mode 100644 index 0000000000..d3816d9323 --- /dev/null +++ b/lib/trento/software_updates/discovery/discovery_result.ex @@ -0,0 +1,22 @@ +defmodule Trento.SoftwareUpdates.Discovery.DiscoveryResult do + @moduledoc """ + This is the schema used to store the results of the software updates discovery process. + """ + + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:host_id, :binary_id, autogenerate: false} + schema "software_updates_discovery_result" do + field :system_id, :string + field :relevant_patches, Trento.Support.Ecto.Payload + field :upgradable_packages, Trento.Support.Ecto.Payload + field :failure_reason, :string + + timestamps(type: :utc_datetime_usec) + end + + def changeset(discovery_result, attrs) do + cast(discovery_result, attrs, __MODULE__.__schema__(:fields)) + end +end diff --git a/priv/repo/migrations/20240422105737_create_software_updates_discovery_result.exs b/priv/repo/migrations/20240422105737_create_software_updates_discovery_result.exs new file mode 100644 index 0000000000..9d6116041c --- /dev/null +++ b/priv/repo/migrations/20240422105737_create_software_updates_discovery_result.exs @@ -0,0 +1,15 @@ +defmodule Trento.Repo.Migrations.CreateSoftwareUpdatesDiscoveryResult do + use Ecto.Migration + + def change do + create table(:software_updates_discovery_result, primary_key: false) do + add :host_id, :uuid, primary_key: true + add :system_id, :string + add :relevant_patches, :map + add :upgradable_packages, :map + add :failure_reason, :string + + timestamps(type: :utc_datetime_usec) + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index e41d021bb0..b74b990a16 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -40,6 +40,7 @@ defmodule Trento.Factory do HostTombstoned, SaptuneStatusUpdated, SlesSubscriptionsUpdated, + SoftwareUpdatesDiscoveryCleared, SoftwareUpdatesDiscoveryRequested, SoftwareUpdatesHealthChanged } @@ -114,6 +115,7 @@ defmodule Trento.Factory do DiscoveryEvent } + alias Trento.SoftwareUpdates.Discovery.DiscoveryResult alias Trento.SoftwareUpdates.Settings alias Trento.Settings.{ @@ -805,6 +807,12 @@ defmodule Trento.Factory do } end + def software_updates_discovery_cleared_event_factory do + SoftwareUpdatesDiscoveryCleared.new!(%{ + host_id: Faker.UUID.v4() + }) + end + def host_health_changed_event_factory do HostHealthChanged.new!(%{ host_id: Faker.UUID.v4(), @@ -884,4 +892,14 @@ defmodule Trento.Factory do to_package_id: "#{RandomElixir.random_between(0, 1000)}" } end + + def software_updates_discovery_result_factory do + %DiscoveryResult{ + host_id: Faker.UUID.v4(), + system_id: Faker.UUID.v4(), + relevant_patches: build_list(2, :relevant_patch), + upgradable_packages: build_list(2, :upgradable_package), + failure_reason: Faker.Lorem.word() + } + end end diff --git a/test/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler_test.exs b/test/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler_test.exs index 00464a785e..1174c4e662 100644 --- a/test/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler_test.exs +++ b/test/trento/infrastructure/commanded/event_handlers/software_updates_discovery_event_handler_test.exs @@ -1,12 +1,14 @@ defmodule Trento.Infrastructure.Commanded.EventHandlers.SoftwareUpdatesDiscoveryEventHandlerTest do - use ExUnit.Case + use Trento.DataCase import Mox import Trento.Factory + alias Trento.SoftwareUpdates.Discovery.DiscoveryResult alias Trento.SoftwareUpdates.Discovery.Mock, as: SoftwareUpdatesDiscoveryMock alias Trento.Hosts.Commands.CompleteSoftwareUpdatesDiscovery + alias Trento.Hosts.Events.SoftwareUpdatesDiscoveryRequested alias Trento.Infrastructure.Commanded.EventHandlers.SoftwareUpdatesDiscoveryEventHandler @@ -15,63 +17,91 @@ defmodule Trento.Infrastructure.Commanded.EventHandlers.SoftwareUpdatesDiscovery setup [:set_mox_from_context, :verify_on_exit!] - test "should discover software updates when a SoftwareUpdatesDiscoveryRequested is emitted" do - %SoftwareUpdatesDiscoveryRequested{ - host_id: host_id, - fully_qualified_domain_name: fully_qualified_domain_name - } = event = build(:software_updates_discovery_requested_event) - - system_id = Faker.UUID.v4() - - expect( - SoftwareUpdatesDiscoveryMock, - :get_system_id, - fn ^fully_qualified_domain_name -> {:ok, system_id} end - ) - - expect( - SoftwareUpdatesDiscoveryMock, - :get_relevant_patches, - fn ^system_id -> {:ok, []} end - ) - - expect( - Trento.Commanded.Mock, - :dispatch, - fn %CompleteSoftwareUpdatesDiscovery{host_id: ^host_id} -> :ok end - ) - - assert :ok = SoftwareUpdatesDiscoveryEventHandler.handle(event, %{}) + describe "Discovering software updates" do + test "should discover software updates when a SoftwareUpdatesDiscoveryRequested is emitted" do + %SoftwareUpdatesDiscoveryRequested{ + host_id: host_id, + fully_qualified_domain_name: fully_qualified_domain_name + } = event = build(:software_updates_discovery_requested_event) + + system_id = Faker.UUID.v4() + + expect( + SoftwareUpdatesDiscoveryMock, + :get_system_id, + fn ^fully_qualified_domain_name -> {:ok, system_id} end + ) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_relevant_patches, + fn ^system_id -> {:ok, []} end + ) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + fn ^system_id -> {:ok, []} end + ) + + expect( + Trento.Commanded.Mock, + :dispatch, + fn %CompleteSoftwareUpdatesDiscovery{host_id: ^host_id} -> :ok end + ) + + assert :ok = SoftwareUpdatesDiscoveryEventHandler.handle(event, %{}) + end + + test "should pass through failures" do + %SoftwareUpdatesDiscoveryRequested{ + fully_qualified_domain_name: fully_qualified_domain_name + } = event = build(:software_updates_discovery_requested_event) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_system_id, + fn ^fully_qualified_domain_name -> {:error, :some_error} end + ) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_relevant_patches, + 0, + fn _ -> :ok end + ) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + 0, + fn _ -> :ok end + ) + + expect( + Trento.Commanded.Mock, + :dispatch, + fn %CompleteSoftwareUpdatesDiscovery{ + health: SoftwareUpdatesHealth.unknown() + } -> + :ok + end + ) + + assert :ok = SoftwareUpdatesDiscoveryEventHandler.handle(event, %{}) + end end - test "should pass through failures" do - %SoftwareUpdatesDiscoveryRequested{ - fully_qualified_domain_name: fully_qualified_domain_name - } = event = build(:software_updates_discovery_requested_event) - - expect( - SoftwareUpdatesDiscoveryMock, - :get_system_id, - fn ^fully_qualified_domain_name -> {:error, :some_error} end - ) - - expect( - SoftwareUpdatesDiscoveryMock, - :get_relevant_patches, - 0, - fn _ -> :ok end - ) - - expect( - Trento.Commanded.Mock, - :dispatch, - fn %CompleteSoftwareUpdatesDiscovery{ - health: SoftwareUpdatesHealth.unknown() - } -> - :ok - end - ) - - assert :ok = SoftwareUpdatesDiscoveryEventHandler.handle(event, %{}) + describe "Clearing up software updates discoveries" do + test "should clear previously tracked software updates discoveries when a SoftwareUpdatesDiscoveryCleared is emitted" do + [%{host_id: host_id} | _] = insert_list(6, :software_updates_discovery_result) + + assert :ok == + :software_updates_discovery_cleared_event + |> build(host_id: host_id) + |> SoftwareUpdatesDiscoveryEventHandler.handle(%{}) + + assert nil == Trento.Repo.get(DiscoveryResult, host_id) + end end end diff --git a/test/trento/software_updates/discovery_test.exs b/test/trento/software_updates/discovery_test.exs index 8cd4ceccde..f86ce5f948 100644 --- a/test/trento/software_updates/discovery_test.exs +++ b/test/trento/software_updates/discovery_test.exs @@ -12,6 +12,7 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do } alias Trento.SoftwareUpdates.Discovery + alias Trento.SoftwareUpdates.Discovery.DiscoveryResult alias Trento.SoftwareUpdates.Discovery.Mock, as: SoftwareUpdatesDiscoveryMock require Trento.SoftwareUpdates.Enums.AdvisoryType, as: AdvisoryType @@ -20,57 +21,95 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do setup :verify_on_exit! describe "Discovering software updates for a specific host" do - test "should return an error when a null FQDN is provided" do - host_id = Faker.UUID.v4() - - assert {:error, :host_without_fqdn} = - Discovery.discover_host_software_updates(host_id, nil) - end - - test "should handle failure when getting host's system id" do - host_id = Faker.UUID.v4() - fully_qualified_domain_name = Faker.Internet.domain_name() - - discovery_error = :some_error_while_getting_system_id - - fail_on_getting_system_id(host_id, fully_qualified_domain_name, discovery_error) - - {:error, ^discovery_error} = - Discovery.discover_host_software_updates(host_id, fully_qualified_domain_name) - end - - test "should handle failure when getting relevant patches" do - host_id = Faker.UUID.v4() - fully_qualified_domain_name = Faker.Internet.domain_name() - system_id = 100 - discovery_error = :some_error_while_getting_relevant_patches - - fail_on_getting_relevant_patches( - host_id, - fully_qualified_domain_name, - system_id, - discovery_error - ) - - {:error, ^discovery_error} = - Discovery.discover_host_software_updates(host_id, fully_qualified_domain_name) - end + test "should handle failures and track causing reasons" do + scenarios = [ + %{ + name: "should return an error when a null FQDN is provided", + host_id: Faker.UUID.v4(), + fully_qualified_domain_name: nil, + expected_error: :host_without_fqdn, + expect_tracked_discovery: false + }, + %{ + name: "should handle failure when getting host's system id", + host_id: Faker.UUID.v4(), + fully_qualified_domain_name: Faker.Internet.domain_name(), + failure_setup: fn scenario -> + fail_on_getting_system_id( + scenario.host_id, + scenario.fully_qualified_domain_name, + scenario.expected_error + ) + end, + expected_error: :some_error_while_getting_system_id + }, + %{ + name: "should handle failure when getting relevant patches", + host_id: Faker.UUID.v4(), + fully_qualified_domain_name: Faker.Internet.domain_name(), + failure_setup: fn scenario -> + fail_on_getting_relevant_patches( + scenario.host_id, + scenario.fully_qualified_domain_name, + 100, + scenario.expected_error + ) + end, + expected_error: :some_error_while_getting_relevant_patches + }, + %{ + name: "should handle failure when getting upgradable packages", + host_id: Faker.UUID.v4(), + fully_qualified_domain_name: Faker.Internet.domain_name(), + failure_setup: fn scenario -> + fail_on_getting_upgradable_packages( + scenario.host_id, + scenario.fully_qualified_domain_name, + 100, + scenario.expected_error + ) + end, + expected_error: :some_error_while_getting_relevant_patches + }, + %{ + name: "should handle failure when dispatching discovery completion command", + host_id: Faker.UUID.v4(), + fully_qualified_domain_name: Faker.Internet.domain_name(), + failure_setup: fn scenario -> + fail_on_dispatching_completion_command( + scenario.host_id, + scenario.fully_qualified_domain_name, + 100, + scenario.expected_error + ) + end, + expected_error: :error_while_dispatching_completion_command + } + ] - test "should handle failure when dispatching discovery completion command" do - host_id = Faker.UUID.v4() - fully_qualified_domain_name = Faker.Internet.domain_name() - system_id = 100 - dispatching_error = :error_while_dispatching_completion_command + for %{ + host_id: host_id, + fully_qualified_domain_name: fully_qualified_domain_name, + expected_error: expected_error + } = scenario <- scenarios do + failure_setup = Map.get(scenario, :failure_setup, fn _ -> nil end) + failure_setup.(scenario) + + {:error, ^expected_error} = + Discovery.discover_host_software_updates(host_id, fully_qualified_domain_name) - fail_on_dispatching_completion_command( - host_id, - fully_qualified_domain_name, - system_id, - dispatching_error - ) + if Map.get(scenario, :expect_tracked_discovery, true) do + stored_failure = Atom.to_string(expected_error) - {:error, ^dispatching_error} = - Discovery.discover_host_software_updates(host_id, fully_qualified_domain_name) + assert %DiscoveryResult{ + host_id: ^host_id, + system_id: nil, + relevant_patches: nil, + upgradable_packages: nil, + failure_reason: ^stored_failure + } = Trento.Repo.get(DiscoveryResult, host_id) + end + end end test "should handle failure when SUMA settings are not configured" do @@ -105,6 +144,7 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do %{advisory_type: AdvisoryType.bugfix()}, %{advisory_type: AdvisoryType.enhancement()} ], + discovered_upgradable_packages: build_list(3, :upgradable_package), expected_health: SoftwareUpdatesHealth.critical() }, %{ @@ -112,31 +152,37 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do %{advisory_type: AdvisoryType.bugfix()}, %{advisory_type: AdvisoryType.enhancement()} ], + discovered_upgradable_packages: build_list(3, :upgradable_package), expected_health: SoftwareUpdatesHealth.warning() }, %{ discovered_relevant_patches: [ %{advisory_type: AdvisoryType.enhancement()} ], + discovered_upgradable_packages: [], expected_health: SoftwareUpdatesHealth.warning() }, %{ discovered_relevant_patches: [ %{advisory_type: AdvisoryType.bugfix()} ], + discovered_upgradable_packages: build_list(3, :upgradable_package), expected_health: SoftwareUpdatesHealth.warning() }, %{ discovered_relevant_patches: [], + discovered_upgradable_packages: build_list(3, :upgradable_package), expected_health: SoftwareUpdatesHealth.passing() } ] for %{ discovered_relevant_patches: discovered_relevant_patches, + discovered_upgradable_packages: discovered_upgradable_packages, expected_health: expected_health } <- scenarios do host_id = Faker.UUID.v4() + fully_qualified_domain_name = Faker.Internet.domain_name() system_id = 100 @@ -152,6 +198,12 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do fn ^system_id -> {:ok, discovered_relevant_patches} end ) + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + fn ^system_id -> {:ok, discovered_upgradable_packages} end + ) + expect( Trento.Commanded.Mock, :dispatch, @@ -163,8 +215,23 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do end ) - {:ok, ^host_id, ^system_id, ^discovered_relevant_patches} = + {:ok, ^host_id, ^system_id, ^discovered_relevant_patches, ^discovered_upgradable_packages} = Discovery.discover_host_software_updates(host_id, fully_qualified_domain_name) + + stored_discovered_relevant_patches = to_stored_representation(discovered_relevant_patches) + + stored_discovered_upgradable_packages = + to_stored_representation(discovered_upgradable_packages) + + stored_system_id = "#{system_id}" + + assert %DiscoveryResult{ + host_id: ^host_id, + system_id: ^stored_system_id, + relevant_patches: ^stored_discovered_relevant_patches, + upgradable_packages: ^stored_discovered_upgradable_packages, + failure_reason: nil + } = Trento.Repo.get(DiscoveryResult, host_id) end end end @@ -174,6 +241,11 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do expect(SoftwareUpdatesDiscoveryMock, :setup, fn -> :ok end) assert {:ok, {[], []}} = Discovery.discover_software_updates() + + assert 0 == + DiscoveryResult + |> Trento.Repo.all() + |> length() end test "should handle authentication error" do @@ -205,6 +277,11 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do Enum.each([host_id1, host_id2], fn host_id -> assert {:error, host_id, :auth_error} in errored_discoveries end) + + assert 2 == + DiscoveryResult + |> Trento.Repo.all() + |> length() end test "should handle SUMA settings not configured error" do @@ -239,6 +316,11 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do Enum.each([host_id1, host_id2], fn host_id -> assert {:error, host_id, :host_without_fqdn} in errored_discoveries end) + + assert 0 == + DiscoveryResult + |> Trento.Repo.all() + |> length() end test "should handle errors when getting a system id" do @@ -253,6 +335,13 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do {:ok, {[], errored_discoveries}} = Discovery.discover_software_updates() assert {:error, host_id, discovery_error} in errored_discoveries + + assert 1 == + DiscoveryResult + |> Trento.Repo.all() + |> length() + + assert_failure_result_tracked(host_id, discovery_error) end test "should handle errors when getting relevant patches" do @@ -261,7 +350,7 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do %{id: host_id, fully_qualified_domain_name: fully_qualified_domain_name} = insert(:host) system_id = 100 - discovery_error = {:error, :some_error_while_getting_relevant_patches} + discovery_error = :some_error_while_getting_relevant_patches fail_on_getting_relevant_patches( host_id, @@ -273,6 +362,40 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do {:ok, {[], errored_discoveries}} = Discovery.discover_software_updates() assert {:error, host_id, discovery_error} in errored_discoveries + + assert 1 == + DiscoveryResult + |> Trento.Repo.all() + |> length() + + assert_failure_result_tracked(host_id, discovery_error) + end + + test "should handle errors when getting upgradable packages" do + expect(SoftwareUpdatesDiscoveryMock, :setup, fn -> :ok end) + + %{id: host_id, fully_qualified_domain_name: fully_qualified_domain_name} = insert(:host) + + system_id = 100 + discovery_error = :some_error_while_getting_upgradable_packages + + fail_on_getting_upgradable_packages( + host_id, + fully_qualified_domain_name, + system_id, + discovery_error + ) + + {:ok, {[], errored_discoveries}} = Discovery.discover_software_updates() + + assert {:error, host_id, discovery_error} in errored_discoveries + + assert 1 == + DiscoveryResult + |> Trento.Repo.all() + |> length() + + assert_failure_result_tracked(host_id, discovery_error) end test "should handle errors when dispatching discovery completion command" do @@ -294,6 +417,13 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do {:ok, {[], errored_discoveries}} = Discovery.discover_software_updates() assert {:error, host_id, dispatching_error} in errored_discoveries + + assert 1 == + DiscoveryResult + |> Trento.Repo.all() + |> length() + + assert_failure_result_tracked(host_id, dispatching_error) end test "should complete discovery" do @@ -368,6 +498,15 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do end ) + upgradable_packages = build_list(3, :upgradable_package) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + 2, + fn _ -> {:ok, upgradable_packages} end + ) + expect( Trento.Commanded.Mock, :dispatch, @@ -386,13 +525,32 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do assert length(errored_discoveries) == 2 assert [ - {:ok, host_id1, system_id1, discovered_relevant_patches}, - {:ok, host_id3, system_id3, discovered_relevant_patches} + {:ok, host_id1, system_id1, discovered_relevant_patches, upgradable_packages}, + {:ok, host_id3, system_id3, discovered_relevant_patches, upgradable_packages} ] == successful_discoveries assert {:error, host_id2, :some_error} in errored_discoveries assert {:error, host_id4, :host_without_fqdn} in errored_discoveries + + assert 3 == + DiscoveryResult + |> Trento.Repo.all() + |> length() + + assert_failure_result_tracked(host_id2, :some_error) + + assert nil == Trento.Repo.get(DiscoveryResult, host_id4) + + assert %DiscoveryResult{ + host_id: ^host_id1, + failure_reason: nil + } = Trento.Repo.get(DiscoveryResult, host_id1) + + assert %DiscoveryResult{ + host_id: ^host_id3, + failure_reason: nil + } = Trento.Repo.get(DiscoveryResult, host_id3) end end @@ -426,6 +584,39 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do assert :ok = Discovery.clear_software_updates_discoveries() end + + test "should clear a previously tracked software updates discovery result" do + %{host_id: host_id} = insert(:software_updates_discovery_result) + insert_list(4, :software_updates_discovery_result) + + assert %DiscoveryResult{host_id: ^host_id} = Trento.Repo.get(DiscoveryResult, host_id) + + assert :ok == Discovery.clear_tracked_discovery_result(host_id) + + assert nil == Trento.Repo.get(DiscoveryResult, host_id) + + assert 4 == + DiscoveryResult + |> Trento.Repo.all() + |> length() + end + + test "should ignore not tracked software updates discovery results" do + insert_list(4, :software_updates_discovery_result) + + host_id = Faker.UUID.v4() + + assert nil == Trento.Repo.get(DiscoveryResult, host_id) + + assert :ok == Discovery.clear_tracked_discovery_result(host_id) + + assert nil == Trento.Repo.get(DiscoveryResult, host_id) + + assert 4 == + DiscoveryResult + |> Trento.Repo.all() + |> length() + end end defp fail_on_getting_system_id(host_id, fully_qualified_domain_name, discovery_error) do @@ -442,6 +633,13 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do fn _ -> :ok end ) + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + 0, + fn _ -> :ok end + ) + expect( Trento.Commanded.Mock, :dispatch, @@ -472,6 +670,49 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do fn ^system_id -> {:error, discovery_error} end ) + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + 0, + fn _ -> :ok end + ) + + expect( + Trento.Commanded.Mock, + :dispatch, + fn %CompleteSoftwareUpdatesDiscovery{ + host_id: ^host_id, + health: SoftwareUpdatesHealth.unknown() + } -> + :ok + end + ) + end + + defp fail_on_getting_upgradable_packages( + host_id, + fully_qualified_domain_name, + system_id, + discovery_error + ) do + expect( + SoftwareUpdatesDiscoveryMock, + :get_system_id, + fn ^fully_qualified_domain_name -> {:ok, system_id} end + ) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_relevant_patches, + fn ^system_id -> {:ok, [%{advisory_type: AdvisoryType.security_advisory()}]} end + ) + + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + fn _ -> {:error, discovery_error} end + ) + expect( Trento.Commanded.Mock, :dispatch, @@ -502,6 +743,12 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do fn ^system_id -> {:ok, [%{advisory_type: AdvisoryType.security_advisory()}]} end ) + expect( + SoftwareUpdatesDiscoveryMock, + :get_upgradable_packages, + fn _ -> {:ok, build_list(3, :upgradable_package)} end + ) + expect( Trento.Commanded.Mock, :dispatch, @@ -524,4 +771,22 @@ defmodule Trento.SoftwareUpdates.DiscoveryTest do end ) end + + defp assert_failure_result_tracked(host_id, failure_reason) do + stored_failure = Atom.to_string(failure_reason) + + assert %DiscoveryResult{ + host_id: ^host_id, + system_id: nil, + relevant_patches: nil, + upgradable_packages: nil, + failure_reason: ^stored_failure + } = Trento.Repo.get(DiscoveryResult, host_id) + end + + defp to_stored_representation(map_with_atoms) do + map_with_atoms + |> Jason.encode!() + |> Jason.decode!() + end end