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

  • Network 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.4.x series, however anything using plugin core 0.12.0 and above needs to use Morpheus 5.4.1 minimum. 5.5.x Morpheus versions use 0.13.x plugin-api

  • 0.13.1 - Added Credential Providers support as well as significant CloudProvider refactoring (more to follow)

  • 0.12.5 - Task Providers now have a hasResults flag for result variable chaining.

  • 0.12.4 - IPAM NetworkPoolType filters for handling multiple pool types in one integration. Deprecated reservePoolAddress from IPAMProvider as its no longer needed. Added typeCode to the NetworkPoolIdentityProjection. Added {{nonce}} helper to handlebars tab providers for injecting javascript safely within the Content Security Policies in place.

  • 0.12.3 - Simplification and Polish if IPAM/DNS Interface Implementations (need Morpheus 5.4.4+). Added new ReportProvider helper for easier management of db connection use withDbConnection { connection → }.

  • 0.12.0 - Cloud Provider Plugin Critical Fixes (WIP). Added Plugin settings.

  • 0.11.0 - Cloud Provider Plugin Support. UI Nonce token attribute added for injecting javascript securely and css. Network Provider Plugin support. Create providers for dynamically creating networks and network related objects.

  • 0.10.0 - Custom Report Type Providers have been added.

  • 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

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 3.0.7 currently running within Java 11 (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 but if using newer Gradle make sure you are familiar with the definition changes needed) as well as Java 11 (if using openjdk over 11 make sure target compatibility is set to 1.11 within your project).

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 3.0.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.4.4"
    id "com.github.johnrengelman.plugin-shadow" version "6.0.0"
}

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

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

sourceCompatibility = '1.11'
targetCompatibility = '1.11'

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

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'com.morpheusdata:morpheus-plugin-api:0.12.4' //use 0.13.1 for 5.5.x
    compileOnly 'org.codehaus.groovy:groovy-all:3.0.7'

    /*
     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.

Settings

As of 0.8.0 Plugins can now have settings that can be applied globally after installing a plugin. Some users may use this to configure an integration to a third party service like Datadog, or affect how providers may behave based on some setting. These can be set after uploading a plugin in Administration → Integrations → Plugins.

Settings, much like any other form aspect of a plugin, can take advantage of OptionType and OptionProvider entities to configure how the options are presented to the user and what options are available to choose from.

    class MyPlugin extends Plugin {
        /**
         * Returns a list of {@link OptionType} settings for this plugin.
         * @return this list of settings
         */
        public List<OptionType> getSettings() { return this.settings; }
    }

Implementing the above getter method allows one to specify the form option-types of settings that could be saved by the user.

Fetching setting values for use in a provider can easily be accomplished via the morpheusContext. There is a method that returns a JSON String of the setting values that is up to the plugin developer to deserialize called morpheusContext.getSettings(Plugin plugin). Simply pass the plugin class instance to it and an rxJava Single<String> is returned.

String pluginSettings = morpheus.getSettings(this.plugin).blockingGet()
def pluginDeserialized = new JsonSlurper().parseText(pluginSettings)

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());
        }

HTTP API Client

The morpheus-plugin-api library comes with a utility for facilitating quick and easy API calls to external integrations. This is called teh HttpApiClient and replaces the use of the RestApiUtil.

The developer is, of course, able to utilize any HTTP or SDK/API client they choose within the java ecosystem if so desired. However, this client provides some common functions such as SSL Validation, Throttling, Keep-Alive, Etc. and is based on the Apache HTTP Client.

Tip
Reuse the same client instance when dealing with periodic refresh methods related to caching for best performance. Be mindful that throttling might be necessary to slow down the calls to the service.

Example

Below is an example API Call taken from the Infoblox plugin.

import com.morpheusdata.core.util.HttpApiClient

HttpApiClient infobloxClient = new HttpApiClient()
try {
    def results = infobloxClient.callJsonApi(serviceUrl, apiPath, poolServer.serviceUsername, poolServer.servicePassword, new HttpApiClient.RequestOptions(headers:['Content-Type':'application/json'],
                                                                                                                    queryParams:pageQuery, ignoreSSL: poolServer.ignoreSsl), 'GET')
} catch(e) {
    log.error("verifyPoolServer error: ${e}", e)
} finally {
    infobloxClient.shutdownClient()
}
return rtn

There are several methods to call the api and automatically handle certain payload formats such as callJsonApi, callXmlApi, or plain callApi.

Options can be passed relating to the request via the HttpApiClient.RequestOptions object. Please refer to the java api doc for available options.

Tip
Remember to always shutdown the client after it is used to clean up the connection manager in a try{} finally{} type block

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"}}" />

Option Sources

