Introduction

The Morpheus Plugin API is a Java 8 based library for creating Plugins that add functionality to Morpheus. The Plugin api supports implementing providers to Morpheus of the following types:

  • UI Extensions

  • Task Types

  • IPAM

  • DNS

  • Approvals

  • Cypher Modules

  • Custom Reports

  • Cloud Providers

Release Notes

Note: The morpheus-plugin-api is still pre 1.0 and therefore core interfaces could change between releases. The minimum morpheus version needed to leverage some plugins starts with the 5.2.x series, however anything using plugin core 0.10.0 and above needs to use Morpheus 5.3.3 minimum.

  • 0.10.0 - Cloud Provider Plugin Enhancements, Network Provider Plugin beginnings, Cypher Context accessor Services, Various other bug fixes.

  • 0.9.0 - Progression on Cloud Provider Plugin Service Contexts and Provider Classes, Custom Layouts for Service Catalog Items.

  • 0.8.0 - Overhauled DNS/IPAM Integrations, Reorganized contexts and standardized formats. Added utility classes for easier sync logic. Custom reports, Cloud Providers, Server Tabs, and more. Only compatible with Morpheus version 5.3.1 forward.

  • 0.7.0 - Please note due to jcenter() EOL Don’t use 0.7.0

  • 0.6.0 - Primary Plugin target base version for 5.2.x Morpheus Releases

NOTE: You will have to rebuild any plugins previously against morpheus-plugin-api:0.10.0

Getting Started

Developing plugins for Morpheus is a bit different than simply creating custom tasks or custom library items in Morpheus. It requires some programming experience. We should preface with the fact that Morpheus runs on top of Java. It is primarily written in a dynamic language called Groovy and is based on Groovy version 2.5 currently running within Java 8 (openjdk). The provided plugin api dependency provides a common set of interfaces a developer can implement to dynamically load functionality into the product. These developers are free to develop plugins in native Java or leverage the groovy runtime. Most examples provided are developed and sampled in groovy. It is a great dynamic language that is much less verbose and easier to understand than native java based languages. Plans for supporting additional languages are in the works. Most notably kotlin is being looked into as an alternative development platform. jRuby is also a viable language that can be used as the runtime is included with Morpheus. The plugin architecture is designed based on reactive models using rxJava2. This library follows ReactiveX models for Observer pattern based programming. One unique thing about Observer pattern programming is to remember when writing a call to an Observable the code is not executed until the final subscribe call. If this call is missing, the code will never execute.

NOTE: rxJava3 is a different project build and cannot be used yet in place of rxJava2.

The plugin pipeline leverages Gradle as the build tooling. Gradle is a cross-platform programmatic build tool that is very commonly used and is most notably also used in the android space. To begin make sure your development environment has Gradle 6.5 (at least) as well as Java 8 or Java 11 (if using openjdk11 make sure target compatibility is set to 1.8 within your project).

NOTE: Groovy 2.5.x does not run/compile correctly in the latest java versions. This will change as we move to Groovy 3 in the near term future.

The morpheus-plugin-core git repository is structured such that sample plugins exist underneath the samples directory. They are part of a multi-project gradle project as can be defined in the root directories settings.gradle. This adds a bit of complexity and should be ignored when developing ones own plugin. Simply start with a blank project folder and a simple build.gradle as demonstrated below.

The structure of a Plugin is a typical "fat jar" (aka shadowJar). The plugin will include morpheus-plugin-api along with all the dependencies required to run. Though not required, it is often conventional to use the groovy programming language when developing plugins as can be seen in the samples. Morpheus is based on the groovy runtime and therefore allows full use of groovy 2.5.x.

The Structure of a project often starts with a gradle build along with a Plugin class implementation and a manifest that points to this class. This is the entrypoint for the plugin where all metadata about the plugin is defined as well as all Providers offered by the plugin are registered.

Gradle project

Create a new project with a build.gradle file. We will use the shadowjar plugin to create our "fat jar"

build.gradle
plugins {
    id "com.bertramlabs.asset-pipeline" version "3.3.2"
    id "com.github.johnrengelman.plugin-shadow" version "2.0.3"
}

apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'maven-publish'

group = 'com.example'
version = '1.0.0'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

ext.isReleaseVersion = !version.endsWith("SNAPSHOT")

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'com.morpheusdata:morpheus-plugin-api:0.10.0'
    compileOnly 'org.codehaus.groovy:groovy-all:2.5.6'

    /*
     When using custom libraries, use the gradle `implementation` directive
     instead of `compileOnly`.
     This will allow shadowJar to package the library into the resulting plugin and keep it
     isolated within the same classloader.
     */
}

