defmodule Runbox.Scenario.OutputAction do
@moduledoc group: :output_actions
@moduledoc """
Output action represents a side effect produced by a run.
## Creating output actions
Creating output action typically happens from scenario. It is done by
creating one of the appropriate struct:
* `t:Runbox.Scenario.OutputAction.UpsertAssetAttributes.t/0`
* `t:Runbox.Scenario.OutputAction.DeleteAssetAttributes.t/0`
* `t:Runbox.Scenario.OutputAction.DeleteAllAssetAttributes.t/0`
* `t:Runbox.Scenario.OutputAction.UpsertEdge.t/0`
* `t:Runbox.Scenario.OutputAction.DeleteEdge.t/0`
* `t:Runbox.Scenario.OutputAction.Notification.t/0`
* `t:Runbox.Scenario.OutputAction.Event.t/0`
* `t:Runbox.Scenario.OutputAction.ExecuteSQL.t/0`
* `t:Runbox.Scenario.OutputAction.Incident.t/0`
* `t:Runbox.Scenario.OutputAction.IncidentPatch.t/0`
For example:
iex> %OutputAction.UpsertAssetAttributes{
...> type: "/asset/camera",
...> id: "one",
...> attributes: %{"foo" => "bar"}
...> }
## Asset and edge output actions
Changes done by output actions to assets and edges are scoped for each
scenario run. The global result is computed from the changes done by
individual runs.
A single run is forbidden to do multiple changes to the same object (asset
attribute or edge) at the same time. However distinct runs can perform such
changes, because they are scoped to each run.
The rules to compute the global state apply in the following order:
1. Change with the higher timestamp has precedence.
1. Updating an asset attribute or an edge has precedence to deleting it.
1. Change with the higher value has precedence (applies only for asset attributes).
"""
alias __MODULE__, as: OA
@type asset_type :: String.t()
@type asset_id :: String.t()
@type edge_type :: String.t()
defmodule UpsertAssetAttributes do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Upsert Asset Attributes.
The resulting output action will create or update attributes of the given
asset.
"""
@enforce_keys [:type, :id, :attributes]
defstruct [:type, :id, :attributes]
@typedoc """
Upsert Asset Attributes
* `:type` - asset type
* `:id` - asset ID
* `:attributes` - a map of attributes in format `%{"name" => value}` to be
inserted or updated
"""
@type t :: %__MODULE__{
type: OA.asset_type(),
id: OA.asset_id(),
attributes: map()
}
end
defmodule DeleteAssetAttributes do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Delete Asset Attributes.
The resulting output action will delete attributes of the given asset. This
action succeeds even if the attributes do not exist.
"""
@enforce_keys [:type, :id, :attributes]
defstruct [:type, :id, :attributes]
@typedoc """
Delete Asset Attributes
* `:type` - asset type
* `:id` - asset ID
* `:attributes` - a map of attributes to be deleted
The leaf attributes of the `:attributes` map are deleted.
E.g., if asset has the following attributes
```elixir
%{
"a": 1,
"b": %{"c" => 2}
}
```
and the following `:attributes` are provided in the output action
```elixir
%{
"b": %{"c" => true}
}
```
then `b.c` attribute will be deleted, resulting in
```elixir
%{
"a": 1
"b": %{}
}
```
"""
@type t :: %__MODULE__{
type: OA.asset_type(),
id: OA.asset_id(),
attributes: map()
}
end
defmodule DeleteAllAssetAttributes do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Delete All Asset Attributes.
The resulting output action will delete all existing attributes of the
given asset. This is scoped to the attributes created by the same run.
"""
@enforce_keys [:type, :id]
defstruct [:type, :id]
@typedoc """
Delete All Asset Attributes
* `:type` - asset type
* `:id` - asset ID
"""
@type t :: %__MODULE__{
type: OA.asset_type(),
id: OA.asset_id()
}
end
defmodule UpsertEdge do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Upsert Edge.
The resulting output action will create an edge between two assets. The
assets do not have to exist at the time of the edge creation.
"""
@enforce_keys [:from_type, :from_id, :to_type, :to_id, :type]
defstruct [:from_type, :from_id, :to_type, :to_id, :type]
@typedoc """
Upsert Edge
* `:from_type` - type of the asset which this edge goes out from
* `:from_id` - id of the asset which this edge goes out from
* `:to_type` - type of the asset which this edge goes to
* `:to_id` - id of the asset which this edge goes to
* `:type` - type of the edge
"""
@type t :: %__MODULE__{
from_type: OA.asset_type(),
from_id: OA.asset_id(),
to_type: OA.asset_type(),
to_id: OA.asset_id(),
type: OA.edge_type()
}
end
defmodule DeleteEdge do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Delete Edge.
The resulting output action will delete an edge between two assets. This
action succeeds even if the edge does not exist.
"""
@enforce_keys [:from_type, :from_id, :to_type, :to_id, :type]
defstruct [:from_type, :from_id, :to_type, :to_id, :type]
@typedoc """
Delete Edge
* `:from_type` - type of the asset which this edge goes out from
* `:from_id` - id of the asset which this edge goes out from
* `:to_type` - type of the asset which this edge goes to
* `:to_id` - id of the asset which this edge goes to
* `:type` - type of the edge
"""
@type t :: %__MODULE__{
from_type: OA.asset_type(),
from_id: OA.asset_id(),
to_type: OA.asset_type(),
to_id: OA.asset_id(),
type: OA.edge_type()
}
end
defmodule Notification do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Notification.
The resulting output action sends a notification.
"""
@enforce_keys [:type, :data]
defstruct type: nil,
data: nil,
primary_asset: nil,
ids: [],
aqls: %{},
priority: :medium,
metadata: %{},
direct_subscriptions: nil,
attachments: nil
@typedoc """
Notification
* `:type` - string, type of the notification
* `:data` - map, data for the template evaluation
* `:primary_asset` - string, optional, id of an asset that corresponds to this notification
* `:ids` - list of asset ids, optional, to filter recipients, each recipient must have
sufficient privileges to see all listed IDs
* `:aqls` - map, AQLs which are evaluated, provide further context to the templates, and also can
serve the same purpose as `ids` - filter recipients to only those who can see the whole
result of the query. Key is the name of the AQL which can be used in the template. Value
is a map with `aql` (the AQL query) and `primary` (whether the AQL should also be used
to filter recipients) keys.
* `:priority` - optional, defaults to `:medium`, can be `:low`, `:medium`, `:high`, only
applicable to some channels
* `:metadata` - map, optional, additional metadata which can be used in notification group
subscriptions. Should adhere to the format specified in `Runbox.Scenario.Manifest.notifications`.
* `:direct_subscriptions` - list of maps, subscriptions to be used additionally to the usual
routing. Each map should have the following.
* `:user` - map, user object to be used instead of Altworx object, should have
`notification.email` and optionally even `notification.language`.
* `:templates` - list of ids, which templates should be used
* `:channels` - list of ids, which channels should be used
* `:attachments` - list of maps, optional, attachments that should be sent with the notification.
Each map has the following keys.
* `:data` - binary, content of the attachment
* `:filename` - string, name of the file
* `:content_type` - string, optional, MIME type of the data
* `:type` - whether the attachment is to inlined in the template (`:inline`) or not
(`:attachment`, this is the default)
"""
@type t :: %__MODULE__{
type: String.t() | atom(),
data: map(),
primary_asset: String.t() | nil,
ids: [String.t()],
aqls: map(),
priority: :low | :medium | :high | String.t(),
metadata: map(),
direct_subscriptions: [map()] | nil,
attachments: [map()] | nil
}
end
defmodule Event do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Event.
The resulting output action creates an event.
"""
@enforce_keys [:type, :template, :actors]
defstruct type: nil,
template: nil,
actors: nil,
params: %{},
origin_messages: []
@typedoc """
Event
* `:type` - type of the event. It's declared in Scenario's Manifest.
* `:template` - event's template that allows to interpolate actors and parameters
(see the *Interpolation* section below).
* `:actors` - map of actors that are to be linked with the event. They may also be
interpolated in the template.
* `:params` - map of template parameters that are interpolated in the template
(optional, defaults to `%{}`).
* `:origin_messages` - list of references to raw messages linked with the event itself
(optional, defaults to `[]`).
## Interpolation
The template may reference actors using placeholders like `${actors.actor_key}`, where
`actor_key` corresponds to a key in the `actors` map within this struct. In the UI,
these placeholders are transformed into links. Each link points to the corresponding
asset and displays the asset's name as the link text.
The template may also contain placeholders like `${params.param_key}`, where `param_key`
is a key under the `params` map. These placeholders are simply replaced with the
corresponding values from the `params` map. These parameters are useful for dynamic
content other than actors.
## Example
%Event{
type: "server_login",
template: "${actors.person} logged into ${actors.server} using OpenSSH ${params.openssh_version}",
actors: %{
"person" => %{
asset_type: "/assets/person",
asset_id: "joe"
},
"server" => %{
asset_type: "/assets/server",
asset_id: "192.168.142.18"
}
},
params: %{"openssh_version" => "9.8"},
origin_messages: [normalized_message.origin]
}
"""
@type t :: %__MODULE__{
type: String.t() | atom(),
template: String.t(),
actors: actors(),
params: %{String.t() => String.t()},
origin_messages: [Toolbox.Message.origin()]
}
@type actors :: %{(actor_key :: String.t()) => actor()}
@type actor :: %{
asset_type: String.t(),
asset_id: String.t()
}
end
defmodule ExecuteSQL do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Execute SQL.
The resulting output action executes an SQL statement.
"""
@enforce_keys [:db_connection, :sql_query, :data, :type]
defstruct db_connection: nil,
sql_query: nil,
data: nil,
type: nil
@typedoc """
Execute SQL
* `:db_connection` - map with database connection info (`hostname`, `username`, `password`, `database`)
* `:sql_query` - SQL query.
* `:data` - list of parameterized data.
* `:type` - database type, right now only PostgreSQL is supported. It's checked in `OutputProcessor` and
the value must be `:postgresql`.
If `:run_id` is present in `data`, it's replaced with actual run_id in `AssetMap.Api.SqlExecutor`.
"""
@type t :: %__MODULE__{
db_connection: map(),
sql_query: String.t(),
data: list(),
type: :postgresql | String.t()
}
end
@typedoc """
String with interpolation support.
Supports interpolating asset references to a format which is useful for
displaying assets in the Altworx UI. The string can contain any number of
placeholders in format
${assets.["/assets/camera", "one"]}
Where `"/assets/camera"` is the asset type and `"one"` is the asset ID. The resulting interpolation is
[Camera One[/assets/camera/one]]
Where `Camera One` is the asset's `name` attribute and `/assets/camera/one`
is the asset's full ID. When the asset `name` is not present, the asset's ID
is used instead
[one[/assets/camera/one]]
When the asset cannot be found, the resulting interpolation is
unknown
The interpolation is done upon "read" operations, therefore the result can
vary based on the asset visibility for the identity of the user performing
the operation.
"""
@type interpolable :: String.t()
defmodule IncidentFuture do
@moduledoc group: :output_actions
@moduledoc "Incident Future"
@enforce_keys [
:status,
:severity,
:timestamp,
:description
]
defstruct [
:status,
:severity,
:timestamp,
:description
]
@type t :: %IncidentFuture{
status: String.t(),
severity: OA.Incident.severity(),
timestamp: integer(),
description: OA.interpolable()
}
end
defmodule IncidentHistory do
@moduledoc group: :output_actions
@moduledoc """
Incident history.
The struct represents a change of an Incident in time. It carries information about how the
Incident changed, all describe how the Incident looks after the change. The struct contains all
necessary parameters to create an Incident history record.
Since Incident history is now synonymous to Events, `event_*` fields of this struct can be used
to control how the final Event looks like.
Available fields
- `:status` - status of the Incident
- `:severity` - severity of the Incident
- `:timestamp` - time of the change
- `:description` - description of the change - what has happened, will be used as Event template
- `:attributes` - (optional) significant additional attributes that changed with the Incident
(usually metrics, like temperature, server load, distance). Can be empty.
- `:event_type` - (optional) type of the underlying Event, if empty a default type
`incident_updated` will be used
- `:event_actors` - (optional) additional actors of the underlying Event. If empty no actors are
defined.
- `:event_params` - (optional) additional parameters of the Event. If empty no params are
defined.
- `:event_origin_messages` - (optional) list of references to raw messages linked with the
underlying Event. If empty no raw messages are linked with the Event.
"""
@enforce_keys [
:status,
:severity,
:timestamp,
:description
]
defstruct [
:status,
:severity,
:timestamp,
:description,
:attributes,
:event_type,
:event_actors,
:event_params,
:event_origin_messages
]
@typedoc """
Incident history.
Note Incident history is synonymous to Events and thus carries similar parameters.
- `:status` - status of the Incident
- `:severity` - severity of the Incident
- `:timestamp` - time of the change
- `:description` - description of the change - what has happened, will be used as Event template
- `:attributes` - (optional) significant additional attributes that changed with the Incident
(usually metrics, like temperature, server load, distance). Can be empty.
- `:event_type` - (optional) type of the underlying Event, if empty a default type
`incident_updated` will be used
- `:event_actors` - (optional) additional actors of the underlying Event. If empty no actors are
defined.
- `:event_params` - (optional) additional parameters of the Event. If empty no params are
defined.
- `:event_origin_messages` - (optional) list of references to raw messages linked with the
underlying Event. If empty no raw messages are linked with the Event.
"""
@type t :: %IncidentHistory{
status: String.t(),
severity: OA.Incident.severity(),
timestamp: integer(),
description: OA.interpolable(),
attributes: map() | nil,
event_type: String.t() | nil,
event_actors: Event.actors() | nil,
event_params: %{String.t() => String.t()} | nil,
event_origin_messages: [Toolbox.Message.origin()] | nil
}
end
defmodule IncidentActor do
@moduledoc group: :output_actions
@moduledoc "Incident Actor"
@enforce_keys [
:type,
:id
]
defstruct [
:type,
:id
]
@type t :: %IncidentActor{
type: String.t(),
id: String.t()
}
end
defmodule Incident do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Incident.
The resulting output action creates an incident.
"""
@enforce_keys [
:type,
:id,
:subject,
:status,
:resolved,
:severity,
:future,
:history
]
defstruct [
:type,
:id,
:subject,
:status,
:resolved,
:severity,
:future,
:history,
:actors,
:user_actions,
:additional_attributes
]
@type severity :: 1..4
@typedoc """
Incident
"""
@type t :: %Incident{
type: String.t(),
id: String.t(),
subject: OA.interpolable(),
status: String.t(),
resolved: boolean(),
severity: severity(),
future: [IncidentFuture.t()],
history: [IncidentHistory.t()],
actors: [IncidentActor.t()] | nil,
user_actions: map() | nil,
additional_attributes: map() | nil
}
end
defmodule IncidentPatch do
@moduledoc group: :output_actions
@moduledoc """
Parameters for output action Incident Patch.
The resulting output action updates an incident.
"""
@enforce_keys [
:type,
:id
]
defstruct [
:type,
:id,
:subject,
:status,
:resolved,
:severity,
:future,
:history,
:actors,
:user_actions,
:additional_attributes
]
@typedoc """
Incident Patch
"""
@type t :: %IncidentPatch{
type: String.t(),
id: String.t(),
subject: OA.interpolable() | nil,
status: String.t() | nil,
resolved: boolean() | nil,
severity: OA.Incident.severity() | nil,
future: [IncidentFuture.t()] | nil,
history: [IncidentHistory.t()] | nil,
actors:
%{
optional(:add) => [IncidentActor.t()],
optional(:remove) => [IncidentActor.t()]
}
| nil,
user_actions:
%{
optional(:upsert) => map(),
optional(:remove) => list()
}
| nil,
additional_attributes:
%{
optional(:upsert) => map(),
optional(:remove) => list()
}
| nil
}
end
defmodule BadOutputAction do
@moduledoc group: :output_actions
@moduledoc """
Body of invalid output action.
"""
@enforce_keys [:params]
defstruct [:params]
@typedoc """
Bad Output Action
* `:params` - holds the original parameters
"""
@type t :: %__MODULE__{
params: any()
}
end
@type oa_params ::
UpsertAssetAttributes.t()
| DeleteAssetAttributes.t()
| DeleteAllAssetAttributes.t()
| UpsertEdge.t()
| DeleteEdge.t()
| Notification.t()
| Event.t()
| ExecuteSQL.t()
| Incident.t()
| IncidentPatch.t()
defstruct [:body, :timestamp, :scenario_id, :run_id]
@typedoc """
Output Action
This struct is produced by `Runbox.Runtime.Stage.Sandbox.execute_run/3`.
"""
@type t :: %__MODULE__{
body: oa_params() | BadOutputAction.t(),
timestamp: integer()
}
end