diff --git a/documentation/extensibility-platform/defined-entities/mqtt-behaviors.md b/documentation/extensibility-platform/defined-entities/mqtt-behaviors.md index 369cd2e..fa9da68 100644 --- a/documentation/extensibility-platform/defined-entities/mqtt-behaviors.md +++ b/documentation/extensibility-platform/defined-entities/mqtt-behaviors.md @@ -1,2 +1,776 @@ # MQTT Behaviors +MQTT behaviors in Cloud Director are based on MQTT API extensions (in the context of [API extensibility]()) . Invocation of a MQTT behavior is another way to trigger a MQTT extension (send a message to the extension for processing). When invoked MQTT behaviors essentially post a message to an extension topic and listen for a response to that message. + +## Prerequisites +In order to create a MQTT behavior an MQTT API extension must be registered in Cloud Director first. + +Please familiarize yourself with [MQTT API extensibility]() and how to register such an extension. + +## Behavior Definition +MQTT behavior example definition: +```json +{ + "name": "mqtt_behavior_test", + "execution": { + "type": "MQTT", + "id": "mqtt_behavior_test", + "execution_properties": { + "serviceId": "urn:vcloud:extension-api:VMWare_TEST:MqttExtension_TEST:1.2.3", + "invocation_timeout": 15 + } + + } +} +``` +The MQTT behavior's execution `type` is `MQTT`. It is a required field. + +The `id` is a user-defined string. It is a required field. + +The `serviceId` from the `execution_properties` section holds the ID of the MQTT API extension which will be consuming the messages of this behavior. It is a required field. + +The `invocation_timeout` field is used to specify a timeout in seconds for the response received from the MQTT extension. If not set, there is a system parameter `extensibility.timeout` which will be used as timeout (default is 10 seconds). + +## MQTT Behavior Message Format +### Cloud Director to Extension +Messages from Cloud Director to Extension are sent on the extension's monitor topic: +``` +topic/extension////ext +``` +The messages have the following format: +```json +{ + "type":"BEHAVIOR_INVOCATION", + "headers":{ + "taskId":"9cb74c5d-2a72-45de-b729-2495f680c7f4", + "entityId":"urn:vcloud:entity:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "behaviorId":"urn:vcloud:behavior-interface:mqtt_behavior_test:vmware:mqttInterface:1.0.0", + "context":{ + "userId": "urn:vcloud:user:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "orgId": "urn:vcloud:org:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "rights":[ + "urn:vcloud:org:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + ... + ] + }, + "payload": " " + + } +} +``` + +The `rights` field from the `context` header is populated only if the `extensibility.enableRightInfo` configuration property is set to true. By default it is set to false. + +
+ Java Class representing invocation MQTT message + +```java +public enum NotificationType { + EVENT, + EXTERNAL_EVENT, + TASK, + EXTENSION_TASK, + API_REQUEST, + API_RESPONSE, + BEHAVIOR_INVOCATION, + BEHAVIOR_RESPONSE, +} +``` + +```java +import java.util.List; + +public class MqttRemoteServerMessage { + private NotificationType type; + private Headers headers; + private String payload; + + public void setType(NotificationType type) { + this.type = type; + } + + public void setHeaders(Headers headers) { + this.headers = headers; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + /** + * @return the notification type of the messageIn the case of a MQTT message coming from VCD this + * is BEHAVIOR_INVOCATION. + */ + public NotificationType getType() { + return type; + } + + public Headers getHeaders() { + return headers; + } + + public String getPayload() { + return payload; + } + + /** + * MQTT message headers + */ + public static class Headers { + private String taskId; + private String entityId; + private String behaviorId; + private Context context; + + + /** + * @return The id of the behavior invocation task + */ + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * @return the id of the RDE which the behavior was invoked on + */ + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + /** + * @return the id of the invoked behavior + */ + public String getBehaviorId() { + return behaviorId; + } + + public void setBehaviorId(String behaviorId) { + this.behaviorId = behaviorId; + } + + /** + * @return the MQTT message context + */ + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } + } + + /** + * @return the MQTT message context + */ + public static class Context { + private String orgId; + private String userId; + private List rights; + + /** + * @return the id of the org which the behavior was invoked in + */ + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + /** + * @return the id of the user who invoked the behavior + */ + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + /** + * @return rights of the user who invoked the behavior. + * This field is populated only if the extensibility.enableRightInfo configuration + * property is set to true (by default it is set to false) + */ + public List getRights() { + return rights; + } + + public void setRights(List rights) { + this.rights = rights; + } + } + +} +``` +
+ +The payload holds the invocation arguments from the MQTT behavior invocation: +```json +{ + "_execution_properties":{ + "serviceId":"urn:vcloud:extension-api:VMWare_TEST:MqttExtension_TEST:1.2.3" + }, + "entityId":"urn:vcloud:entity:vmware:mqttType_test:1.0.0:3542f778-37e3-4ce9-b244-41ccc36e27c3", + "typeId":"urn:vcloud:type:vmware:mqttType_test:1.0.0", + "arguments":{ + "argument1":"argument1" + }, + "additionalArguments":null, + "_metadata": { + "executionId":"mqtt_behavior_test", + "invocation":{}, + "behaviorId":"urn:vcloud:behavior-interface:mqtt_behavior_test:vmware:mqttInterface:1.0.0", + "taskId":"c359ea34-2db6-419f-bad6-468a9704d49e", + "executionType":"MQTT" + }, + "entity": { + "property1":"property1", + } +} +``` +
+ Java Class to deserialize payload to InvocationArguments + +```java +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class InvocationArguments { + private String entityId; + private String typeId; + @JsonProperty("_metadata") + private InvocationArgumentsMetadata metadata; + private Map entity; + private Map arguments; + @JsonProperty("_execution_properties") + private Map executionProperties; + private Map additionalArguments; + + /** + * @return the id of the RDE which the behavior was invoked on + */ + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + /** + * @return The id of the RDE Type of the entity which the behavior was invoked in + */ + public String getTypeId() { + return typeId; + } + + public void setTypeId(String typeId) { + this.typeId = typeId; + } + + /** + * @return The invocation {@link InvocationArgumentsMetadata} + */ + public InvocationArgumentsMetadata getMetadata() { + return metadata; + } + + public void setMetadata(InvocationArgumentsMetadata metadata) { + this.metadata = metadata; + } + + /** + * @return the entity contents of the RDE which the behavior was invoked on + */ + public Map getEntity() { + return entity; + } + + public void setEntity(Map entity) { + this.entity = entity; + } + + /** + * @return the user-provided arguments upon invocation + */ + public Map getArguments() { + return arguments; + } + + public void setArguments(Map arguments) { + this.arguments = arguments; + } + + /** + * @return the behavior's execution_properties + */ + public Map getExecutionProperties() { + return executionProperties; + } + + public void setExecutionProperties(Map executionProperties) { + this.executionProperties = executionProperties; + } + + /** + * @return additional_arguments from the behavior's execution + */ + public Map getAdditionalArguments() { + return additionalArguments; + } + + public void setAdditionalArguments(Map additionalArguments) { + this.additionalArguments = additionalArguments; + } + + + /** + * The behavior invocation metadata + */ + public static class InvocationArgumentsMetadata { + private String behaviorId; + private String taskId; + private String executionId; + private String executionType; + private String actAsToken; + private Map invocation; + + + /** + * @return the id of the invoked behavior + */ + public String getBehaviorId() { + return behaviorId; + } + + public void setBehaviorId(String behaviorId) { + this.behaviorId = behaviorId; + } + + /** + * @return the id of the behavior invocation task + */ + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * @return the behavior's execution id + */ + public String getExecutionId() { + return executionId; + } + + public void setExecutionId(String executionId) { + this.executionId = executionId; + } + + /** + * @return the behavior's execution type + */ + public String getExecutionType() { + return executionType; + } + + public void setExecutionType(String executionType) { + this.executionType = executionType; + } + + /** + * @return a {@link Map} of the user-provided metadata upon invocation + */ + public Map getInvocation() { + return invocation; + } + + public void setInvocation(Map invocation) { + this.invocation = invocation; + } + + /** + * @return an act-as token if additional API calls to VCD need to be made + * (it os only populated if it is specified in the behavior's definition) + */ + public String getActAsToken() { + return actAsToken; + } + + public void setActAsToken(String actAsToken) { + this.actAsToken = actAsToken; + } + } +} +``` + +
+ +### Extension to Cloud Director +Response messages from Cloud Director to extension must be sent on the extension's respond topic: +``` +topic/extension////vcd +``` +The messages must have the following format: +```json +{ + "type": "BEHAVIOR_RESPONSE", + "headers": { + "taskId": "9cb74c5d-2a72-45de-b729-2495f680c7f4", + "entityId": "urn:vcloud:entity:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }, + "payload": "
" + +} +``` + +
+ Java Class representing response MQTT message + +```java +import java.util.Arrays; + +public class MqttRemoteServerResponseMessage { + private NotificationType type; + private Headers headers; + private String payload; + + public void setType(NotificationType type) { + this.type = type; + } + + public void setHeaders(Headers headers) { + this.headers = headers; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + /** + * @return the notification type of the MQTT message. In the case of a MQTT message sent as a + * response from extension to VCD this is BEHAVIOR_RESPONSE. + */ + public NotificationType getType() { + return type; + } + + /** + * @return the {@link Headers} of the MQTT message + */ + public Headers getHeaders() { + return headers; + } + + /** + * @return the payload of the MQTT message. Must be a JSON encoded string. + */ + public String getPayload() { + return payload; + } + + /** + * The headers of the MQTT message + */ + public static class Headers { + private String taskId; + private String entityId; + private String contentType; + + /** + * @return the id of the RDE which the behavior was invoked on + */ + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + /** + * @return The id of the behavior invocation task + */ + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * @return the content-type of the payload of the MQTT message response + */ + public String getContentType() { + return contentType; + } + + public void setContentType(ResponseContentType contentType) { + this.contentType = contentType.getValue(); + } + } + + /** + * Content-type of the payload of the MQTT message response + */ + public static enum ResponseContentType { + PLAIN_TEXT("plain/text"), + TASK("application/vnd.vmware.vcloud.task+json"), + ; + + private final String value; + ResponseContentType(String value){ + this.value = value; + } + + public String getValue() { + return value; + } + + public static ResponseContentType fromValue(String string) { + return Arrays.stream(ResponseContentType.values()).filter( + t -> t.getValue().equals(string) + ).findFirst().orElse(null); + } + } +} +``` +
+There are two types of responses an extension can send back to Cloud Director - a simple response and a task update response. + +The simple response (`ResponseContentType.PLAIN_TEXT`) completes the behavior invocation task successfully and uses the payload string as the task result. + +The task update response (`ResponseContentType.TASK`) allows for updating not only the behavior invocation task's `result`, but also the task's `status`, `details`, `operation`, `error`, `progress`. The payload must represent a valid JSON representation of `TaskType` with the task properties that need to be modified. The headers must contain a `Content-Type` header with the value of `application/vnd.vmware.vcloud.task+json`. + +Multiple task update responses can be sent back to Cloud Director. This allows the task progress to be updated continuously, for example. The last task update must complete the task. Once the task is completed, later task updates regarding this task are ignored. + +Example success task update payload: + +```json +{ + "status": "success", + "operation": "test-operation", + "details": "test details", + "result": { + "resultContent": "test result" + }, + "progress": 100 +} +``` + +Example error task update payload: +```json +{ + "status": "error", + "operation": "test-operation", + "details": "test details", + "error": { + "majorErrorCode": 409, + "minorErrorCode": "TEST_ERROR", + "message": "Test error message" + } +} +``` + +Example aborted task update payload +```json +{ + "status": "aborted", + "operation": "test-operation", + "details": "test details", + "progress": 50 +} +``` +
+ Java class representing a Task + +```java +import com.fasterxml.jackson.annotation.JsonValue; + +public class TaskType { + + public static enum TaskStatus { + PENDING("pending"), + PRE_RUNNING("pre-running"), + RUNNING("running"), + SUCCESS("success"), + ABORTED("aborted"), + ERROR("error"), + CANCELED("canceled"), + EXPECTING_ACTION("expectingAction"); + + private final String value; + + TaskStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + public static class ErrorType { + private String majorErrorCode; + private String minorErrorCode; + private String message; + + public String getMajorErrorCode() { + return majorErrorCode; + } + + public void setMajorErrorCode(String majorErrorCode) { + this.majorErrorCode = majorErrorCode; + } + + public String getMinorErrorCode() { + return minorErrorCode; + } + + public void setMinorErrorCode(String minorErrorCode) { + this.minorErrorCode = minorErrorCode; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } + + private TaskType status; + private String operation; + private String details; + private ErrorType error; + private int progress; + + /** + * @return the task status + */ + public TaskType getStatus() { + return status; + } + + public void setStatus(TaskType status) { + this.status = status; + } + + /** + * @return the task operation + */ + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + /** + * @return the task details + */ + public String getDetails() { + return details; + } + + public void setDetails(String details) { + this.details = details; + } + + /** + * @return the task error as {@link ErrorType} + */ + public ErrorType getError() { + return error; + } + + public void setError(ErrorType error) { + this.error = error; + } + + /** + * @return the task progress. Must be in the range [0,100]. + */ + public int getProgress() { + return progress; + } + + public void setProgress(int progress) { + this.progress = progress; + } +} +``` +
+ +## Example `IMqttMessageListener` implementation for processing MQTT messages + +```java +public class MqttListener implements IMqttMessageListener { + + private final String topicToRespond; + private final MqttClient mqttClient; + private static final ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + public MqttListener(final String topicToRespond, final MqttClient mqttClient) { + this.topicToRespond = topicToRespond; + this.mqttClient = mqttClient; + } + + public void closeListener() throws MqttException { + this.mqttClient.disconnectForcibly(); + } + + + @Override + public void messageArrived(final String s, final MqttMessage mqttMessage) throws Exception { + // Message from VCD received + + MqttRemoteServerMessage request = objectMapper.readValue(mqttMessage.getPayload(), MqttRemoteServerMessage.class); + + if (NotificationType.BEHAVIOR_INVOCATION != request.getType()) { + // ignore not behavior invocation messages + return; + } + //parse the request payload to a Map + InvocationArguments invocationArguments = objectMapper.readValue(request.getPayload(), InvocationArguments.class); + + //now the information can be accessed + Map executionProperties = invocationArguments.getExecutionProperties(); + InvocationArguments.InvocationArgumentsMetadata metadata = invocationArguments.getMetadata(); + Map arguments = invocationArguments.getArguments(); + String typeId = invocationArguments.getTypeId(); + + final MqttRemoteServerResponseMessage response = createResponse(request); + + String responseAsJson = objectMapper.writeValueAsString(response); + + mqttClient.publish(topicToRespond, new MqttMessage(responseAsJson.getBytes())); + } + + private MqttRemoteServerResponseMessage createResponse(MqttRemoteServerMessage request) { + // your business logic goes here + } + +} + +``` diff --git a/documentation/extensibility-platform/defined-entities/webhook-behaviors.md b/documentation/extensibility-platform/defined-entities/webhook-behaviors.md index 1bb820d..2aac8db 100644 --- a/documentation/extensibility-platform/defined-entities/webhook-behaviors.md +++ b/documentation/extensibility-platform/defined-entities/webhook-behaviors.md @@ -1 +1,1088 @@ # WebHook Behaviors +WebHook behaviors provide webHook functionality in the context of Cloud Director. In short, upon invocation a webHook behavior a POST request is sent to a webHook server. After receiving the server's response, the behavior invocation task is updated accordingly. The payload sent to the webHook server is configurable. + +## Prerequisites +A webHook server must be set-up to process requests from webHook behavior invocations before creating any webHook behaviors. The webHook server must support HTTPs and its SSL certificate must be trusted in the organization of the user invoking the webHook behavior in order for the connection to be successful ([how to trust a certificate in Cloud Director](https://docs.vmware.com/en/VMware-Cloud-Director/10.4/VMware-Cloud-Director-Service-Provider-Admin-Portal-Guide/GUID-80B4CB1C-9353-4EB9-8557-4F6705949D0F.html)). + +For testing purposes you can use [webhooks.site](https://webhook.site/). It provides a random URL which canbe used as a webHook server URL. The response sent back from webhooks.site to each request can be customized. + +## Behavior Definition +WebHook behavior example definition: + +```json +{ + "name": "webhookBehavior", + "execution": { + "type": "WebHook", + "id": "testWebHook", + "href": "https://example.com/webhooks" , + "_internal_key": "verySecretKey", + "execution_properties": { + "template": { + "content": "<#assign header_Authorization = \"${_execution_properties._secure_token}\" />{\"text\": \"Behavior with id ${_metadata.behaviorId} was executed on entity with id ${entityId}\" }" + }, + "_secure_token": "secureToken" + } + } +} +``` + +The webHook behavior's execution `type` is `WebHook`. It is a required field. + +The `id` is a user-defined string. It is a required field. + + +The `href` holds the URL of the webHook server which will be processing the webHook requests. The value must be a valid URL of `https` protocol. It is a required field. + +The `_internal_key` holds a shared secret string which is only known to Cloud Director and the webHook server. This shared secret will be used to generate the signature header of the request which is part of the [HMAC authentication](#authentication-of-webhook-behavior-requests). It is a required field. + +The `template` field in the execution can be used to set-up a custom payload for the requests sent to the webHook server. More information can be found [here](#custom-request-payload). + +## WebHook Request Payload (Cloud Director -> WebHook Server) +The default payload sent to webHook servers is a JSON containing information about the actual behavior invocation (behavior information, entity information, and some metadata). However, you can also customize this payload by setting a template in the behavior definition.
FreeMarker template engine is used to render the payload from the provided template and the data model. + +### Default Request Payload + +The default payload sent to webHook servers is a JSON containing information about the actual behavior invocation - invocation arguments, behavior information, entity information, and some metadata. + + +Default headers: +``` +content-type=[application/json], +x-vcloud-signature=[ algorithm="hmac-sha512", headers="host date (request-target) digest", signature="WjuxZuVgNXctRBuDLONeQ0NWXt+O36YL8wMdjhCGCeW7Fq8sMHfU6NCS0O6STJx2z/wRkHTjzil4GAfuho9ZUw=="], +x-vcloud-digest=[SHA-512=Yt4eiT2VmUyX8wDt6wneZ10VRk1B1H2jmHP1R7YanI9hykEAjUdtg7JzxfioBQm/iWRRNBM8B0aJnw6Jd29Jqg==], +date=[Thu, 01 Oct 2020 12:57:31 GMT], +accept=[*/*], +user-agent=[Apache-CXF/3.1.17], +cache-control=[no-cache], +pragma=[no-cache], +host=[127.0.0.1:1234], +connection=[keep-alive], +content-length=[579] +``` +Default payload: +```json +{ + "_execution_properties": { + "actAsToken": true + }, + "entity.masked": { + "entity": { + "VcdVm": { + "name": true + } + } + }, + "entityId": "urn:vcloud:entity:vmware:testType:28b0488f-39f0-49d6-a78b-c37e8eaf40be", + "typeId": "urn:vcloud:type:vmware:testType:1.0.1", + "arguments": { + "x": 7 + }, + "_metadata": { + "executionId": "testWebHook", + "execution": { + "href": "https://webhook.site/f17d3903-5f89-468b-b4b0-40a63795bb8b" + }, + "invocation": { + "y": 6 + }, + "apiVersion": "37.3", + "behaviorId": "urn:vcloud:behavior-interface:webhookBehavor2:vmware:test2:1.0.0", + "requestId": "4551c4ee-27df-47dc-b065-b67a91d3904c", + "executionType": "WebHook", + "actAsToken": "...", + "invocationId": "b2031c44-c426-4eee-bae1-423bf5bb4e92", + "taskId": "c928b926-7c1d-40bd-ae0e-2b541e0d5511" + }, + "entity": { + "entity": { + "VcdVm": { + "name": true + } + } + } +} +``` + +
+ Java Class representing the InvocationArguments + +```java +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class InvocationArguments { + private String entityId; + private String typeId; + @JsonProperty("_metadata") + private InvocationArgumentsMetadata metadata; + private Map entity; + private Map arguments; + @JsonProperty("_execution_properties") + private Map executionProperties; + private Map additionalArguments; + + /** + * @return the id of the RDE which the behavior was invoked on + */ + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + /** + * @return The id of the RDE Type of the entity which the behavior was invoked in + */ + public String getTypeId() { + return typeId; + } + + public void setTypeId(String typeId) { + this.typeId = typeId; + } + + /** + * @return The invocation {@link InvocationArgumentsMetadata} + */ + public InvocationArgumentsMetadata getMetadata() { + return metadata; + } + + public void setMetadata(InvocationArgumentsMetadata metadata) { + this.metadata = metadata; + } + + /** + * @return the entity contents of the RDE which the behavior was invoked on + */ + public Map getEntity() { + return entity; + } + + public void setEntity(Map entity) { + this.entity = entity; + } + + /** + * @return the user-provided arguments upon invocation + */ + public Map getArguments() { + return arguments; + } + + public void setArguments(Map arguments) { + this.arguments = arguments; + } + + /** + * @return the behavior's execution_properties + */ + public Map getExecutionProperties() { + return executionProperties; + } + + public void setExecutionProperties(Map executionProperties) { + this.executionProperties = executionProperties; + } + + /** + * @return additional_arguments from the behavior's execution + */ + public Map getAdditionalArguments() { + return additionalArguments; + } + + public void setAdditionalArguments(Map additionalArguments) { + this.additionalArguments = additionalArguments; + } + + + /** + * The behavior invocation metadata + */ + public static class InvocationArgumentsMetadata { + private String behaviorId; + private String taskId; + private String executionId; + private String executionType; + private String actAsToken; + private Map invocation; + + + /** + * @return the id of the invoked behavior + */ + public String getBehaviorId() { + return behaviorId; + } + + public void setBehaviorId(String behaviorId) { + this.behaviorId = behaviorId; + } + + /** + * @return the id of the behavior invocation task + */ + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * @return the behavior's execution id + */ + public String getExecutionId() { + return executionId; + } + + public void setExecutionId(String executionId) { + this.executionId = executionId; + } + + /** + * @return the behavior's execution type + */ + public String getExecutionType() { + return executionType; + } + + public void setExecutionType(String executionType) { + this.executionType = executionType; + } + + /** + * @return a {@link Map} of the user-provided metadata upon invocation + */ + public Map getInvocation() { + return invocation; + } + + public void setInvocation(Map invocation) { + this.invocation = invocation; + } + + /** + * @return an act-as token if additional API calls to VCD need to be made + * (it os only populated if it is specified in the behavior's definition) + */ + public String getActAsToken() { + return actAsToken; + } + + public void setActAsToken(String actAsToken) { + this.actAsToken = actAsToken; + } + } +} +``` + +
+ +### Custom Request Payload +The payload can be customized by setting a template in the behavior definition. FreeMarker template engine is used to render the payload from the provided template and the data model. +#### The Data Model +The data model represents all the data that was prepared for the template. Or in other words, all the data which can be included in the payload. As far as the template author is concerned, the data model is a tree-like structure and can be visualized as: + +``` ++ - entityId +| ++ - typeId +| ++ - arguments +| ++ - arguments_string +| ++ - _execution_properties +| ++ - _metadata +| | +| + - executionId +| + - behaviorId +| + - executionType +| + - taskId +| + - execution // can select all the values from execution by key name +| | | +| | + - href +| + - invocation // can select all the values from invocation by key name +| + - invocationId + | + - requestId +| + - apiVersion +| ++ - entity +| ++ - entity_string + +``` +- `entityId` - the ID of the defined entity which the behavior was invoked on. +- `typeId` - the ID of the entity type of the defined entity which the behavior was invoked on. +- `arguments` - the user supplied arguments from the body of the behavior invocation. +- `arguments_string` - a JSON-encoded string of arguments, can be used if the user wants to add all the arguments to the payload. +- `_execution_properties` - the `execution_properties` from the behavior definition. +- `behaviorId` - the ID of the invoked behavior. +- `executionType` - the execution type of the invoked behavior. +- `taskId` - the ID of the behavior invocation task. +- `execution` - the execution from the behavior definition. +- `invocation` - the user supplied metadata from the body of the behavior invocation. +- `invocationId` - the ID of the behavior invocation. +- `apiVersion` - the API version the behavior was invoked with. +- `entity` - a map of the entity contents, can select all values in entity by key name. +- `entity_string` - a JSON-encoded string of the entity contents. + +#### The Template +More details on how to build a template can be found in the FreeMarker documentation . In short, the way to evaluate a key from the data model is wrapping the key in `${…}`. For example `${_metadata.execution.href}` is the way to get the `href` value from the execution. An example template is: +```json +{ + "text": "Behavior with id ${_metadata.behaviorId} was executed on entity with id ${entityId}" +} +``` +#### Setting Custom Request Headers +Custom headers can be set in the webHook reques by using the template. Each header can be set as a variable in the template with the prefix `header_`. + +Examples: +``` +<#assign header_Content-Type = "application/json" /> + +<#assign header_Authorization = "${_execution_properties._secure_token}" /> +``` + +Example behavior definition with custom payload and headers: +```json +{ + "name": "test-behavior", + "execution": { + "type": "WebHook", + "id": "testWebHook", + "href": "https://webhook.site/45b2d281-9de8-4eec-b7cc-a67529ec4be4", + "_internal_key": "secretKey", + "execution_properties": { + "template": { + "content": "<#assign header_Authorization = \"${_execution_properties._secure_token}\" />{\"text\": \"Behavior with id ${_metadata.behaviorId} was executed on entity with id ${entityId}\"}" + }, + "_secure_token": "secureToken" + } + } +} +``` +## WebHook Response Payload (WebHook Server -> Cloud Director) +There are three possible response formats for the webHook server to send back to Cloud Director. Each one is processed differently by Cloud Director. + +### Simple Response +Sets the behavior invocation task result only. +- Success response - the behavior invocation task status is set to `success` and the response body is used as the task result +``` +Status code: 200 +Content-Type: text/plain //or none + +...some string... +``` +- Error response - the behavior invocation task status is set to `ERROR` +``` +response code != 200 +``` +### Task Update Response (one-time task update) + +Allows for updating not only the behavior invocation task `result`, but also task's `status`, `details`, `operation`, `error`, `progress`. The payload must represent a valid JSON representation of a `TaskType` with the task properties that need to be modified. + +- Success response +``` +Status code: 200 +Content-Type: application/vnd.vmware.vcloud.task+json +``` +```json +{ + "status": "success", + "details": "example details", + "operation": "example operation", + "progress": 100, + "result": { + "resultContent": "example result" + } +} +``` +- Error response +``` +Status code: 200 +Content-Type: application/vnd.vmware.vcloud.task+json +``` +```json +{ + "status": "error", + "details": "example details", + "operation": "example operation", + "progress": 50, + "error": { + "majorErrorCode": 404, + "minorErrorCode": "ERROR", + "message": "example error message" + } +} +``` +### Continuous Task Update Response + +Allows for sending multiple task updates before completing the behavior invocation task. This is done by using HTTP multipart response. Each update is a separate body part of the response body. The last body part of the response body must complete the task. If it does not, the task is completed with an error message indicating that the task should have been completed but was not. + +Example response: +``` +Status code: 200 +Content-Type: multipart/form-data; boundary= +``` +``` +-- +Content-Type: application/vnd.vmware.vcloud.task+json +{ + "details": "example details", + "operation": "example operation", + "progress": 50 +} +-- +Content-Type: application/vnd.vmware.vcloud.task+json +{ + "status": "success", + "progress": 100, + "result": { + "resultContent": "example result" + } +} +-- +``` + +There is a new line after each `boundary string`, `Content-Type` header and body. The body parts of the response can either represent a simple response or a task update response. The last body part of the webHook server response body must complete the task. If it does not, the task is completed with an error message indicating that the task should have been completed but was not. Once a body part completes the behavior invocation task, any other body parts received after that are ignored. + +
+ Java class representing a Task + +```java +import com.fasterxml.jackson.annotation.JsonValue; + +public class TaskType { + + public static enum TaskStatus { + PENDING("pending"), + PRE_RUNNING("pre-running"), + RUNNING("running"), + SUCCESS("success"), + ABORTED("aborted"), + ERROR("error"), + CANCELED("canceled"), + EXPECTING_ACTION("expectingAction"); + + private final String value; + + TaskStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + public static class ErrorType { + private String majorErrorCode; + private String minorErrorCode; + private String message; + + public String getMajorErrorCode() { + return majorErrorCode; + } + + public void setMajorErrorCode(String majorErrorCode) { + this.majorErrorCode = majorErrorCode; + } + + public String getMinorErrorCode() { + return minorErrorCode; + } + + public void setMinorErrorCode(String minorErrorCode) { + this.minorErrorCode = minorErrorCode; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } + + private TaskType status; + private String operation; + private String details; + private ErrorType error; + private int progress; + + /** + * @return the task status + */ + public TaskType getStatus() { + return status; + } + + public void setStatus(TaskType status) { + this.status = status; + } + + /** + * @return the task operation + */ + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + /** + * @return the task details + */ + public String getDetails() { + return details; + } + + public void setDetails(String details) { + this.details = details; + } + + /** + * @return the task error as {@link ErrorType} + */ + public ErrorType getError() { + return error; + } + + public void setError(ErrorType error) { + this.error = error; + } + + /** + * @return the task progress. Must be in the range [0,100]. + */ + public int getProgress() { + return progress; + } + + public void setProgress(int progress) { + this.progress = progress; + } +} +``` +
+ +## Authentication of WebHook Behavior Requests +WebHook behavior requests are secured via HMAC authentication. +### What is HMAC authentication? +HMAC (or hash-based message authentication code) is a mechanism used to ensure the authenticity and integrity of HTTP requests. This is achieved by including two custom headers to each HTTP request – signature header and digest (in our case `x-vcloud-signature` and `x-vcloud-digest`). + +``` +x-vcloud-signature=[ algorithm="hmac-sha512", headers="host date (request-target) digest", signature="WjuxZuVgNXctRBuDLONeQ0NWXt+O36YL8wMdjhCGCeW7Fq8sMHfU6NCS0O6STJx2z/wRkHTjzil4GAfuho9ZUw=="] + +x-vcloud-digest=[SHA-512=Yt4eiT2VmUyX8wDt6wneZ10VRk1B1H2jmHP1R7YanI9hykEAjUdtg7JzxfioBQm/iWRRNBM8B0aJnw6Jd29Jqg==] +``` +### Some terms +- Shared secret - this a string which is only known to Cloud Director and the webHook server (`_internal_key`). This shared secret will be used to generate the signature header +- Signature header (`x-vcloud-signature`) - This is a custom header sent with each webHook request. It consists of three parts: + - `algorithm` (`hmac-sha512`) - this is the algorithm used to generate the signature + - `headers` (`host date (request-target) digest`) - this shows what is included in the base string which is signed to create the signature + - `host` - the webHook server host + - `date` - date of the request + - `(reguest-target) = + + `(e.g. `post /webhook`) + - `digest` - sha-512 digest of the request body + - `signature` - this is the generated signature +- Digest header (`x-vcloud-digest`) - this is a `base64` encoded sha-512 digest of the request body + +### How are requests authenticated by the webHook server? +Each webHook request from Cloud Director can be verified by generating the signature using the shared secret and comparing it to the signature in the signature header from Cloud Director. If they match, the request is verified. + + +
+ Example Java code for verifying a webHook behavior request's signature header + +```java +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +/** + * Verifies an incoming webHook behavior request from VCD. + */ +public class HMACRequestVerificator { + private static final String DIGEST_ALGORITHM = "hmac-sha512"; + private static final String HEADER_SIGNATURE = "x-vcloud-signature"; + private static final String HEADER_DATE = "date"; + private static final String SIGNATURE_PARAM_SIGNATURE = "signature"; + private static final String SIGNATURE_PARAM_ALGORITHM = "algorithm"; + private static final String SIGNATURE_PARAM_HEADERS = "headers"; + private static final String HEADERS_DATE = "date"; + private static final String HEADERS_HOST = "host"; + private static final String HEADERS_REQUEST_TARGET = "(request-target)"; + private static final String HEADERS_DIGEST = "digest"; + private static final Pattern HEADER_SIGNATURE_PATTERN = Pattern.compile("^algorithm=\".+\",headers=\".+\",signature=\".+\"$"); + private Map> headers = null; + private Map signatureParams = null; + private final String sharedSecret; + private String host; + private String path; + private String payload; + private static final ObjectMapper objectMapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + /** + * @param sharedSecret the shared secret which will be used to verify the signature from the VCD request headers. + */ + public HMACRequestVerificator(String sharedSecret) { + this.sharedSecret = sharedSecret; + } + /** + * Provides the host and path of the webhook server. + * @param webhookServerUrl URL of the webhook server endpoint + * @return this {@link HMACRequestVerificator} + */ + public HMACRequestVerificator withWebhookServerUrl(String webhookServerUrl) { + Optional webhookServerUri = buildWebhookServerURI(webhookServerUrl); + if (webhookServerUri.isPresent()) { + URI uri = webhookServerUri.get(); + host = uri.getHost(); + path = uri.getPath(); + } + return this; + } + /** + * Provides the host and path of the webhook server. + * @param host the host of the webhook server + * @param path the path of the webhook server + * @return this {@link HMACRequestVerificator} + */ + public HMACRequestVerificator withWebhookServerHostAndPath(String host, String path) { + this.host = host; + this.path = path; + return this; + } + /** + * @param headers the headers of the incoming request from VCD + * @return this {@link HMACRequestVerificator} + */ + public HMACRequestVerificator withHeaders(Map> headers) { + this.headers = headers; + return this; + } + /** + * @param payload the payload of the incoming request from VCD + * @return this {@link HMACRequestVerificator} + */ + public HMACRequestVerificator withPayload(Object payload) { + this.payload = serializePayload(payload); + return this; + } + /** + * Executes verification. + * @return true if verification was successful and false otherwise + */ + public boolean verify() { + if (!checkParameters()) { + return false; + } + String signatureFromServer = signatureParams.get(SIGNATURE_PARAM_SIGNATURE); + Optional signatureToVerify = buildSignatureToVerify(payload); + return signatureToVerify.isPresent() && signatureFromServer.equals(signatureToVerify.get()); + } + private boolean checkParameters() { + return validateParameters() && validateHeaders(); + } + private boolean validateParameters() { + return sharedSecret != null && payload != null && host != null && path != null; + } + private boolean validateHeaders() { + if (headers == null + || !headers.containsKey(HEADERS_DATE) + || !headers.containsKey(HEADER_SIGNATURE)) { + return false; + } + String signatureHeader = headers.get(HEADER_SIGNATURE).get(0); + Matcher matcher = HEADER_SIGNATURE_PATTERN.matcher(signatureHeader); + if (!matcher.matches()) { + return false; + } + signatureParams = extractSignatureParams(headers.get(HEADER_SIGNATURE).get(0)); + return signatureParams.containsKey(SIGNATURE_PARAM_SIGNATURE) + && signatureParams.containsKey(SIGNATURE_PARAM_HEADERS) + && signatureParams.containsKey(SIGNATURE_PARAM_ALGORITHM); + } + private Map extractSignatureParams(String signatureHeader) { + String [] signatureHeaders = signatureHeader.split(","); + Map result = new HashMap<>(); + for (String h : signatureHeaders) { + String param = h.split("=")[0]; + String value = h.split("\"")[1]; + result.put(param.toLowerCase(), value); + } + return result; + } + private byte[] getDigest(String payload) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + digest.update(payload.getBytes()); + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + //should not happen + throw new RuntimeException(e); + } + } + private Optional buildSignatureToVerify(String payload) { + String signatureString = buildSignatureString(payload); + if (signatureParams.get(SIGNATURE_PARAM_ALGORITHM).equalsIgnoreCase(DIGEST_ALGORITHM)) { + return Optional.of(signDataSHA512HMAC(signatureString, sharedSecret)); + } + return Optional.empty(); + } + private String signDataSHA512HMAC(String data, String sharedSecret) { + try { + final byte[] byteKey = sharedSecret.getBytes(StandardCharsets.UTF_8); + Mac sha512Hmac = Mac.getInstance("HmacSHA512"); + SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA512"); + sha512Hmac.init(keySpec); + byte[] macData = sha512Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return getBase64(macData); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage()); + } + } + private String buildSignatureString(String payload) { + String[] headersInSignature = signatureParams.get(SIGNATURE_PARAM_HEADERS).split(" "); + return Arrays.stream(headersInSignature) + .map(headerInSignature -> buildSignatureStringHeaderParam(headerInSignature, payload)) + .filter(Objects::nonNull) + .collect(Collectors.joining("\n")); + } + private String buildSignatureStringHeaderParam(String paramName, String payload) { + if (paramName.equalsIgnoreCase(HEADERS_HOST)) { + return "host: " + host; + } else if (paramName.equalsIgnoreCase(HEADERS_DATE)) { + return "date: " + getDate(); + } else if (paramName.equalsIgnoreCase(HEADERS_DIGEST)) { + return "digest: SHA-512=" + getBase64(getDigest(payload)); + } else if (paramName.equalsIgnoreCase(HEADERS_REQUEST_TARGET)) { + return "(request-target): post " + path; + } + return null; + } + private String getBase64(byte[] signature) { + return Base64.getEncoder().encodeToString(signature); + } + private Optional buildWebhookServerURI(String webhookServerUrl) { + try { + return Optional.of(new URI(webhookServerUrl)); + } catch (URISyntaxException e) { + return Optional.empty(); + } + } + private String getDate() { + return headers.get(HEADER_DATE).get(0).split("\"")[0]; + } + private String serializePayload(Object payload) { + try { + return objectMapper.writeValueAsString(payload); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} +``` +
+ +## Example Slack integration with webHook behaviors + +Slack has a functionality for creating incoming webHooks which can be used for posting messages to Slack channels. WebHook behaviors can be used to integrate Slack incoming webHooks with Cloud Director, allowing Cloud Director to post messages directly to a slack channel. +More information on how to create a Slack incoming webHook for a channel can be found [here]( https://api.slack.com/messaging/webhooks). + +Once a Slack webHook is configured, messages to it can be sent via a POST request to the webHook URL - `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX`. The request payload has to be of a specific format in order for it to be recognized by the Slack webHook and the `Content-Type` header must be set to `application/json`. + +In order to use Cloud Director's webHook behaviors to send messages to slack webHooks, the following steps must be followed: + +1. [Set-up incoming webHook URL in Slack](https://api.slack.com/messaging/webhooks). +1. Create an Interface +``` +POST /cloudapi/1.0.0/interfaces +``` +```json +{ + "name": "test", + "vendor": "vmware", + "nss": "test", + "version": "1.0.0" +} +``` + +Response: +```json +{ + "name": "test", + "id": "urn:vcloud:interface:vmware:test:1.0.0", + "version": "1.0.0", + "vendor": "vmware", + "nss": "test", + "readonly": false +} +``` +2. Create a template for the payload sent to the webHook server +``` +<#assign header_Content\-Type= "application/json" /> + +{ + + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*BEHAVIOR_EXECUTION*:vcda:\n Behavior with id \n_${_metadata.behaviorId}_\n was executed on entity with id \n_${entityId}_\n ${arguments.greeting}" + } + } + ] +} +``` +2. Create a webHook behavior in the test interface + +For the behavior's `href` use the slack webHook URL. +``` +POST /cloudapi/1.0.0/interfaces/urn:vcloud:interface:vmware:test:1.0.0/behaviors +``` +```json +{ + "name": "testTemplateWebhookBehaviorSlack", + "execution": { + "type": "WebHook", + "id": "testWebHook", + "href": "https://hooks.slack.com:443/services/T07UZFN0N/B01EW5NC42D/rfjhHCGIwzuzQFrpPZiuLkIX" , + "key": "secretKey", + "execution_properties": { + "template": { + "content": " \n \"blocks\": [\n {\n \"type\": \"section\",\n \"text\": {\n\"type\": \"mrkdwn\",\n\"text\": \"*BEHAVIOR_EXECUTION*:vcda:\\n Behavior with id \\n_${_metadata.behaviorId}_\\n was executed on entity with id \\n_${entityId}_\\n ${arguments.greeting}\"\n}\n }\n ]\n}" + } + } + + } +} +``` +Response: +```json +{ + "name": "testTemplateWebhookBehaviorSlack", + "id": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0", + "ref": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0", + "description": null, + "execution": { + "id": "testWebHook", + "href": "https://hooks.slack.com:443/services/T07UZFN0N/B01EW5NC42D/rfjhHCGIwzuzQFrpPZiuLkIX", + "type": "WebHook", + "key": "secretKey", + "execution_properties": { + "template": { + "content": "<#assign header_Content-Type= \"application/json\" /> { \"blocks\": [ { \"type\": \"section\", \"text\": { \"type\": \"mrkdwn\", \"text\": \"*BEHAVIOR_EXECUTION*:vcda:\n Behavior with id \n_${_metadata.behaviorId}_\n was executed on entity with id \n_${entityId}_\n ${arguments.greeting}\" } } ] }" + } + } + } +} +``` +The template configures the payload to be alligned with what the Slack server expects to receive. The request `Content-Type` header is set to `application/json`. The body of the request is formatted using [Slack webHooks formatting](https://api.slack.com/messaging/webhooks#advanced_message_formatting). + +``` +<#assign header_Content\-Type= "application/json" /> + +{ + + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*BEHAVIOR_EXECUTION*:vcda:\n Behavior with id \n_${_metadata.behaviorId}_\n was executed on entity with id \n_${entityId}_\n ${arguments.greeting}" + } + } + ] +} +``` +3. Trust Slack server SSL certificate + + In order to be able to send data to Slack, the Slack server SSL certificate must be trusted. More information on managing SSL certificates in Cloud Director can be found [here](https://docs.vmware.com/en/VMware-Cloud-Director/10.4/VMware-Cloud-Director-Service-Provider-Admin-Portal-Guide/GUID-80B4CB1C-9353-4EB9-8557-4F6705949D0F.html). + +4. Create a RDE Type implementing the newly created interface + +``` +POST /cloudapi/1.0.0/entityTypes +``` +```json +{ + "name": "testType", + "description": "string", + "nss": "testType", + "version": "1.0.0", + "externalId": "123", + "schema": { + "type": "object", + "properties": { + "application/json": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + }, + "interfaces": ["urn:vcloud:interface:vmware:test:1.0.0"], + "vendor": "vmware", + "readonly": false +} +``` +Response: +```json +{ + "id": "urn:vcloud:type:vmware:testType:1.0.0", + "name": "testType", + "description": "string", + "nss": "testType", + "version": "1.0.0", + "inheritedVersion": null, + "externalId": "123", + "schema": { + "type": "object", + "properties": { + "application/json": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + }, + "vendor": "vmware", + "interfaces": [ + "urn:vcloud:interface:vmware:test:1.0.0" + ], + "hooks": null, + "readonly": false, + "maxImplicitRight": null +} +``` +5. Create a Behavior Access Control for the webHook behavior + + The webHook behavior invocations will be limited to users who have Full Control access to the defined entity the behavior will be invoked on. + +``` +POST /cloudapi/1.0.0/entityTypes/urn:vcloud:type:vmware:testType:1.0.0/behaviorAccessControls +``` +```json +{ + "behaviorId": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0", + "accessLevelId": "urn:vcloud:accessLevel:FullControl" +} +``` +Response: +```json +{ + "behaviorId": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0", + "accessLevelId": "urn:vcloud:accessLevel:FullControl" +} +``` +6. Create an entity of the `testType` RDE type +``` +POST /cloudapi/1.0.0/entityTypes/urn:vcloud:type:vmware:testType:1.0.0 +``` +```json +{ + + "name": "testEntity", + "externalId": null, + "entity": { + "application/json": { + "name": "test" + } + } +} +``` +Response: +``` +Headers: + +Location: https://localhost:8443/api/task/9f1fe9a7-dd5e-4975-a6eb-06502359e2e4 +``` + +To obtain the newly created entity's ID you must retrieve the associated `createDefinedEntity` task from the response headers. The `owner` property holds the entity's ID: + +```json +{ + ... + "owner": { + "otherAttributes": {}, + "href": "", + "id": "urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e", + "type": "application/json", + "name": "entity", + "vCloudExtension": [] + }, + ... +} +``` +7. Resolve newly created entity +``` +POST /cloudapi/1.0.0/entities/urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e/resolve +``` +Response: +```json +{ + "id": "urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e", + "entity": { + "application/json": { + "name": "test" + } + }, + "state": "RESOLVED", + "entityState": "RESOLVED", + "message": null +} +``` +8. Invoke webHook behavior +``` +POST /cloudapi/1.0.0/entities/urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e/behaviors/urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0/invocations +``` +```json +{ + "arguments": { + "greeting": "Greetings from vCloudDirector" + } +} +``` +And in Slack the following message is posted: + +![WebHook behavior invocation's Slack message](../../images/webHook-behavior-slack-example.png) + diff --git a/documentation/images/webHook-behavior-slack-example.png b/documentation/images/webHook-behavior-slack-example.png new file mode 100644 index 0000000..028ed56 Binary files /dev/null and b/documentation/images/webHook-behavior-slack-example.png differ