tasks.assemble.dependsOn tasks.shadowJar

To let the Morpheus plugin manager know what class to load you must specify the class in jar’s manifest:

build.gradle
jar {
    manifest {
        attributes(
            'Plugin-Class': 'com.example.MyPlugin', //Reference to Plugin class
            'Plugin-Version': archiveVersion.get() // Get version defined in gradle
        )
    }
}

When writing plugin code, it is important to note a typical groovy/java project folder structure

ls -R
./
.gitignore
build.gradle
src/main/groovy/
src/main/resources/renderer/hbs/
src/test/groovy/
src/assets/images/
src/assets/javascript/
src/assets/stylesheets/

Be sure to configure your .gitignore file to ignore the build/ directory which appears after performing your first build.

Most of the folder structure can be self-explanatory if familiar with groovy/java. Project packages live within src/main/groovy and contain source files ending in .groovy. View resources are stored in the src/main/resources subfolder and vary depending on the view renderer of choice. While static assets like icons or custom javascript live within the src/assets folder. This is handled by the Asset Pipeline plugin. View rendering and static assets will be covered in more detail later.

Building a project is as simple as calling

./gradlew shadowJar

NOTE: If the gradle wrapper does not yet exist, simply run gradle wrapper within the root of the project to generate the wrapper.

The resulting jar will exist, by default, in the build/libs directory of the project.

Plugin Class

Following the example above, create your plugin class under src/main/java/com/example/MyPlugin.java

Your plugin must extend the com.morpheus.core.Plugin class:

MyPlugin.java
import com.morpheus.core.Plugin;

class MyPlugin extends Plugin {

        @Override
        void initialize() {
                this.setName("My Custom Tabs Plugin");
                CustomTabProvider tabProvider = new CustomTabProvider(this, morpheus);
                this.registerProvider(tabProvider);
        }
}

Here we see a basic plugin that initializes some metadata (the Plugin Name) and registers a custom tab provider.

Registering Providers

A plugin may contain any number of Plugin Providers. A Plugin Provider contains the functionality of your plugin such as a Custom Tab, IPAM, Backup Provider, etc.

There are provided classes such as TaskProvider, ProvisioningProvider, ApprovalProvider, ReportProvider, InstanceTabProvider, ServerTabProvider, and others to get you started building your provider. For example the InstanceTabProvider provides a renderer, show method, and security checking method to make it easy to build your own custom tab on the instance page.

Providers are registered in your plugin during the initialize() method call within the Plugin class as can be seen in the MyPlugin.java sample seen above.

Contexts / Services

The Morpheus context allows you to interact with, query and save data with Morpheus. It is organized into several sub service classes to perform operations that may involve database calls or calling common methods within the Morpheus core that are not available directly. Some calls may be as simple as listById or save. But they can also be as complex as executeSshCommand or executeWinrmCommand. All calls within a Morpheus Context implement RxJava2 conventions. For details on how to program in Reactive syntax and RxJava concepts please see the documentation site http://reactivex.io/.

When interacting with various subcontexts it is helpful to know there is a common guideline on method names involving database calls. These common method names include:

  • listIdentityProjections

  • listById

  • listBy*

  • get

  • findBy*

  • remove

  • create

  • save

There are, of course, exceptions and non ORM related methods that also may exist in certain services that provide common helper methods. For a full listing of the various service classes please check the API Docs and look at the MorpheusContext class. There are several in line with general ORM calls as well as some related to certain actions.

Models

When interacting with Morpheus you must use the models provided in com.morpheusdata.model.*. This allows for the context to be strongly typed. These Models have a few conventions that you might like to know about. Firstly, models that are often synced from a cloud provider or integration of any kind often inherit from their base class known as an IdentityProjection which also inherits from MorpheusModel.

For Example: the NetworkDomain model inherits NetworkDomainIdentityProjection which in turn inherits MorpheusModel.

The IdentityProjection contains a subset of properties that are typically used to match the object with its equivalent on the other side of an Api implementation. This is typically persisted in the externalId property of the object. Some objects also leverage uniqueId as well.

It is also recommended to use the getter and setter methods on the Models (which should be strictly required in the near future) so as to ensure the dirtyProperties map is updated appropriately. This will be used in future distributed worker installations to reduce bandwidth in transmission of data updates back to Morpheus.

HTTP Routing

A plugin may register an endpoint or endpoints to respond with html or json. To register your routes in your plugin you must implement the PluginController class.

Models