An Option source is a server side call to load a dynamic dataset for custom form inputs. These normally feed dropdown lists, multiselect components, and typeahead components. These can be very useful when designing task types that have custom options or even custom catalog item layouts.

Getting Started

To get started, simply create an implementation of an OptionSourceProvider. The primary implementation method is called getMethodNames and just returns a list of methods on the class that are accessed via API as datasets.

class GoogleOptionSourceProvider implements OptionSourceProvider {
@Override
    List<String> getMethodNames() {
        return new ArrayList<String>(['googlePluginProjects', 'googlePluginRegions', 'googlePluginZonePools', 'googlePluginMtu'])
    }

    def googlePluginProjects(args) {
        Map authConfig = getAuthConfig(args)
        def projectResults = []
        if(authConfig.clientEmail && authConfig.privateKey) {
            def listResults = GoogleApiService.listProjects(authConfig)
            if(listResults.success) {
                projectResults = listResults.projects?.collect { [name: it.name, value: it.projectId] }
                projectResults = projectResults.sort { a, b -> a.name?.toLowerCase() <=> b.name?.toLowerCase() }
            }
        }
        projectResults
    }
}

Above is an example option source registered for the Google Cloud Plugin. NOTE: Every method must have a singular input argument of type Object (which is the default in Groovy). In reality, it is a Map containing passed in params from the api call that can be referenced as well as the current user object. It is also worth noting that the return type expected is a List<Map<String,String>> whereby the properties on the map are of keys name,value.

Now, when defining your OptionType simply set your optionSource property to the field name defined in your provider. It is important not to conflict names with other plugins so please try to use a unique method name.

Tip
Use a Unique Method name that will not interfere with other plugins the user may load.
OptionType ot4 = new OptionType(
                                name: 'Region',
                                code: 'google-plugin-region',
                                fieldName: 'googleRegionId',
                                optionSource: 'googlePluginRegions', //Note the Option Source Defined here.
                                displayOrder: 3,
                                fieldLabel: 'Region',
                                required: true,
                                inputType: OptionType.InputType.SELECT,
                                dependsOn: 'google-plugin-project-id',
                                fieldContext: 'config'
                )

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.

Tip
The HandlebersRenderer provides a helper called {{nonce}} for injecting into script tags

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 may 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
}

You can also use the per request nonce token to set the attribute on the <script/> tags you may be injecting:

<script src="blah.js" nonce="{{nonce}}"/>

Catalog Layouts Plugin

It is becoming popular for some enterprises and managed service providers to expose simpler options when it comes to provisioning workloads. These could be used to target employees who are not highly technical or to further restrict what someone is allowed to order. Not only can they provision workloads like vms, and containers, but also execute operational tasks created by the administrator. Sometimes, it is necessary to further customize how a catalog detail page looks. There may be special ways of displaying information, or even the order form components need some advanced customization.

This plugin exposes the ability to control everything from the HTML used to render the catalog item, to the javascript that controls the form options. By default, we use a server-side handlebars template renderer however this can be completely customized if so desired.

Setup

Given the advanced nature of this plugin, it may be best to start with the sample plugin provided in the main project repository called samples/morpheus-standard-catalog-layout-plugin. This plugin replicates the embedded layout functionality so acts as a great starting point. It even includes the javascript used for rendering the option types within it.

Tip
Reference the Sample Catalog Layout Plugin before making your own.

The core of the plugin starts with the CatalogItemLayoutProvider which extends the common UIProvider. Most of the UI related plugin types have some commonalities. The primary difference is the command line arguments sent to the render() method.

/**
 * Example TabProvider
 */
class StandardCatalogLayoutProvider extends AbstractCatalogItemLayoutProvider {
        Plugin plugin
        MorpheusContext morpheus

        String code = 'catalog-item-standard'
        String name = 'Standard Catalog Layout'

        StandardCatalogLayoutProvider(Plugin plugin, MorpheusContext context) {
                this.plugin = plugin
                this.morpheus = context
        }

    /**
     * Demonstrates building a TaskConfig to get details about the Server and renders the html from the specified template.
     * @param server details of a ComputeServer
     * @return
     */
        @Override
    HTMLResponse renderTemplate(CatalogItemType catalogItemType, User user) {
        ViewModel<CatalogItemType> model = new ViewModel<>()
        model.object = catalogItemType
        getRenderer().renderTemplate("hbs/standardCatalogItem", model)
    }

}

The render method allows the CatalogItemType model to be passed into a handlebars view for rendering.

The handlebars template, in this case, takes over the rendering of the entire page below the main navigation. It allows inclusion of external assets as well as assets included in the project via asset-pipeline.

