Building on Horizon

Gabriel Hurley

Building on Horizon

What are we gonna cover?

You can find a reference implementation of the code being described here on github at https://github.com/gabrielhurley/horizon_demo.

Creating a dashboard

Note

It's perfectly valid to create a panel without a dashboard.

We'll talk about how later.

Structure

Follows the typical Django application layout.

visualizations
  |--__init__.py
  |--dashboard.py
  |--templates/
  |--static/

The dashboard.py module will contain our dashboard class for use by Horizon; the templates and static directories give us homes for our Django template files and static media respectively.

Inside static and templates

Within the static and templates directories it’s generally good to namespace your files:

templates/
      |--visualizations/
    static/
      |--visualizations/
         |--css/
         |--js/
         |--img/

Defining a dashboard

A dashboard class can be incredibly simple (about 3 lines at minimum), defining nothing more than a name and a slug:

import horizon

        class VizDash(horizon.Dashboard):
            name = _("Visualizations")
            slug = "visualizations"
        

In practice, a dashboard class will usually contain more information...

class VizDash(horizon.Dashboard):
            name = _("Visualizations")
            slug = "visualizations"
            panels = ('flocking',)
            default_panel = 'flocking'
            roles = ('admin',)
        

Registering a dashboard

Once our dashboard class is complete, all we need to do is register it:

horizon.register(VizDash)

The typical place for that would be the bottom of the dashboard.py file, but it could also go elsewhere.

Creating a panel

You don't always need a dashboard to create a panel, but when you do, you'd better drink Dos Equis.

Structure

Without a dashboard

flocking/
  |--__init__.py
  |--panel.py
  |--urls.py
  |--views.py
  |--templates/
     |--flocking/
        |--index.html

With a dashboard

visualizations/
|--__init__.py
|--dashboard.py
|--flocking/
   |--__init__.py
   |--panel.py
   |--urls.py
   |--views.py
|--templates/
   |--visualizations/
      |--flocking/
         |--index.html

Defining a panel

The panel.py file has a special meaning.

Inside the panel.py module we define our Panel class:

class Flocking(horizon.Panel):
            name = _("Flocking")
            slug = 'flocking'
        

Simple, right?

Once we’ve defined it, we register it with the dashboard:

from visualizations import dashboard

        dashboard.VizDash.register(Flocking)
        

URLs

You need a urls.py file in your panel directory with a view named index:

from django.conf.urls.defaults import patterns, url
    from .views import IndexView

    urlpatterns = patterns('',
        url(r'^$', IndexView.as_view(), name='index')
    )
    

Tables, Tabs, and Views

Now we get to the really exciting parts; everything before this was structural.

Defining a table

In a tables.py module:

from horizon import tables

class FlockingInstancesTable(tables.DataTable):
host = tables.Column("OS-EXT-SRV-ATTR:host", verbose_name=_("Host")) tenant = tables.Column('tenant_name', verbose_name=_("Tenant")) user = tables.Column('user_name', verbose_name=_("user")) vcpus = tables.Column('flavor_vcpus', verbose_name=_("VCPUs")) memory = tables.Column('flavor_memory', verbose_name=_("Memory")) age = tables.Column('age', verbose_name=_("Age"))
class Meta: name = "instances" verbose_name = _("Instances")

Defining tabs

We have a table, now what...

Let’s make a tab for our visualization:

class VizTab(tabs.Tab):
            name = _("Visualization")
            slug = "viz"
            template_name = "visualizations/flocking/_flocking.html"

            def get_context_data(self, request):
                return None
        

More tabs...

We also need a tab for our data table:

from .tables import FlockingInstancesTable

class DataTab(tabs.TableTab):
    name = _("Data")
    slug = "data"
    table_classes = (FlockingInstancesTable,)
    template_name = "horizon/common/_detail_table.html"
    preload = False
    
def get_instances_data(self): try: instances = utils.get_instances_data(self.tab_group.request) except: instances = [] exceptions.handle(self.tab_group.request, _('Unable to retrieve instance list.')) return instances

Tying it together in a view

We want to handle both tabs and tables... There's a view for that.

from .tables import FlockingInstancesTable
from .tabs import FlockingTabs

class IndexView(tabs.TabbedTableView):
    tab_group_class = FlockingTabs
    table_class = FlockingInstancesTable
    template_name = 'visualizations/flocking/index.html'

Setting up a project

The vast majority of people will just customize the OpenStack Dashboard example project that ships with Horizon.

Structure

A site built on Horizon takes the form of a very typical Django project:

site/
  |--__init__.py
  |--manage.py
  |--demo_dashboard/
     |--__init__.py
     |--models.py  # required for Django even if unused
     |--settings.py
     |--templates/
     |--static/

The key bits here are that demo_dashboard is on our python path, and that the settings.py` file here will contain our customized Horizon config.

The settings file

There are several key things you can customiz in your site’s settings file:

Specifying dashboards

HORIZON_CONFIG = {
    'dashboards': ('nova', 'syspanel', 'visualizations', 'settings',),
}

Error handling

Adding custom error handlers to HORIZON_CONFIG for your API client is easy:

import my_api.exceptions as my_api

'exceptions': {'recoverable': [my_api.Error,
                               my_api.ClientConnectionError],
               'not_found': [my_api.NotFound],
               'unauthorized': [my_api.NotAuthorized]},

Override file

The override file is the “god-mode” dashboard editor.

Comes between the automatic discovery mechanisms and the final setup routines for the entire site.

With great power comes great responsibility.

How to specify an override file

To specify an override file, you set the 'customization_module' value in the HORIZON_CONFIG dictionary to the dotted python path of your override module:

HORIZON_CONFIG = {
    'customization_module': 'demo_dashboard.overrides'
}

Conclusion

The cake was a lie.

If you want to see the finished product, check out the github example referenced at the beginning of this tutorial and linked from the etherpad.

What you’ve learned here are the fundamentals.

Go forth and build!

/

#