Morpheus is a hybrid cloud management platform with a plugin SDK that lets you extend almost everything cloud integrations, custom reports, IPAM and DNS providers, backup integrations, UI tabs, global UI hooks. The SDK is documented in pieces, but the day-to-day workflow of going from "I have an idea" to "shadow JAR is running on my appliance" isn't laid out anywhere. This post walks through that workflow.
I'll use a plugin we recently built as the running example: a small enforcement utility that forces every Morpheus user to enable 2FA. If MFA isn't your problem, the example is incidental. What matters is the path.
What you can extend
The SDK exposes more than fifty provider types. A provider is your hook into Morpheus; Morpheus calls back into your code at well defined moments, and you pick the type that matches what you want to do.
A non exhaustive sample, mostly to give a feel for the catalog:
-
CloudProvider,ProvisionProvider— add a new cloud integration -
BackupProvider— register a backup integration -
ReportProvider— add a custom report type -
AppTabProvider,ServerTabProvider,ClusterTabProvider,InstanceTabProvider,NetworkTabProvider— add a tab to a detail page -
GlobalUIComponentProvider— inject HTML into every authenticated page render -
TaskProvider— register a task type for the automation engine -
CypherProvider— extend the secure-vault subsystem -
GuidanceRecommendationProvider,AnalyticsProvider— feed dashboard widgets -
GenericProvider— generic integration for things that don't fit elsewhere
The full list lives at morpheus-plugin-core/morpheus-plugin-api/src/main/java/com/morpheusdata/core/providers/. Picking the right one is the most important early decision; everything else flows from it.
Set up your reference material first
Before writing any code, clone these three repositories locally:
git clone --depth 1 --branch v1.2.x \
https://github.com/HewlettPackard/morpheus-plugin-core.git
git clone --depth 1 \
https://github.com/HewlettPackard/morpheus-docs.git
git clone --depth 1 \
https://github.com/HewlettPackard/morpheus-openapi.git
morpheus-plugin-core is the source of the com.morpheusdata:morpheus-plugin-api library you'll compile against. Provider interfaces, abstract classes, model classes everything is here. When you want to know what a method returns or which fields a model exposes, grep this.
morpheus-docs is the source of the official Morpheus product documentation, currently published as HPE Morpheus VM Essentials on the HPE support portal. When you need to understand a feature from the user's perspective what does Morpheus call this concept, how does the UI organize it this is the reference.
morpheus-openapi is the OpenAPI 3.1 spec for Morpheus's REST API: 600+ endpoint files, 650+ schemas. If your plugin needs to call Morpheus over HTTP, or you need the canonical shape of a data model, this is faster than the live docs site.
Grep is fast; docs search is slow. A local clone pays off within the first hour.
A few web resources are worth bookmarking alongside the local clones:
- developer.morpheusdata.com — Morpheus developer portal landing page; starter guides and links into the rest of the developer materials.
- developer.morpheusdata.com/docs — plugin development handbook, organized by provider type and concept (Reports, Cloud, Tasks, UI Extensions, etc.).
- developer.morpheusdata.com/api — plugin API Javadoc, useful when you want a browsable class hierarchy view instead of grepping the source.
Use them when the local clones don't have what you need; the local clones answer most questions faster.
Pick the right provider type
The methodology is straightforward:
- Write down what you want Morpheus to do.
- Translate that into a question: when does Morpheus need to call my code?
- Find the provider whose method signatures answer that question.
For the MFA enforcer, the chain went:
"Force users without 2FA to enable it before they can use Morpheus."
"We need to be invoked on every page render, with the current user, so we can decide whether to render a blocking overlay."
Match:
GlobalUIComponentProvider, which definesBoolean show(User, Account)andHTMLResponse renderTemplate(User, Account). Morpheus invokes both on every authenticated page render.
If we'd wanted a custom report, we'd have picked ReportProvider. If we'd wanted a tab on the instance detail page, InstanceTabProvider. The question-to-provider mapping becomes mechanical once you know the catalog.
One thing worth knowing up front: not every hook you might want exists. There is no LoginProvider and no AuthenticationProvider — you can't intercept the login flow itself. For the MFA enforcer that meant settling for GlobalUIComponentProvider, which runs after login. The SDK won't always give you exactly what you want; pick the closest match and design around its constraints.
Build environment
The plugin API targets Java 8, but Gradle 7.x needs JDK 11 to build. The shadow plugin used by older Morpheus samples (com.bmuschko.gradle-plugin-shadow 2.0.3) is Gradle 6.x only — use the maintained com.github.johnrengelman.shadow 7.1.2 instead.
On Linux:
curl -fsSL "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 11.0.22-tem
sdk install gradle 7.6.4
cd my-plugin
gradle wrapper --gradle-version 7.6.4 --distribution-type bin
./gradlew shadowJar
A minimal build.gradle:
plugins {
id 'java'
id 'groovy'
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
group = 'com.morpheusdata'
version = '1.0.0'
sourceCompatibility = '11'
targetCompatibility = '11'
repositories { mavenCentral() }
dependencies {
compileOnly 'com.morpheusdata:morpheus-plugin-api:0.15.4'
compileOnly 'org.codehaus.groovy:groovy-all:3.0.11'
compileOnly 'io.reactivex.rxjava3:rxjava:3.1.5'
compileOnly 'org.slf4j:slf4j-api:1.7.26'
}
shadowJar {
manifest {
attributes(
'Plugin-Class' : 'com.example.myplugin.MyPlugin',
'Plugin-Version': archiveVersion.get()
)
}
}
compileOnly is not optional. Morpheus's runtime already provides these libraries; bundling them into your shadow JAR causes classpath conflicts that surface as NoSuchMethodError at load time.
Bumping the version field on every revision is worth doing — the version becomes part of the JAR filename, so you can confirm which build is loaded by glancing at the plugin list in the Morpheus UI.
Anatomy of a plugin
Three pieces.
The Plugin class is your entry point, registered in the JAR manifest as Plugin-Class. It registers your providers in initialize():
class MfaEnforcerPlugin extends Plugin {
@Override String getCode() { 'morpheus-mfa-enforcer-plugin' }
@Override void initialize() {
setName("MFA Enforcer")
def provider = new MfaEnforcerProvider(this, morpheus)
pluginProviders.put(provider.code, provider)
}
}
The Provider class is where the work lives. Always extend the matching Abstract*Provider instead of implementing the interface directly. The abstract classes set up the renderer with the correct classpath prefix, register handlebars helpers (asset, nonce, i18n), and wire up things you don't want to wire up yourself.
@Slf4j
class MfaEnforcerProvider extends AbstractGlobalUIComponentProvider {
Plugin plugin
MorpheusContext morpheusContext
MfaEnforcerProvider(Plugin plugin, MorpheusContext ctx) {
this.plugin = plugin
this.morpheusContext = ctx
}
@Override Boolean show(User user, Account account) {
if (user == null || account == null) return false
if (isMasterTenantAdmin(user, account)) return false
return !isUsing2FA(user)
}
@Override HTMLResponse renderTemplate(User user, Account account) {
def model = new ViewModel<>()
model.object = [userSettingsUrl: '/user-settings']
return getRenderer().renderTemplate("hbs/mfaEnforcer", model)
}
@Override MorpheusContext getMorpheus() { morpheusContext }
@Override Plugin getPlugin() { plugin }
}
The getMorpheus() and getPlugin() overrides are required by PluginProvider and need to be explicit because Groovy's auto-generated getters use the field name (getMorpheusContext, not getMorpheus).
Resources live under src/main/resources/. Handlebars templates go in renderer/hbs/. The renderer's classpath prefix is renderer, so getRenderer().renderTemplate("hbs/mfaEnforcer", model) resolves to src/main/resources/renderer/hbs/mfaEnforcer.hbs.
For inline <style> and <script> tags, use the {{nonce}} helper:
<div id="mfa-shield" role="dialog" aria-modal="true">
<style nonce="{{nonce}}">
#mfa-shield {
position: fixed; inset: 0; z-index: 2147483647;
background: rgba(15, 23, 42, 0.75);
display: flex; align-items: center; justify-content: center;
}
/* ... */
</style>
<div class="card">
<h1>Two-Factor Authentication Required</h1>
<a class="cta" href="{{userSettingsUrl}}" target="_top">
Go to User Settings → Enable 2FA
</a>
</div>
</div>
Without the nonce, Morpheus's Content-Security-Policy header blocks the styles and the script silently. There's no console error, no log entry, no visible failure mode — the page just renders without your component. Easy hours to lose.
The example, end-to-end
A quick note on why this plugin exists, for the curious. Morpheus supports two-factor authentication out of the box, but only as an opt-in toggle on each user's profile there is no appliance wide setting to require it. The product documentation says so explicitly: "There is not currently a way for an administrator to enforce the use of two-factor authentication appliance wide." As an MSP we couldn't rely on every sub-tenant user enabling 2FA on their own, so we wrote a small plugin to close the gap.
MFA enforcer's full source is at emrbaykal/morpheus-plugin-test/morpheus-mfa-enforcer. It's roughly 200 lines split across four files:
morpheus-mfa-enforcer/
├── build.gradle # project config
└── src/main/
├── groovy/com/morpheusdata/mfaenforcer/
│ ├── MfaEnforcerPlugin.groovy # entry point, registers provider
│ └── MfaEnforcerProvider.groovy # show() + renderTemplate() + state lookup
└── resources/renderer/hbs/
└── mfaEnforcer.hbs # overlay HTML + CSS + small JS
The decision logic in show() reads the user's 2FA flag from the Morpheus database through MorpheusReportService.getReadOnlyDatabaseConnection(). The result is cached in a static ConcurrentHashMap with a short TTL — show() is hot, called on every authenticated page render, and uncached SELECTs would generate noticeable load and contention against Morpheus's own writes.
The visible result, in five steps:
A user logs in:
The dashboard loads, but the overlay covers it immediately:
The CTA hard-navigates to /user-settings, where the standard Morpheus 2FA enable flow runs (password confirmation, QR code, authenticator app):
After enabling, the user-settings page reflects the new state:
On the next login, Morpheus prompts for the 2FA code from the authenticator:
And the dashboard loads cleanly the overlay is gone, because show() now reads is_using2fa = true:
Build, package, deploy
./gradlew shadowJar
# build/libs/morpheus-mfa-enforcer-1.0.10-all.jar
Then in the Morpheus UI: Administration → Integrations → Plugins → Choose File. Upload the shadow JAR. The plugin loads on every appliance node, no daemon restart required.
The plugin list in that section shows the version from the JAR filename. Combined with version-bump-on-every-revision, that's all the deployment tracking you need for a small plugin.
A few practical notes
A handful of things that aren't obvious from reading the SDK:
Always extend the matching Abstract*Provider. The abstract classes do non-trivial work — registering handlebars helpers, configuring the renderer's classpath prefix, wiring up locale support. Implementing the bare interface is reinventing this work, and the omissions tend to manifest as silent failures (template not found, CSS blocked by CSP) that are time-consuming to diagnose.
Be careful with hot-path DB access. Any provider invoked per page render needs caching. Without it you add latency to every authenticated request and, in the worst case, contend with Morpheus's own writes via InnoDB share locks. A small ConcurrentHashMap with a short TTL plus an explicit sql.commit() after each SELECT keeps the plugin a good citizen.
/api/* endpoints require an OAuth2 bearer token. Browser session cookies aren't accepted. If your plugin tries to fetch them from inline JavaScript in a template, you'll get 401. For state lookups, do them server-side via the database or via plugin-side service calls.
When something doesn't work, check the Morpheus daemon log first. Browser console second. Plugin failures often produce no console output at all — the symptoms are silent CSP blocks, silent SPA click intercepts, silent fail-open paths. The daemon log is more honest.
Closing
The plugin SDK is more capable than the documentation makes obvious. Once you have the three reference repositories cloned, the build environment set up, and the right Abstract*Provider class identified, the actual work moves quickly.
The same pattern applies to password complexity rules, IP-based access restrictions, role-aware session timeouts, and other guardrails the platform doesn't ship with: pick the provider that matches your hook, extend the abstract class, ship the shadow JAR.
If you've built something interesting with the SDK, drop a link in the comments.






Top comments (0)