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
@typedoc """
Asset type.
Assets are grouped by their asset types. Assets of the same type should be of similar nature
(e.g. servers or buildings) and should share the same set of attributes.
Asset types must start with a slash and must not end with a slash. Slashes can be used
within the asset type to denote structure. For example, these are valid asset types:
`/assets/servers`, `/servers`, and `/assets/network/routers`.
Although asset types like `/servers` are permitted, it is recommended to prefix asset types
with the `/assets` prefix to enable consistent configuration of common attributes (e.g., `name`)
via the root `/assets` asset type in the UI.
"""
@type asset_type :: String.t()
@typedoc """
Asset ID.
Asset ID together with an asset type uniquely identifies an asset.
Asset ID must not contain any slashes. They can contain any other character though.
For example, these are valid asset IDs: `fire_alarm`, `192.168.0.142`, `Meeting room`, and
`Příliš žluťoučký kůň`.
"""
@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 (for more information, see `t:Runbox.Scenario.OutputAction.asset_type/0`)
* `:id` - asset ID (for more information, see `t:Runbox.Scenario.OutputAction.asset_id/0`)
* `: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.
See `t:t/0` for more information.
> #### Note {: .warning}
>
> Beware that creating incident history manually via event's `incident_actors` property, instead
> of relying on Incident output actions, is low-level and prone to errors. You must always ensure
> that you create the event **and** you accordingly update the incident itself. Failing to do
> either will result in inconsistent state.
"""
@enforce_keys [:type, :template, :actors]
defstruct type: nil,
template: nil,
actors: nil,
incident_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 asset actors that are to be linked with the event. They may also be
interpolated in the template.
* `:incident_actors` - map of incident actors that are to be linked with the event. This means
the event is either part of the incident's history, or is just related. They may also be
interpolated in the template. See `t:incident_actor/0` for more information.
* `: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.
Incident actors can be interpolated in a very similar manner using `incidents` keyword -
`${incidents.fire}`.
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(),
incident_actors: incident_actors(),
params: %{String.t() => String.t()},
origin_messages: [Runbox.Message.origin()]
}
@type actors :: %{(actor_key :: String.t()) => actor()}
@type actor :: %{
asset_type: String.t(),
asset_id: String.t()
}
@type incident_actors :: %{(actor_key :: String.t()) => incident_actor()}
@typedoc """
Incident actor of the event.
Can mean two things depending on if `status` and `severity` is set. If either is empty, then the
event is considered related to the incident, similar to `actors`. If both are set, then the
event is considered a part of the incident's history. `status` and `severity` then represent the
change of these incident properties at the time of the event. `attributes` is fully optional
and contains further context about the change of the incident.
"""
@type incident_actor :: %{
:type => String.t(),
:id => String.t(),
optional(:status) => String.t() | nil,
optional(:severity) => 1 | 2 | 3 | 4 | nil,
optional(:attributes) => map() | nil
}
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: [Runbox.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