Create a workflow – tutorial
This chapter shows you how you can structure a workflow based on a simple example that uses the operation and switch states to create a VPN service in Cisco NSO for some simulated devices. We go through the example workflow definition part
by part to give you an idea how you can use different definition components in creating your original workflows.
If you need full information on how workflows can be defined, refer to the Serverless Workflow specification.
Example workflow overview
The goal of this example workflow is to automatically create a VPN service for Cisco NSO devices.
First, we point to the devices in the data input and then try to perform the NSO check-sync operation on them. Then, depending on the result:
-
If a device is not in sync with NSO, we push the device to perform a
sync-from, and only then try to create a VPN for it; -
If it is in sync, we don't perform
sync-frombut directly create a VPN for the device.
If all the steps are executed successfully, CNC Workflow Automation reports workflow execution completion and diplays the
final data input. The results are visible in Cisco NSO, too. If the execution engine encounters errors while performing a
step, it uses the specified retry policy. If errors persist beyond the retry limits, the execution engine ends the execution with a Failed status.
Go through the sections below to understand how data input, functions, states, actions, and data filters are defined.
Provide data input
The workflow definition usually includes some input data at the beginning of the JSON file. While the provided data is not part of the workflow, it is referred to within the workflow definition and can also be updated between states, if such a data update is defined. For more details, see Workflow data input in the Serverless Workflow Specification.
In this example, we'll only need to provide two user-defined deviceName JSON object keys and values, which are the names of the test devices in the local NSO instance, and the nsoResource key, where we specify which CNC Workflow Automation resource we will be using in the workflow. The workflow data input in
JSON should look like this:
{
"device0Name": "ce0",
"device1Name": "ce1",
"nsoResource": "NSOLocal"
}
Define top-level parameters and functions
A workflow definition starts with the required workflow id key. Among other keys, specVersion is also required, defining the Serverless Workflow specification release version. The start key defines the name of the workflow starting state, but it is not required.
In the functions key, you pass in Cisco NSO adapter activity name as function name, adapter activity ID as function operation, and provide the worker name under metadata: worker key:
{
"id": "CreateL3VPN",
"name": "Create Layer3 VPN",
"start": "start",
"version": "1.0",
"functions": [
{
"name": "NSO.RestconfPost",
"metadata": {
"worker": "cisco.nso.v1.0.1"
},
"operation": "cisco.nso.v1.0.1.restconf.Post"
}
],
"description": "Create an L3 VPN for MPLS devices",
"specVersion": "0.9"
}
![]() Note |
Effectively, what you do under |
Specify retry policy
With the retries key, you define the retry policies for state actions in the event that an action fails. Multiple retry policies can be specified
under this key and they are reusable across multiple defined state actions.
"retries": [
{
"name": "Default",
"maxAttempts": 4,
"delay": "PT5S",
"multiplier": 2
},
{
"name": "Custom",
"maxAttempts": 2,
"delay": "PT30S",
"multiplier": 1
}
],
![]() Note |
As you can see, the |
Define states
Workflow states are the building blocks of a workflow definition. In the present quickstart example, we will be using the
operation and switch states, but others are possible. You can check them in detail in the Workflow states section of the Serverless specification.
Operation state
"states": [
{
"name": "start",
"type": "operation",
"stateDataFilter": {
"input": "${ . }"
},
"actions": [],
"transition": {
"nextState": "syncFromOrCreateVPN"
}
}
]
Inside the operation state, apart from state name and type, you define:
-
stateDataFilter: Point to the data input defined at the beginning of the example JSON file. In theinputparameter, we state${ . }, which is a jq expression that means: "use the whole of the data input existing at this point of workflow execution".
Note
For more information on how jq expressions are used in workflows, see the Workflow expressions chapter in the Serverless Workflow specification.
-
actions: Specify the function to be used by the action, and two basicarguments:inputandconfig. Read more in the subsection below. -
transitionorend: Point to the next state to which the workflow should transition after executing the present one. If there are no more steps to be executed, useend.
Switch state
{
"name": "syncFromOrCreateVPN",
"type": "switch",
"dataConditions": [
{
"name": "shouldSyncFrom",
"condition": "${ if (.checkSyncResult0) then .checkSyncResult0 != \"in-sync\" else null end }",
"transition": {
"nextState": "syncFrom"
}
},
{
"name": "shouldCreateVPN",
"condition": "${ if (.checkSyncResult0) then .checkSyncResult0 == \"in-sync\" else null end }",
"transition": {
"nextState": "createVPN"
}
}
]
}
Inside the switch state, apart from state name and type, you define:
-
dataConditions: Define the conditions to be met by a device to be transitioned to a specified next state. You can view theswitchstate as a "gateway" for the workflow, which directs the devices to appropriate states based on their status. Using the jq expression${ if (.checkSyncResult0) then .checkSyncResult0 == \"in-sync\" else null end }in theconditionparameter, we create a boolean value that, if it evaluates totrue, is used to transition the device directly to theCreateVPNstate.
Specify actions
Let's analyse actions on the basis of the checkSync action of the operation state for device ce0.
{
"name": "checkSync",
"retryRef": "Default",
"functionRef": {
"refName": "NSO.RestconfPost",
"arguments": {
"input": {
"path": "restconf/operations/tailf-ncs:devices/device=${ .device0Name }/check-sync"
},
"config": {
"resourceId": "${ .nsoResource }"
}
}
},
"actionDataFilter": {
"results": "${ if (.data) then .data | .\"tailf-ncs:output\".result else null end }",
"toStateData": "${ .checkSyncResult0 }"
}
}
Among the possible parameters, two are especially useful to consider:
-
functionRef: Refer to the function (aka an activity, from the NSO adapter perspective) to be used in action execution. Here, you need to pass in somearguments:-
input:-
path: Point to a path for the adapter to send the request to. -
data: Forward any data to be included in the request (not applicable for thecheckSyncaction).
-
-
config:-
resourceId: Provide the ID of the resource you created for an external service. In the example workflow, the local host and the default port of the Cisco NSO instance is provided. The resource also points to the secret ID, which is used to provide authentication data for an external service. In this case, that will be theusernameandpasswordto the Cisco NSO instance.
-
-
-
actionDataFilter: Define how to process the data passed on in thecheckSyncresponse from NSO:-
results: Use the jq expression"${ if (.data) then .data | .\"tailf-ncs:output\".result else null end }"to handle incoming NSO data. Using the.resultyou cherrypick theresultkey value. In this case (if the device is in the in-sync state), the output of the expression would be"in-sync". -
toStateData: Take the output of the expression defined in theresultsparameter above and save it as a key and value pair inside the workflow input data under any name that you pick. In this case,.checkSyncResult0.
-
Example workflow definition
The following example workflow definition is the end result of the workflow creation process presented in this chapter.
For a complete procedure on how to execute the example workflow in CNC Workflow Automation and get tangible results in Cisco NSO, see the CNC Workflow Automation Getting Started guide.
{
"id": "CreateL3VPN-1.0",
"name": "CreateL3VPN",
"start": "start",
"states": [
{
"name": "start",
"type": "operation",
"actions": [
{
"name": "checkSync",
"retryRef": "Default",
"functionRef": {
"refName": "NSO.RestconfPost",
"arguments": {
"input": {
"path": "restconf/operations/tailf-ncs:devices/device=${ .device0Name }/check-sync"
},
"config": {
"resourceId": "${ .nsoResource }"
}
}
},
"actionDataFilter": {
"results": "${ if (.data) then .data | .\"tailf-ncs:output\".result else null end }",
"toStateData": "${ .checkSyncResult0 }"
}
},
{
"name": "checkSync",
"retryRef": "Default",
"functionRef": {
"refName": "NSO.RestconfPost",
"arguments": {
"input": {
"path": "restconf/operations/tailf-ncs:devices/device=${ .device1Name }/check-sync"
},
"config": {
"resourceId": "${ .nsoResource }"
}
}
},
"actionDataFilter": {
"results": "${ if (.data) then .data | .\"tailf-ncs:output\".result else null end }",
"toStateData": "${ .checkSyncResult1 }"
}
}
],
"transition": {
"nextState": "syncFromOrCreateVPN"
},
"stateDataFilter": {
"input": "${ . }"
}
},
{
"name": "syncFromOrCreateVPN",
"type": "switch",
"dataConditions": [
{
"name": "shouldSyncFrom",
"condition": "${ if (.checkSyncResult0) then .checkSyncResult0 != \"in-sync\" else null end }",
"transition": {
"nextState": "syncFrom"
}
},
{
"name": "shouldCreateVPN",
"condition": "${ if (.checkSyncResult0) then .checkSyncResult0 == \"in-sync\" else null end }",
"transition": {
"nextState": "createVPN"
}
},
{
"name": "shouldSyncFrom",
"condition": "${ if (.checkSyncResult1) then .checkSyncResult1 != \"in-sync\" else null end }",
"transition": {
"nextState": "syncFrom"
}
},
{
"name": "shouldCreateVPN",
"condition": "${ if (.checkSyncResult1) then .checkSyncResult1 == \"in-sync\" else null end }",
"transition": {
"nextState": "createVPN"
}
}
],
"defaultCondition": {
"end": {
"terminate": true
}
}
},
{
"name": "syncFrom",
"type": "operation",
"actions": [
{
"name": "syncFrom",
"retryRef": "Default",
"functionRef": {
"refName": "NSO.RestconfPost",
"arguments": {
"input": {
"path": "restconf/operations/tailf-ncs:devices/device=${ .device0Name }/sync-from"
},
"config": {
"resourceId": "${ .nsoResource }"
}
}
},
"actionDataFilter": {
"results": "${ if (.data) then .data | .\"tailf-ncs:output\".result else null end }",
"toStateData": "${ .syncFromResult0 }"
}
},
{
"name": "syncFrom",
"retryRef": "Default",
"functionRef": {
"refName": "NSO.RestconfPost",
"arguments": {
"input": {
"path": "restconf/operations/tailf-ncs:devices/device=${ .device1Name }/sync-from"
},
"config": {
"resourceId": "${ .nsoResource }"
}
}
},
"actionDataFilter": {
"results": "${ if (.data) then .data | .\"tailf-ncs:output\".result else null end }",
"toStateData": "${ .syncFromResult1 }"
}
}
],
"transition": {
"nextState": "createVPN"
}
},
{
"end": {
"terminate": true
},
"name": "createVPN",
"type": "operation",
"actions": [
{
"name": "createVPN",
"retryRef": "Custom",
"functionRef": {
"refName": "NSO.RestconfPost",
"arguments": {
"input": {
"data": "{\"l3vpn\":[{\"name\":\"testnetwork\",\"route-distinguisher\":250,\"endpoint\":[{\"id\":\"boffice\",\"ce-device\":\"ce0\",\"ce-interface\":\"GigabitEthernet0/5\",\"ip-network\":\"10.8.9.0/24\",\"bandwidth\":4500000},{\"id\":\"hoffice\",\"ce-device\":\"ce1\",\"ce-interface\":\"GigabitEthernet0/5\",\"ip-network\":\"192.168.9.0/32\",\"bandwidth\":4500000}]}]} ",
"path": "restconf/data/l3vpn:vpn"
},
"config": {
"resourceId": "${ .nsoResource }"
}
}
},
"actionDataFilter": {
"results": "${ if (.status) then .status else null end }",
"toStateData": "${ .createServiceResult }"
}
}
]
}
],
"retries": [
{
"name": "Default",
"delay": "PT30S",
"multiplier": 2,
"maxAttempts": 4
},
{
"name": "Custom",
"delay": "PT10S",
"multiplier": 1,
"maxAttempts": 2
}
],
"version": "1.0",
"functions": [
{
"name": "NSO.RestconfPost",
"metadata": {
"worker": "cisco.nso.v1.0.3"
},
"operation": "cisco.nso.v1.0.3.restconf.Post"
}
],
"description": "",
"specVersion": "0.9"
}
Feedback