Incoming requests come with a ViewModel object populated with the http request & response details.

Example

class MyPluginController implements PluginController {
        List<Route> getRoutes() {
                [
                        Route.build("/myPrefix/example", "html", Permission.build("admin", "full")),
                        Route.build("/reverseTask/json", "json", Permission.build("admin", "full"))
                ]
        }

        def html(ViewModel<String> model) {
                return HTMLResponse.success("Some Text")
        }

        def json(ViewModel<Map> model) {
                Map simpleMap = [serverid: "abc-123", other: model.object.someData]
                return JsonResponse.of(simpleMap)
        }
}

Route provides a builder to allow your plugin to easily build a route with permissions. It takes the url, the method in this class to call, and a list of permissions which can be built with the Permission builder.

The route can either return:

  • HTMLResponse - simple text, or a full rendered view.

  • JsonResponse - an object rendered as json.

After creating a PluginController, register it in your plugin like so:

MyPlugin.java
        @Override
        void initialize() {
                this.setName("My Custom Task Plugin");
                CustomTaskProvider taskProvider = new CustomTaskProvider(this, morpheusContext);
                this.pluginProviders.put(taskProvider.providerCode, taskProvider);

                this.controllers.add(new MyPluginController());
        }

Views

Plugins may render html sections such as adding a tab to different areas of Morpheus which you can populate with your own content. By default a Handlebars template provider is provided by the Plugin Manager. If you wish to use your own template engine you may implement com.morpheusdata.views.Renderer interface.

Rendering a view

Views are stored in your plugin at src/main/resources/renderer/<plugin scope>/<your view>.hbs. Many plugin providers provide a convention to store views that will be rendered.

If you wish to render and return html manually you can call the renderer directly:

getRenderer().renderTemplate("prefix/someview", model);

Do not provide the suffix (.hbs) - you may also pass a com.morpheusdata.views.ViewModel into the view to use when rendering the html.

Asset helper

The template engine can also process static assets such as images, css, and javascript for you. Place your assets under src/assets/{plugin-code}. To include them in the plugin template you can use the {{ asset }} handlebars helper as so:

<script src="{{asset "/instance-tab.js"}}"></script>
<img src="{{asset "/some/image.png"}}" />

Testing

We recommend Spock for easily testing your plugin. The interfaces can easily be mocked and stubbed to allow you to test your integrations without a running Morpheus instance.

Example

class InfobloxProviderSpec extends Specification {
    @Shared MorpheusContext context
    @Shared InfobloxPlugin plugin
    @Shared InfobloxAPI infobloxAPI
    @Shared MorpheusNetworkContext networkContext
    @Subject @Shared InfobloxProvider provider

    void setup() {
        // Create a Mocks of the Morepohus contexts you will use
        context = Mock(MorpheusContextImpl)
        networkContext = Mock(MorpheusNetworkContext)
        context.getNetwork() >> networkContext
        plugin = Mock(InfobloxPlugin)
        infobloxAPI = Mock(InfobloxAPI)

        // Create the actual provider to unit test
        provider = new InfobloxProvider(plugin, context, infobloxAPI)
    }

    void "listNetworks"() {
        given: "A pool server"
        def poolServer = new NetworkPoolServer(apiPort: 8080, serviceUrl: "http://localhost")
        // Here we are stubbing the actual API call to infoblox, but we could create a integration test by actually providing the real infoblox API class instead of a mock.
        infobloxAPI.callApi(_, _, _, _, _, _) >> new ServiceResponse(success: true, errors: null , content:'{"foo": 1}')

        when: "We list the networks"
        def response = provider.listNetworks(poolServer, [doPaging: false])

        then: "We get a response"
        response.size() == 1
    }
}

As you can see, implementing unit and integration testing for your plugins can be done easily with Spock. Of course any other JVM unit testing framework should work as well.

Examples

Task Plugin

Add custom Tasks types to Morpheus. See a full example.

Setup

Tasks are useful components of your provisioning workflow. This plugin allows you to create custom Tasks

  • Create a new class that implements com.morpheusdata.core.TaskProvider

  • Create a new class that extends com.morpheusdata.core.AbstractTaskService. This service defines methods for task execution in a variety of contexts, described below.

Options

OptionType is an easy way to create configuration for your new Task. Simply provide a list of com.morpheusdata.model.OptionType to the TaskProvider.getOptionTypes method.

        @Override
        List<OptionType> getOptionTypes() {
                OptionType optionType = new OptionType(
                                name: 'myTask',
                                code: 'myTaskText',
                                fieldName: 'myTask',
                                optionSource: true,
                                displayOrder: 0,
                                fieldLabel: 'Text to Reverse',
                                required: true,
                                inputType: OptionType.InputType.TEXT
                )
                return [optionType]
        }

