Otter Documentation

Otter SDK

The Otter SDK allows you to create your own custom extensions using .NET code. There are 2 ways to get started with the Otter SDK:

For a complete type reference of the SDK, please visit the Otter SDK Reference.

Writing a Simple Operation

You can easily add new operations to Otter using any .NET language.

Example: Creating an Execute Operation

Otter has a number of different types of operations, the simplest of which to develop is an execute operation. An execute operation does not collect or ensure any configuration, it simply does something, and is generally only used as part of an orchestration plan.

Follow this guide to create a simplified version of the Create File operation that is already included in Otter.

Creating the Project

First, create a new class library project in Visual Studio, and target .NET 4.5. Use NuGet to add a reference to the Inedo.Otter.SDK package.

Make sure that the SDK assemblies added by the NuGet package have the Copy Local property set to False. SDK assemblies should not be included in the extension.

Creating the Operation

Next, create a new public class called CreateFileOperation, and have it inherit the ExecuteOperation class:

public class CreateFileOperation : ExecuteOperation
{
}

Since this operation is supposed to create a file, it would be nice if we could access the file name and what to put in the file as inputs. To do this, just add a couple of properties and attributes:

[Required]
[ScriptAlias("Name")]
[DisplayName("File name")]
public string FileName { get; set; }

[ScriptAlias("Text")]
[DisplayName("Contents")]
[Description("The contents of the file. If this value is missing or empty, a 0-byte file will be created.")]
public string FileText { get; set; }

Here's what everything means:

  • [Required] - the user must supply a value for this property for the plan to validate
  • [ScriptAlias(name)] - this is what the name of the property will look like to OtterScript; this value is required and must be unique to the type of operation
  • [DisplayName(name)] - this is displayed in the graphical plan editor to provide a friendlier name than the script alias; this is optional
  • [Description(text)] - another optional attribute that provides some additional help text in the graphical editor

Now that the inputs are configured, write the code that will actually create the file:

public override async Task ExecuteAsync(IOperationExecutionContext context)
{
	var path = PathEx.Combine(context.WorkingDirectory, this.FileName);
	var fileOps = context.Agent.GetService<IFileOperationsExecuter>();
	this.LogDebug($"Creating {path}...");

	await fileOps.CreateDirectoryAsync(PathEx.GetDirectoryName(path));
	await fileOps.WriteAllTextAsync(path, this.Text ?? "");

	this.LogInformation(this.FileName + " file created.");
}

Although the ExecuteAsync method isn't doing too much, we'll break everything down right here:

  • async
    The async keyword in the method declaration instructs the compiler to allow the await keyword in the method body. Otter operations can execute asynchronously; if you don't know what this means, consider reading this MSDN article. If you would prefer to implement ExecuteAsync synchronously, you can just omit the async keyword and return the static Complete property when the method is complete.
  • var path = PathEx.Combine(context.WorkingDirectory, this.FileName);
    This allows a relative path to be used for the FileName property. Without it, the operation would only work correctly when a user supplies an absolute path. Note that absolute paths will still work; if the second argument of PathEx.Combine is absolute, the first argument is ignored.
  • var fileOps = context.Agent.GetService<IFileOperationsExecuter>();
    This requests a IFileOperationsExecuter service from the Otter agent in the current context. This interface is an abstraction that allows a common set of file system operations on either hosted or SSH agents.
  • await fileOps.CreateDirectoryAsync(PathEx.GetDirectoryName(path));
    This is a simple way to ensure that the directory exists where we are trying to write the file.
  • await fileOps.WriteAllTextAsync(path, this.Text ?? "");
    This actually writes the desired text to the file.

For brevity, we've left out the logging messages; but those are pretty self-explanatory. Anything logged will be associated with the current operation.

Now apply a few attributes to the operation itself to make it discoverable to Otter:

[DisplayName("Create File (Example)")]
[Description("Creates a file on a server.")]
[ScriptAlias("Create-File-Example")]
[ScriptNamespace("HDARS")]
[Tag("files")]
[DefaultProperty(nameof(FileName))]
public class CreateFileOperation : ExecuteOperation
{
}

The meaning of DisplayName, Description, and ScriptAlias is the same as for properties. As for the other attributes:

  • ScriptNamespace - specifies a prefix that is used to qualify the name specified in ScriptAlias. For convenience, this attribute can also be applied to the assembly rather than on each operation individually.
  • Tag - specifies a tag which acts as a kind of category for the operation. This attribute can be applied multiple times and can help with discoverability.
  • DefaultProperty - specifies the name of a property on the operation that receives the default argument value. When in script form, the default argument is the value this is passed to the operation positionally, rather than by name. A default is not necessary, but can help increase the readability of a plan when it is viewed or edited as OtterScript.

Building and Deploying the Extension

With the operation complete (and compiling), it is now ready to be packaged as an extension, and deployed to Otter. To do this, create a zip file with the same name as the assembly. For example, if the assembly name is MyExample.dll, call the zip file MyExample.zip.

Next, add the .dll file to the zip file, then rename the zip file so that its extension is .otterx.

Copy the .otterx file into the Otter extensions directory. By default, this will usually be in C:\ProgramData\Otter\Extensions, but you can verify the exact location by going to the Admin->All Settings page in Otter and looking for the ExtensionsPath value.

Restart the Otter services (and application pool if hosting in IIS).

Testing the Extension

Go to any plan editor in Otter and verify that your new operation is displayed along with all of the other operations. This create file operation should work on any server, just like the built-in version.