<script src="{{asset "/form_manager.js"}}" ></script>
<link rel="stylesheet" type="text/css" href="{{asset "/styles.css"}}">
<script src="{{asset "/templates/plugin-configurable-option.js"}}" ></script>
<div class="page-content">
        <div class="catalog-item-details">
                <div class="item-type-header">
                        <div class="item-type-image">
                                <img class="item-header-img" src="{{catalogTypeImage}}" title="{{name}}" onError="loadImage(this);"/>
                        </div>
                        <h1 class="ellipsize" title="{{name}}">{{name}}</h1>
                        <div class="desc">{{description}}</div>
                </div>
                <div class="catalog-item-body break-container-sm">
                        {{#hasWiki}}
                                <div class="catalog-item-content wiki-content">
                                        {{wiki}}
                                </div>
                        {{/hasWiki}}
                        <div class="catalog-item-configuration {{#hasWiki}}with-item-content{{/hasWiki}}">
                                {{#orderForm}}
                                        <div class="actions text-right">

                                        </div>
                                {{/orderForm}}
                        </div>
                </div>
        </div>
</div>

NOTE: A couple helper methods are registered such as the orderForm block which injects the order form data into the html render.

Tip
Pay special attention to the included javascript files used for rendering options. More often than not, one would want to copy these for use in a custom layout.

Consuming the Plugin

Once a Catalog Layout plugin is compiled and loaded into a Morpheus environment, the layout is automatically made available globally for use when creating a service catalog item in the Blueprints section. Simply edit the catalog item and a new dropdown showing available layouts should be available to choose.

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 = []
  withDbConnection { Connection dbConnection ->
    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;")
    }
  }

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 features the ability to get a read only database connection to the morpheus MySQL Database. This isn’t always the best option but is a good fallback 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. A good example of this is the MorpheusAccountInvoiceService found via the MorpheusCostingService. It enables you to query all invoices just as you would from the api.

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)
}
Tip
When using custom javascript or stylesheets be sure to use the provided {{nonce}} helper to inject the appropriate nonce token for the Content-Security-Policy.

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.

IPAM/DNS Plugins

The IPAM and DNS Provider Plugin interfaces provide an easy means to create direct orchestration for IP Address allocation/release as well as DNS Name server registrations. An IPAM Provider is often capable of implementing both interfaces as they often provide both services at once. It is also possible to independently register a DNSProvider only.

Both the DNS and IPAM Provider plugins typically consist of just 3 parts. First is defining the provider information such as the configuration options for adding the integration as well as pool types it offers. Secondly periodically syncing state data into morpheus back from the remote integration endpoints. This could include just syncing in available pools or zones, all the way to syncing in all IP host records or DNS zone records. This is up to your implementation.

Setup

Before Getting Started, It is recommended to look at the infoblox plugin just to get some bearings. To get started you firstly will create a new class that implements com.morpheusdata.core.IPAMProvider and com.morpheusdata.core.DNSProvider. The Providers requires implementation of several methods including the code required to sync existing records. All sync related code normally lives in here.

Both providers also require implementing CRUD based methods for creating host records, deleting host records, as well as creating zone records and deleting zone records. It is important to note that the host record object allows a user to directly enter an ip address to be requested for allocation or, if none is provided, it should be assumed the next available IP should be acquired. Host records are also special in that there are additional options for simultaneously creating DNS records such as A and PTR records. The additional complexity of tieing these pieces into the automated provisioning of workloads is hidden and taken care of by the Morpheus orchestrator.

Cloud Provider Plugins

The Cloud provider plugin interfaces are among the more complicated plugin types to implement within Morpheus, but when implemented successfully it provides a very powerful method for creating custom clouds or even updating existing cloud functionality. There are two primary concepts that must first be discussed when developing a cloud plugin.

A Cloud plugin typically defines a cloud type (you may see this as zoneType in api for legacy compatibility.) as well as at least one ProvisionProvider. A Provision Provider (also seen in api as provisionType) defines how a resource is provisioned within a cloud. A cloud could offer many provisioning types. For example, Amazon offers both EC2 as well as RDS.

There are several other provider implementations that are needed on more advanced cloud implementations and some are not yet built out. This includes NetworkProvider and BackupProvider implementations as well as a few more.

Setup

Before Getting Started, It is recommended to look at the digital ocean sample plugin just to get some bearings. To get started you firstly will create a new class that implements com.morpheusdata.core.CloudProvider. The CloudProvider requires implementation of several methods including the code required to sync existing workloads from the cloud. All sync related code normally lives in here.

On a cloud implementation there are 2 scheduled jobs for refreshing. There is firstly a 5 minute sync job (typically) that syncs state changes on a periodic basis. Then secondly a daily job that runs at Midnight UTC to do larger syncs like price data or things that may change less frequently. Refer to implementations of the refresh() method as well as the refreshDaily() methods.

Finally one must also provide the option types inputs for configuring the add cloud wizard as well as the ComputeServerType objects available to this zone type. There should be multiple based often on platform and management state.