diff --git a/lib/trento/application/integration/discovery/payloads/sap_system_discovery_payload.ex b/lib/trento/application/integration/discovery/payloads/sap_system_discovery_payload.ex new file mode 100644 index 0000000000..346b404d78 --- /dev/null +++ b/lib/trento/application/integration/discovery/payloads/sap_system_discovery_payload.ex @@ -0,0 +1,351 @@ +defmodule Trento.Integration.Discovery.SapSystemDiscoveryPayload do + @moduledoc """ + SAP system discovery integration event payload + """ + + alias Trento.Integration.Discovery.SapSystemDiscoveryPayload.{ + Database, + Instance, + Profile + } + + @required_fields [:Id, :SID, :Type, :Profile, :Databases, :Instances] + + use Trento.Type + + deftype do + field :Id, :string + field :SID, :string + field :Type, :integer + field :DBAddress, :string + + embeds_one :Profile, Profile + embeds_many :Databases, Database + embeds_many :Instances, Instance + end + + def changeset(sap_system, attrs) do + modified_attrs = + attrs + |> databases_to_list + + sap_system + |> cast(modified_attrs, fields()) + |> cast_embed(:Profile, with: {Profile, :changeset, [parse_system_type(attrs)]}) + |> cast_embed(:Databases) + |> cast_embed(:Instances) + |> validate_required_fields(@required_fields) + end + + defp parse_system_type(%{"Type" => system_type}), do: system_type + defp parse_system_type(_), do: nil + + defp databases_to_list(%{"Databases" => nil} = attrs), + do: %{attrs | "Databases" => []} + + defp databases_to_list(attrs), do: attrs + + defmodule Profile do + @moduledoc """ + Profile field payload + """ + + @application_type 2 + @application_required_fields [:"dbs/hdb/dbname"] + + # Cannot use Trento.Type here, Jason.Encoder is breaking the schema creation + use Ecto.Schema + import Ecto.Changeset + + @type t() :: %__MODULE__{} + + @primary_key false + + embedded_schema do + field :"dbs/hdb/dbname", :string + end + + def changeset(profile, attrs, type) do + profile + |> cast(attrs, __MODULE__.__schema__(:fields)) + |> maybe_validate_required_fields(type) + end + + defp maybe_validate_required_fields(changeset, @application_type), + do: + changeset + |> validate_required(@application_required_fields) + + defp maybe_validate_required_fields(changeset, _), do: changeset + end + + defmodule Database do + @moduledoc """ + Databases field payload + """ + + @required_fields [:Database] + + use Trento.Type + + deftype do + field :Host, :string + field :User, :string + field :Group, :string + field :Active, :string + field :UserId, :string + field :GroupId, :string + field :SqlPort, :string + field :Database, :string + field :Container, :string + end + + def changeset(database, attrs) do + database + |> cast(attrs, fields()) + |> validate_required_fields(@required_fields) + end + end + + defmodule Instance do + @moduledoc """ + Instances field payload + """ + + alias Trento.Integration.Discovery.SapSystemDiscoveryPayload.{ + SapControl, + SystemReplication + } + + @required_fields [:Host, :Name, :Type, :SAPControl, :SystemReplication] + + use Trento.Type + + deftype do + field :Host, :string + field :Name, :string + field :Type, :integer + + embeds_one :SAPControl, SapControl + embeds_one :SystemReplication, SystemReplication + end + + def changeset(instance, attrs) do + instance + |> cast(attrs, fields()) + |> cast_embed(:SAPControl) + |> cast_embed(:SystemReplication) + |> validate_required_fields(@required_fields) + end + end + + defmodule SapControl do + @moduledoc """ + SAP control field payload + """ + + alias Trento.Integration.Discovery.SapSystemDiscoveryPayload.{ + SapControlInstance, + SapControlProcess, + SapControlProperty + } + + @required_fields [:Properties, :Instances, :Processes] + + use Trento.Type + + deftype do + embeds_many :Properties, SapControlProperty + embeds_many :Instances, SapControlInstance + embeds_many :Processes, SapControlProcess + end + + def changeset(sap_control, attrs) do + hostname = find_property("SAPLOCALHOST", attrs) + + instance_number = + case find_property("SAPSYSTEM", attrs) do + instance_number when is_binary(instance_number) -> String.to_integer(instance_number) + _ -> nil + end + + sap_control + |> cast(attrs, fields()) + |> cast_embed(:Properties) + |> cast_embed(:Instances, + with: {SapControlInstance, :changeset, [hostname, instance_number]} + ) + |> cast_embed(:Processes) + |> validate_required_fields(@required_fields) + end + + defp find_property(property, %{"Properties" => properties}) do + properties + |> Enum.find_value(fn + %{"property" => ^property, "value" => value} -> value + _ -> nil + end) + end + + defp find_property(_, _), do: nil + end + + defmodule SapControlProperty do + @moduledoc """ + SAP control property field payload + """ + + @required_fields [:value, :property] + + use Trento.Type + + deftype do + field :value, :string + field :property, :string + field :propertytype, :string + end + + def changeset(property, attrs) do + property + |> cast(attrs, fields()) + |> validate_required_fields(@required_fields) + end + end + + defmodule SapControlInstance do + @moduledoc """ + SAP control instances field payload + """ + + @required_fields [ + :features, + :hostname, + :httpPort, + :httpsPort, + :dispstatus, + :instanceNr, + :startPriority + ] + + use Trento.Type + + deftype do + # current_instance is a custom field to make data extraction easier + field :current_instance, :boolean, default: false + field :features, :string + field :hostname, :string + field :httpPort, :integer + field :httpsPort, :integer + + field :dispstatus, Ecto.Enum, + values: [ + :"SAPControl-GREEN", + :"SAPControl-YELLOW", + :"SAPControl-RED", + :"SAPControl-GRAY" + ] + + field :instanceNr, :integer + field :startPriority, :string + end + + def changeset(instance, attrs, hostname, instance_number) do + enriched_attrs = enrich_current_instance(attrs, hostname, instance_number) + + instance + |> cast(enriched_attrs, fields()) + |> validate_required_fields(@required_fields) + end + + defp enrich_current_instance( + %{"hostname" => current_hostname, "instanceNr" => current_instance_number} = attrs, + hostname, + instance_number + ) + when hostname == current_hostname and instance_number == current_instance_number, + do: Map.put(attrs, "current_instance", true) + + defp enrich_current_instance(attrs, _, _), do: attrs + end + + defmodule SapControlProcess do + @moduledoc """ + SAP control process field payload + """ + + @required_fields [ + :pid, + :name, + :starttime, + :dispstatus, + :textstatus, + :description, + :elapsedtime + ] + + use Trento.Type + + deftype do + field :pid, :integer + field :name, :string + field :starttime, :string + + field :dispstatus, Ecto.Enum, + values: [ + :"SAPControl-GREEN", + :"SAPControl-YELLOW", + :"SAPControl-RED", + :"SAPControl-GRAY" + ] + + field :description, :string + field :elapsedtime, :string + end + + def changeset(process, attrs) do + process + |> cast(attrs, fields()) + |> validate_required_fields(@required_fields) + end + end + + defmodule SystemReplication do + @moduledoc """ + SystemReplication process field payload + """ + + @required_fields [:local_site_id] + + # Cannot use Trento.Type here, Jason.Encoder is breaking the schema creation + use Ecto.Schema + import Ecto.Changeset + + @type t() :: %__MODULE__{} + + @primary_key false + + embedded_schema do + field :local_site_id, :string + field :overall_replication_status, :string + field :"site/1/REPLICATION_MODE", :string + field :"site/2/REPLICATION_MODE", :string + end + + def changeset(system_replication, attrs) do + local_site_id = parse_local_site_id(attrs) + + system_replication + |> cast(attrs, __MODULE__.__schema__(:fields)) + |> validate_required(@required_fields) + |> validate_replication_mode(local_site_id) + end + + defp parse_local_site_id(%{"local_site_id" => local_site_id}), do: local_site_id + defp parse_local_site_id(_), do: 1 + + defp validate_replication_mode(changeset, local_site_id) do + changeset + |> validate_required([:"site/#{local_site_id}/REPLICATION_MODE"]) + end + end +end diff --git a/lib/trento/application/integration/discovery/policies/sap_system_policy.ex b/lib/trento/application/integration/discovery/policies/sap_system_policy.ex index f79d35f146..8ac98d9554 100644 --- a/lib/trento/application/integration/discovery/policies/sap_system_policy.ex +++ b/lib/trento/application/integration/discovery/policies/sap_system_policy.ex @@ -8,6 +8,15 @@ defmodule Trento.Integration.Discovery.SapSystemPolicy do RegisterDatabaseInstance } + alias Trento.Integration.Discovery.SapSystemDiscoveryPayload + + alias SapSystemDiscoveryPayload.{ + Instance, + Profile, + SapControl, + SystemReplication + } + @uuid_namespace Application.compile_env!(:trento, :uuid_namespace) @database_type 1 @@ -20,235 +29,130 @@ defmodule Trento.Integration.Discovery.SapSystemPolicy do "agent_id" => agent_id, "payload" => payload }) do - payload - |> Enum.flat_map(fn sap_system -> parse_sap_system(sap_system, agent_id) end) - |> Enum.reduce_while( - {:ok, []}, - fn - {:ok, command}, {:ok, commands} -> {:cont, {:ok, commands ++ [command]}} - {:error, _} = error, _ -> {:halt, error} - end - ) + case SapSystemDiscoveryPayload.from_list(payload) do + {:ok, sap_systems} -> + sap_systems + |> Enum.flat_map(fn sap_system -> build_commands(sap_system, agent_id) end) + |> Enum.reduce_while( + {:ok, []}, + fn + {:ok, command}, {:ok, commands} -> {:cont, {:ok, commands ++ [command]}} + {:error, _} = error, _ -> {:halt, error} + end + ) + + error -> + error + end end - @spec parse_sap_system(map, String.t()) :: [ - {:ok, RegisterDatabaseInstance.t()} - | {:ok, RegisterApplicationInstance.t()} - | {:error, any} - ] - defp parse_sap_system( - %{ - "Type" => @database_type, - "Id" => id, - "SID" => sid, - "Databases" => databases, - "Instances" => instances + defp build_commands( + %SapSystemDiscoveryPayload{ + Id: id, + SID: sid, + Type: @database_type, + Databases: databases, + Instances: instances }, host_id ) do - Enum.flat_map(databases, fn %{"Database" => tenant} -> + Enum.flat_map(databases, fn %{:Database => tenant} -> Enum.map(instances, fn instance -> - instance_number = parse_instance_number(instance) - instance_hostname = parse_instance_hostname(instance) - RegisterDatabaseInstance.new(%{ sap_system_id: UUID.uuid5(@uuid_namespace, id), sid: sid, tenant: tenant, host_id: host_id, - instance_number: instance_number, - instance_hostname: instance_hostname, - features: parse_features(instance, instance_number, instance_hostname), - http_port: parse_http_port(instance, instance_number, instance_hostname), - https_port: parse_https_port(instance, instance_number, instance_hostname), - start_priority: parse_start_priority(instance, instance_number, instance_hostname), + instance_number: parse_instance_number(instance), + instance_hostname: parse_instance_hostname(instance), + features: parse_features(instance), + http_port: parse_http_port(instance), + https_port: parse_https_port(instance), + start_priority: parse_start_priority(instance), system_replication: parse_system_replication(instance), system_replication_status: parse_system_replication_status(instance), - health: parse_instance_health(instance, instance_number, instance_hostname) + health: parse_dispstatus(instance) }) end) end) end - defp parse_sap_system( - %{ - "Type" => @application_type, - "DBAddress" => db_host, - "SID" => sid, - "Instances" => instances, - "Profile" => %{ - "dbs/hdb/dbname" => tenant + defp build_commands( + %SapSystemDiscoveryPayload{ + SID: sid, + Type: @application_type, + Instances: instances, + DBAddress: db_host, + Profile: %Profile{ + "dbs/hdb/dbname": tenant } }, host_id ) do Enum.map(instances, fn instance -> - instance_number = parse_instance_number(instance) - instance_hostname = parse_instance_hostname(instance) - RegisterApplicationInstance.new(%{ sid: sid, tenant: tenant, db_host: db_host, - instance_number: instance_number, - instance_hostname: instance_hostname, - features: parse_features(instance, instance_number, instance_hostname), - http_port: parse_http_port(instance, instance_number, instance_hostname), - https_port: parse_https_port(instance, instance_number, instance_hostname), - start_priority: parse_start_priority(instance, instance_number, instance_hostname), + instance_number: parse_instance_number(instance), + instance_hostname: parse_instance_hostname(instance), + features: parse_features(instance), + http_port: parse_http_port(instance), + https_port: parse_https_port(instance), + start_priority: parse_start_priority(instance), host_id: host_id, - health: parse_instance_health(instance, instance_number, instance_hostname) + health: parse_dispstatus(instance) }) end) end - defp parse_sap_system( - %{ - "Type" => _ - }, - _ - ), - do: [] - - @spec parse_http_port(map, String.t(), String.t()) :: integer() | nil - defp parse_http_port(%{"SAPControl" => sap_control}, instance_number, instance_hostname) do - case extract_sap_control_instance_data( - sap_control, - instance_number, - instance_hostname, - "httpPort" - ) do - {:ok, instance_http_port} -> - instance_http_port - - _ -> - nil - end - end - - @spec parse_https_port(map, String.t(), String.t()) :: integer() | nil - defp parse_https_port(%{"SAPControl" => sap_control}, instance_number, instance_hostname) do - case extract_sap_control_instance_data( - sap_control, - instance_number, - instance_hostname, - "httpsPort" - ) do - {:ok, instance_https_port} -> - instance_https_port - - _ -> - nil - end - end - - @spec parse_start_priority(map, String.t(), String.t()) :: String.t() | nil - defp parse_start_priority(%{"SAPControl" => sap_control}, instance_number, instance_hostname) do - case extract_sap_control_instance_data( - sap_control, - instance_number, - instance_hostname, - "startPriority" - ) do - {:ok, start_priority} -> - start_priority - - _ -> - nil - end - end - - @spec parse_features(map, String.t(), String.t()) :: String.t() - defp parse_features(%{"SAPControl" => sap_control}, instance_number, instance_hostname) do - case extract_sap_control_instance_data( - sap_control, - instance_number, - instance_hostname, - "features" - ) do - {:ok, features} -> - features + defp parse_instance_number(instance), do: parse_sap_control_property("SAPSYSTEM", instance) - _ -> - "" - end - end + defp parse_instance_hostname(instance), do: parse_sap_control_property("SAPLOCALHOST", instance) - @spec parse_instance_number(map) :: String.t() | nil - defp parse_instance_number(%{ - "SAPControl" => %{"Properties" => properties} + defp parse_sap_control_property(property, %Instance{ + SAPControl: %SapControl{Properties: properties} }) do properties |> Enum.find_value(fn - %{"property" => "SAPSYSTEM", "value" => value} -> value + %{property: ^property, value: value} -> value _ -> nil end) end - @spec parse_instance_hostname(map) :: String.t() | nil - defp parse_instance_hostname(%{ - "SAPControl" => %{"Properties" => properties} - }) do - properties + defp parse_sap_control_instance_value( + %Instance{SAPControl: %SapControl{Instances: instances}}, + key + ) do + instances |> Enum.find_value(fn - %{"property" => "SAPLOCALHOST", "value" => value} -> value + %{current_instance: true} = current_instance -> Map.get(current_instance, key) _ -> nil end) end - @spec parse_instance_health(map, String.t(), String.t()) :: - :passing | :warning | :critical | :unknown - defp parse_instance_health(%{"SAPControl" => sap_control}, instance_number, instance_hostname) do - case extract_sap_control_instance_data( - sap_control, - instance_number, - instance_hostname, - "dispstatus" - ) do - {:ok, dispstatus} -> - parse_dispstatus(dispstatus) + defp parse_features(instance), do: parse_sap_control_instance_value(instance, :features) + defp parse_http_port(instance), do: parse_sap_control_instance_value(instance, :httpPort) + defp parse_https_port(instance), do: parse_sap_control_instance_value(instance, :httpsPort) - _ -> - :unknown - end - end + defp parse_start_priority(instance), + do: parse_sap_control_instance_value(instance, :startPriority) - defp parse_dispstatus("SAPControl-GREEN"), do: :passing - defp parse_dispstatus("SAPControl-YELLOW"), do: :warning - defp parse_dispstatus("SAPControl-RED"), do: :critical - defp parse_dispstatus(_), do: :unknown + defp parse_dispstatus(instance), + do: + instance + |> parse_sap_control_instance_value(:dispstatus) + |> normalize_dispstatus - @spec extract_sap_control_instance_data(map, String.t(), String.t(), String.t()) :: - {:ok, String.t()} | {:error, :key_not_found} - defp extract_sap_control_instance_data( - %{"Instances" => instances}, - instance_number, - instance_hostname, - key - ) do - instances - |> Enum.find(fn - %{"instanceNr" => number, "hostname" => hostname} -> - number - |> Integer.to_string() - |> String.pad_leading(2, "0") == instance_number && hostname == instance_hostname - - _ -> - nil - end) - |> case do - %{^key => value} -> - {:ok, value} + defp normalize_dispstatus(:"SAPControl-GREEN"), do: :passing + defp normalize_dispstatus(:"SAPControl-YELLOW"), do: :warning + defp normalize_dispstatus(:"SAPControl-RED"), do: :critical + defp normalize_dispstatus(_), do: :unknown - _ -> - {:error, :key_not_found} - end - end - - defp parse_system_replication(%{ - "SystemReplication" => %{"local_site_id" => local_site_id} = system_replication + defp parse_system_replication(%Instance{ + SystemReplication: %SystemReplication{local_site_id: local_site_id} = system_replication }) do - case Map.get(system_replication, "site/#{local_site_id}/REPLICATION_MODE") do + case Map.get(system_replication, :"site/#{local_site_id}/REPLICATION_MODE") do "PRIMARY" -> "Primary" @@ -262,10 +166,8 @@ defmodule Trento.Integration.Discovery.SapSystemPolicy do # Find status information at: # https://help.sap.com/viewer/4e9b18c116aa42fc84c7dbfd02111aba/2.0.04/en-US/aefc55a27003440792e34ece2125dc89.html - defp parse_system_replication_status(%{ - "SystemReplication" => %{"overall_replication_status" => status} + defp parse_system_replication_status(%Instance{ + SystemReplication: %SystemReplication{overall_replication_status: status} }), do: status - - defp parse_system_replication_status(_), do: "" end