Task Contexts

A task can be run in one of three contexts:

  • None/Local (executeLocalTask)

  • Remote (executeRemoteTask)

  • Instance (executeContainerTask, executeContainerTask)

A custom logo can be used in the Morpheus UI by placing an image at src/assets/images/{task-code}.png. Recommended file size is 180 x 60 px.

UI Extensions

Morpheus UI Extension Plugins provide a way to expand the capabilityies of the Morpheus UI. Render custom content as a tab on an Instance, or even on a Server. Create a global injection component to the main layout footer for support/chat services. The possibilities are growing with each release and new functionality since 0.8.0 brings a lot. See a full tabs example.

Setup Instance Tabs

Create a new class that extends com.morpheusdata.core.AbstractInstanceTabProvider. When the Morpheus UI builds the Instance UI it calls the renderTemplate method. Below is a simple example binding the Instance object to the template model.

@Override
HTMLResponse renderTemplate(Instance instance) {
  ViewModel<Instance> model = new ViewModel<>()
  model.object = instance
  getRenderer().renderTemplate("hbs/instanceTab", model)
}
Tip
Use MorpheusContext.buildInstanceConfig to get more details about your Instance. See com.morpheusdata.model.TaskConfig

Handlebars is the default provided template engine. To override this default, implement the com.morpheusdata.core.InstanceTabProvider interface and then write your own getRenderer() method.

Setup Server Tabs

Create a new class that extends com.morpheusdata.core.AbstractServerTabProvider. When the Morpheus UI builds the Server Details UI, it calls the renderTemplate method. Below is a simple example binding the Instance object to the template model.

@Override
HTMLResponse renderTemplate(ComputeServer server) {
  ViewModel<ComputeServer> model = new ViewModel<>()
  model.object = server
  getRenderer().renderTemplate("hbs/serverTab", model)
}
Tip
Use MorpheusContext.buildComputeServerConfig to get more details about your Server. See com.morpheusdata.model.TaskConfig

Handlebars is the default provided template engine. To override this default, implement the com.morpheusdata.core.ServerTabProvider interface and then write your own getRenderer() method.

Templating

See the Views section and the documentation for your templating engine for specific syntax.

Security Policies

User Permissions

Before a template is rendered in the UI, the InstanceTabProvider.show method is called to determine if the current user can view the custom Instance Tab. For example, you may wish to check that the current User has been granted the custom permission defined by your plugin.

@Override
Boolean show(Instance instance, User user, Account account) {
  def show = true
  plugin.permissions.each { Permission permission ->
    if(user.permissions[permission.code] != permission.availableAccessTypes.last().toString()){
      show = false
    }
  }
  return show
}
Content Security Policy

If your custom UI needs to include external resources such as scripts, stylesheets, or frames, you will need to customize the Morpheus Content-Security-Policy Header to allow those elements to be loaded in the browser.

@Override
TabContentSecurityPolicy getContentSecurityPolicy() {
  def csp = new TabContentSecurityPolicy()
  csp.scriptSrc = '*.jsdelivr.net'
  csp.frameSrc = '*.digitalocean.com'
  csp.imgSrc = '*.wikimedia.org'
  csp.styleSrc = 'https: *.bootstrapcdn.com'
  csp
}

=

Reports Plugin

Create custom report types for users to consume within Morpheus. Customize the behavior of how the report data is assembled and generated as well as the way it is rendered/displayed to the user. See a full example.

Setup

Creating a report is just a matter of registering a new implementation of a ReportProvider. If using standard handlebars rendering similar to UI Extensions, simply extend the com.morpheusdata.core.AbstractReportProvider. Before creating a report a few concepts should be made known.

There are a few model objects of importance. Firstly, the ReportProvider implementation always generates a ReportType for reference and display when the user is browsing a report ro run.

After a report is run a ReportResult is generated. This represents the information of who created the report as well as any submitted filters / report options related to the report. When creating a process method the results of the run should be stored as ReportResultRow objects. These have a displayOrder and section. This allows one to store header data as well as line item data for rendering and csv export. These rows are generated using rxjava asynchronous flows in the process method. Example Here:

void process(ReportResult reportResult) {
  morpheus.report.updateReportResultStatus(reportResult,ReportResult.Status.generating).blockingGet();
  Long displayOrder = 0
  List<GroovyRowResult> results = []
  Connection dbConnection

  try {
    dbConnection = morpheus.report.getReadOnlyDatabaseConnection().blockingGet()
    if(reportResult.configMap?.phrase) {
      String phraseMatch = "${reportResult.configMap?.phrase}%"
      results = new Sql(dbConnection).rows("SELECT id,name,status from instance WHERE name LIKE ${phraseMatch} order by name asc;")
    } else {
      results = new Sql(dbConnection).rows("SELECT id,name,status from instance order by name asc;")
    }
  } finally {
    morpheus.report.releaseDatabaseConnection(dbConnection)
  }
  Observable<GroovyRowResult> observable = Observable.fromIterable(results) as Observable<GroovyRowResult>
  observable.map{ resultRow ->
    Map<String,Object> data = [name: resultRow.name, id: resultRow.id, status: resultRow.status]
    ReportResultRow resultRowRecord = new ReportResultRow(section: ReportResultRow.SECTION_MAIN, displayOrder: displayOrder++, dataMap: data)
    return resultRowRecord
  }.buffer(50).doOnComplete {
    morpheus.report.updateReportResultStatus(reportResult,ReportResult.Status.ready).blockingGet();
  }.doOnError { Throwable t ->
    morpheus.report.updateReportResultStatus(reportResult,ReportResult.Status.failed).blockingGet();
  }.subscribe {resultRows ->
    morpheus.report.appendResultRows(reportResult,resultRows).blockingGet()
  }
}

NOTE: Notice that this process method feaures the ability to get a read only database connection to the morpheus MySQL Database. This isnt always the best option but is a good fallbak option for grabbing data you may not otherwise be able to get. Other data query methods are available on the various MorpheusContext subService classes. Expect more of these to be filled out as the plugin ecosystem develops.

Custom Filters

It is often the case that a user may want to adjust how a filter runs. Perhaps they want to reduce the result set to a filtered set of data or group by certain properties. For this, the ReportProvider provides a getOptionTypes method that when implemented allows the developer to specify custom form inputs the user has to select when running the report.

@Override
List<OptionType> getOptionTypes() {
  [new OptionType(code: 'status-report-search', name: 'Search', fieldName: 'phrase', fieldContext: 'config', fieldLabel: 'Search Phrase', displayOrder: 0)]
}

It is important to note the fieldContext should almost always be set to config in this instance.

Rendering

Render is very similar to rendering a tab. The main difference is the payload that is sent for the render is the ReportResult representing the particular report run as well as the dataset rows grouped by section.

@Override
HTMLResponse renderTemplate(ReportResult reportResult, Map<String, List<ReportResultRow>> reportRowsBySection) {
    ViewModel<String> model = new ViewModel<String>()
    model.object = reportRowsBySection
    getRenderer().renderTemplate("hbs/instanceReport", model)
}

Approvals plugin

Integrate Morpheus with your own ITSM solution. See a full example.

Setup

This plugin will enable you to create configuration for several aspects of Approvals within Morpheus.

Integration

A new Integration Type will be created when this plugin is installed. You are able to customize the OptionType for the new Integration using the ApprovalProvider.integrationOptionTypes method. These OptionType will be visible when creating the new Integration in the Morpheus UI (Administration → Integrations).

Policies

Policies (Administration → Policies in the Morpheus UI) define the conditions in which approval is required for provisioning. Custom OptionType can be defined for Policy creation by implementing the ApprovalProvider.policyOptionTypes method.

Create Approval

ApprovalProvider.createApprovalRequest is called after a Provision Request is created. Here is where you can send the request to your ITSM. Each Request will have one or more RequestReference for each resource associated with the provision request.

Important
It is important that you specify an externalId in the RequestResponse and each RequestReference so that Morpheus can track the approval status.

The integrationOptionTypes you specified are available in the method argument Policy.configMap

String itsmEndpoint = accountIntegration.configMap?.cm?.plugin?."itsm-endpoint"

and the policyOptionTypes you specified are available in the method argument AccountIntegration.configMap.

String myPolicyConfigValue = policy.configMap?."my-policy-config-option"

Monitor Approval

At a regular interval, Morpheus checks for Request approvals. In the ApprovalProvider.monitor method define your logic for retrieving a list of approval requests in your ITSM solution.

Approval RequestReference should be returned with one of the following ApprovalStatus:

  • requesting

  • requested

  • error

  • approved

  • rejected

  • cancelled

A custom logo can be used in the Morpheus UI by placing an image at src/assets/images/{plugin-code}.png. Recommended file size is 180 x 60 px.