Cancel Command

Introduction

In versions prior to 7.1.0.0, Universal Extension task instances could be cancelled via the Controller just like any other task type, but the instances do not participate in the Cancellation process. As a result, there is no chance to do any sort of cleanup before Cancellation. Starting with 7.1.0.0, the Universal Extension API was enhanced with a new method called extension_cancel() which allows for any cleanup work before the process is terminated.

On this page, we will cover the following:

  1. Add a new cancel_cleanup_time field to the "UE Task" Universal Template
  2. Add a backing implementation for Cancel Command to the extension.py file.
  3. Build the modified Extension.
  4. Upload the modified Extension.
  5. Demonstrate the Cancel command in three different scenarios
    1. Graceful Cancellation
    2. Timeout
    3. Double Cancel

Step 1 - Add a new "cancel_cleanup_time" field to the "UE Task" Universal Template

Navigate to the "UE Task" Universal Template.

In the "Fields" tab, add a new field called "cancel_cleanup_time" as shown below:

The value of this field will be used to simulate the time spent doing the cleanup work in extension_cancel().

Save the field.

Step 2 - Add a backing implementation for Cancel Command to the extension.py file

Open file ~/dev/extensions/sample-task/src/extension.py in your editor of choice.

Add the implementation of the extension_cancel() method:

reset_environment Dynamic Command
from __future__ import (print_function)
import time
from universal_extension import UniversalExtension
from universal_extension import ExtensionResult
from universal_extension import logger
from universal_extension import ui
from universal_extension.deco import dynamic_choice_command
from universal_extension.deco import dynamic_command


class Extension(UniversalExtension):
    """Required class that serves as the entry point for the extension
    """

    def __init__(self):
        """Initializes an instance of the 'Extension' class
        """
        # Call the base class initializer
        super(Extension, self).__init__()

    @dynamic_choice_command("primary_choice_field")
    def primary_choice_command(self, fields):
        """Dynamic choice command implementation for
        primary_choice_field.

        Parameters
        ----------
        fields : dict
            populated with the values of the dependent fields
        """
        return ExtensionResult(
            rc=0,
            message="Values for choice field: 'primary_choice_field'",
            values=["Start", "Pause", "Stop", "Build", "Destroy"]
        )

    @dynamic_choice_command("secondary_choice_field")
    def secondary_choice_command(self, fields):
        """Dynamic choice command implementation for
        secondary_choice_field.

        Parameters
        ----------
        fields : dict
            populated with the values of the dependent fields
        """
        return ExtensionResult(
            rc=0,
            message="Values for choice field: 'secondary_choice_field'",
            values=["System", "Command", "Application", "Transfer", "Evidence"]
        )

    @dynamic_command("reset_environment")
    def reset_environment(self, fields):
        """Dynamic command implementation for reset_environment command.

        Parameters
        ----------
        fields : dict
            populated with the values of the dependent fields
        """

        # Reset the state of the Output Only 'step' fields.
        out_fields = {}
        out_fields["step_1"] = "Initial"
        out_fields["step_2"] = "Initial"
        ui.update_output_fields(out_fields)

        return ExtensionResult(
            message="Message: Hello from dynamic command 'reset_environment'!",
            output=True,
            output_data='The environment has been reset.',
            output_name='DYNAMIC_OUTPUT')

    @dynamic_command("async_print_word")
    def async_print_word(self, fields):
        """
        Adds each letter of self.WORD to self.async_queue

        If curr_index is odd, then the function will sleep
        for 5 seconds before adding self.WORD to self.async_queue
        """

        curr_index = self.async_index

        if curr_index % 2 != 0:
            self.async_index += 1
            time.sleep(5)
        else:
            self.async_index += 1

        self.async_queue.append(self.WORD[curr_index])

        return ExtensionResult(
            message="",
            output=True,
            output_data="async_print_word()",
            output_name='DYNAMIC_ASYNC_OUTPUT')

    @dynamic_command("sync_print_word")
    def sync_print_word(self, fields):
        """
        Adds each letter of self.WORD to self.sync_queue

        If curr_index is odd, then the function will sleep
        for 5 seconds before adding self.WORD to self.sync_queue
        """

        curr_index = self.sync_index

        if curr_index % 2 != 0:
            self.sync_index += 1
            time.sleep(5)
        else:
            self.sync_index += 1

        self.sync_queue.append(self.WORD[curr_index])

        return ExtensionResult(
            message="",
            output=True,
            output_data="sync_print_word()",
            output_name='DYNAMIC_SYNC_OUTPUT')

    def extension_start(self, fields):
        """Required method that serves as the starting point for work performed
        for a task instance.

        Parameters
        ----------
        fields : dict
            populated with field values from the associated task instance
            launched in the Controller

        Returns
        -------
        ExtensionResult
            once the work is done, an instance of ExtensionResult must be
            returned. See the documentation for a full list of parameters that
            can be passed to the ExtensionResult class constructor
        """

        # Extract the cancel_cleanup_time
        self.cancel_cleanup_time = fields.get('cancel_cleanup_time', 0)

        # Sleep for 45 seconds
        time.sleep(45)

        # Get the value of the 'action' field
        action = fields.get('action', [""])[0]

        if action.lower() == 'print':
            # Print to standard output...
            print("Hello STDOUT!")
        else:
            # Log to standard error...
            logger.info('Hello STDERR!')

        # Return the result with a payload containing a Hello message...
        return ExtensionResult(
            unv_output='Hello Extension!'
        )

    def extension_cancel(self):
        """Optional method that allows the Extension instance to perform
        any cleanup work.
        """
        logger.info('About to sleep for %d seconds' % self.cancel_cleanup_time)
        time.sleep(self.cancel_cleanup_time)
        logger.info('Done with extension_cancel()')

