Why write a Horizon plugin?

There are many reasons you will want to package your code as a plugin. The most important reason is that it ensures the longevity and reliability of your code. Things are constantly changing in Horizon, but as long as your adhere to the plugin architecture, we guarantee that our releases will not break your plugin.

Plugins are a way to extend and add functionalities. Since you own the code, you are essentially in the driver seat. You can release it when it is ready or keep it private for internal use. Writing your code as a plugin also modularizes your code – making it easier to translate and test. Finally, if you have independent teams actively working on the same code, our plugin architecture can potentially alleviate merge conflicts that your teams will encounter.

Building your plugin

Below is a skeleton of what your plugin should look like. Note that for brevity, we will focus on creating a custom AngularJS step in the images workflow. An AngularJS plugin is a collection of JavaScript files and static resources. Because it runs entirely in your browser, we need to place all of our static resources inside the static folder. This ensures that the Django static collector picks it up and distributes it to the browser correctly.

Plugin structure


myplugin
├─MANIFEST.in
├─setup.py
├─enabled
│   └─_31000_myplugin.py
└─myplugin
  ├─__init__.py
  ├─panel.py
  └─static/app/core/images
     ├─plugins
     │ └─myplugin.module.js
     └─steps/mystep
        ├─mystep.html
        └─mystep.controller.js

panel.py
This file creates a panel that would allow us to hook into Horizon. Hold on a second… why do I need to create a panel if all I want to do is append a simple step to an existing workflow? Unfortunately Horizon uses the Django plugin mechanism underneath and requires a standard file structure. Even though our panel does not show up in the dashboard, we still have to register it so that our other code gets incorporated.


from django.utils.translation import ugettext_lazy as _

import horizon


class MyPanel(horizon.Panel):
    name = _("My Plugin")
    slug = "myplugin"

_31000_myplugin.py
The enabled file contains the configuration that registers our plugin with Horizon. The file is prefixed with an alpha-numeric string that determines the load order. Here we are saying, install myplugin and automatically discover all of my static resources.


  PANEL = 'myplugin'
  PANEL_DASHBOARD = 'project'
  ADD_INSTALLED_APPS = ['myplugin']
  AUTO_DISCOVER_STATIC_FILES = True

myplugin.module.js
The code below illustrates how your code can plug into an existing workflow. In this example, we are adding mystep to an existing create-volume workflow. The most important thing to note is the templateUrl which points to our view below.


(function() {
  'use strict';

  angular
    .module('horizon.app.core.images')
    .run(myplugin);

  myplugin.$inject = [
    'horizon.app.core.images.basePath',
    'horizon.app.core.images.workflows.create-volume.service'
  ];

  function myplugin(basePath, workflow) {
    workflow.steps.push({
      title: gettext('My custom step'),
      templateUrl: basePath + 'steps/mystep/mystep.html',
      formName: 'myStepForm'
    });
  }
})();

mystep.html
This is our view. In this example, we are looping through the list of items provided by the controller and displaying the name and id. The important thing to note is the reference to our controller using the ng-controller directive.


<div ng-controller="horizon.dashboard.project.mystepController as ctrl">
  <div>Loading data from your controller:</div>
  <ul>
    <li ng-repeat="item in ctrl.items">
      <span class="c1">{$ item.name $}</span>
      <span class="c2">{$ item.id $}</span>
    </li>
  </ul>
</div>

mystep.controller.js
The controller is the glue between the model and the view. In this example, we are going to initialize it with some mocked data. This is the data that gets displayed in the view.


(function() {
  'use strict';

  angular
    .module('horizon.app.core.images')
    .controller('horizon.app.core.images.steps.mystepController',
      mystepController);

  function mystepController() {
    var ctrl = this;
    ctrl.items = [
      { name: 'abc', id: 123 },
      { name: 'def', id: 456 },
      { name: 'ghi', id: 789 },
    ];
  }
})();

Deploying Your Plugin

Now that you have a complete plugin, it is time to install and test it. But before we are able to package our plugin, we need to go cover MANIFEST.in and setup.py.

MANIFEST.in
This manifest file tells the packager what files to include. In our manifest, we want to include all of the static content.

include setup.py

recursive-include myplugin/static *

setup.py
This file contains basic information about you and our plugin. I removed the information in classifiers to keep the code compact, feel free to add your own classifiers. Note that you can choose to use pbr and setup.cfg instead, but we will not be covering that in this article.

from setuptools import setup, find_packages

setup(
    name = 'myplugin',
    version = '0.0.1',
    description = 'Workflow Plugin for Horizon',
    author = 'Thai Tran',
    author_email = 'tqtran@us.ibm.com',
    classifiers = [],
    packages=find_packages(),
    include_package_data = True,
)

Finally!

1. Run "cd <plugin> & python setup.py sdist"
2. Run "cp -rv enabled <horizon>/openstack_dashboard/local/"
3. Run "<horizon>/tools/with_venv.sh pip install dist/<package>.tar.gz"
4. Restart Apache

Visit the Horizon dashboard and launch the Angular images workflow. Hopefully you will see the additional step you added in your plugin. Although the example provided in this article is very basic, it highlighted the level of customization that is now available to Horizon developers, something that was previously very hard to do. At the writing of this article, users are able to extend workflows, actions, and table configurations (things like columns, headers, etc...). In the near future, we should be able to use the same architecture to extend forms, search facets, and tabs. To view a working example available on github, click here. For the full write-up, click here.

Join The Discussion

Your email address will not be published. Required fields are marked *