Line 144The cancel_cleanup_time field is extracted from fields and stored in self.cancel_cleanup_time so that it can be accessed in extension_cancel()
Lines 147The extension_start() method is modified to sleep for 45 seconds.
Lines 164-170The extension_cancel() method sleeps for self.cancel_cleanup_time number of seconds. It also logs two statements; one before and one after the sleep time period.

Step 2 - Build and Upload the Extension Zip Archive Using the UIP VS Code Extension

Save all changes to extension.py.

From the VS Code command pallet, execute the UIP: Push command as shown below:

Recall that the UIP: Push command builds and uploads the Extension zip archive


 Click here to expand uip-cli details...

Step 2 Supplemental - Build and Upload the Extension Zip Archive Using the CLI

Save all changes to extension.py.

From the command line, cd to the ~/dev/extensions/sample-task directory and execute the push command as shown below:

Recall that the push command builds and uploads the Extension zip archive

Step 3 - Demonstrate Cancel Command

Before working with the Cancel command, we will need to make sure the cancel timeout value (this is NOT the cancel_cleanup_time field above) is set to 10 seconds. Unless you have explicitly modified the value, it will be 10 seconds by default. Open uags.conf, and if there is an entry for extension_cancel_timeout, make sure it is 10 seconds.

a. Graceful Cancellation

Graceful Cancellation is when the extension_cancel() method finishes before the 10 second Cancel timeout period is up.

  • Navigate to the "ue-task-test" task instance form, and ensure the Cancel Cleanup Time field is set to 0 as shown:
  • Launch the task using the VS Code "UIP: Task Launch" command
  • After about 2-3 seconds, right-click the task instance on the Controller and click "Cancel"
  • Wait until the task status is "Cancelled"
  • The entire process should look similar to:

Notice that both the log statements were printed since the timeout period of 10 seconds did not expire before the extension_cancel() cleanup finished. 

b. Timeout

Timeout is when the extension_cancel() method has not finished before the 10 second Cancel timeout period is up.

  • Navigate to the "ue-task-test" task instance form, and ensure the Cancel Cleanup Time field is set to 15 as shown:
  • Launch the task using the VS Code "UIP: Task Launch" command
  • After about 2-3 seconds, right-click the task instance on the Controller and click "Cancel"
  • Wait until the task status is "Cancelled"
  • The entire process should look similar to:

Notice that only the first log statement was printed. Since the cancel cleanup time value was set to 15 seconds, the Cancel command timed out after 10 seconds, and the Extension process was forcefully terminated.

c. Double Cancel

Double Cancel is when the Extension instance is "Cancelled" twice from the Controller. When the agent receives the second Cancel, it immediately terminates the Extension process regardless of whether the timeout has occurred or not.

  • Navigate to the "ue-task-test" task instance form, and ensure the Cancel Cleanup Time field is set to 15 as shown:
  • Launch the task using the VS Code "UIP: Task Launch" command
  • After about 2-3 seconds, right-click the task instance on the Controller and click "Cancel"
  • Immediately after, right-click the task instance and click "Cancel" once again.
  • The status should immediately transition to "Cancelled"
  • The entire process should look similar to:

Notice that only the first log statement was printed. Since the Extension process was terminated by the Double Cancel before the 15 second sleep period was up, the second log statement did not get printed.

Step 4 - Update the Local template.json

In step 1, we modified the Universal Template by adding the new cancel_cleanup_time field. Recall that inside ~/dev/extensions/sample-task/src/templates, there is a template.json file. This file should correspond to the Universal Template in the Controller. 

Right now, they are not both the same. The Controller's version of template.json has the new field. To grab those changes, use the pull command as shown below:

UIP VS Code Extension

 Click here to expand uip-cli details...

Step Supplemental - CLI


Now, both the local and Controller's version of the Universal Template are the same.