Skip to content

Instantly share code, notes, and snippets.

@gabrielhurley
Created October 27, 2011 01:41
Show Gist options
  • Save gabrielhurley/1318553 to your computer and use it in GitHub Desktop.
Save gabrielhurley/1318553 to your computer and use it in GitHub Desktop.
A proposal for a new long-term extensible architecture to meet the Horizon projects goals.

Horizon Architecture

"Think simple" as my old master used to say - meaning reduce the whole of its parts into the simplest terms, getting back to first principles.

—Frank Lloyd Wright

In order for Horizon (OpenStack's dashboard project) to be a viable long-term solution it needs to meet several key values:

  • Core Support: Out-of-the-box support for all core OpenStack projects.
  • Extensible: Anyone can add a new component as a "first-class citizen".
  • Manageable: The core codebase should be simple and easy-to-navigate.
  • Consistent: Visual and interaction paradigms are maintained throughout.
  • Stable: A reliable API with an emphasis on backwards-compatibility.
  • Usable: Providing an awesome interface that people want to use.

The only way to attain and uphold those ideals is to define an architectural strategy which makes it easy for the developers to implement those values.

Horizon started life as a single app to manage OpenStack's compute project. As such, all it needed was a set of views, templates, and API calls.

From there it grew to support multiple OpenStack projects and APIs gradually, arranged rigidly into "dash" and "syspanel" groupings.

Finally a first-attempt plugin system was added using signals to add additional URL patterns and links to into the dash and syspanel framework.

This incremental growth has served the goal of "Core Support" phenomenally, but has left "Extensible" and "Manageable" out. The other key values are all taking shape of their own accord but can be further supported with a new architectural understanding.

Note

See the Frequently Asked Questions section for clarification of some of these high-level concepts.

At it's core, Horizon should be a registration pattern for applications to hook into. Here's what that means and how it's implemented in terms of our values:

Horizon will ship with the existing "User Dashboard" and "Syspanel" modules reconstituted as applications which register themselves with Horizon.

The Horizon application will also ship with a set of API abstractions for the core OpenStack projects in order to provide a consistent, stable set of reusable common methods. This would just be a refactor of the existing api.py file into individual files per service.

A Horizon application will be based around a Dashboard class that provides a constistent API and set of capabilities for both core OpenStack apps shipped with Horizon and third-party apps. The Dashboard class will be treated as a top-level navigation item--as "dash" and "syspanel" are currently.

Should a developer wish to provide functionality within an existing dashboard (e.g. adding a monitoring panel to the user dashboard) the simple registration patterns make it possible to write an app which hooks into other dashboards just as easily as creating a new dashboard. All you have to do is import the dashboard you wish to modify.

Within the application, there will be a simple method for registering "panels" (sub-navigation items). Each panel will contain all the necessary logic (views, forms, tests, etc.) for that interface. This granular breakdown prevents files (such as api.py) from becoming thousands of lines long and makes code easy to find by correlating it directly to the navigation.

By providing the necessary core classes to build from, as well as a solid set of reusable templates and additional tools (base form classes, base widget classes, template tags, and perhaps even base class-based views) we can maintain consistency across applications.

By architecting around these core classes and reusable components we create an implicit contract that changes to these components will be made in the most backwards-compatible ways whenever possible.

Ultimately that's up to each and every developer that touches the code, but if we get all the other goals out of the way then we are free to focus on the best possible experience.

At the project level you add Horizon and any desired dashboards to your settings.INSTALLED_APPS:

INSTALLED_APPS = (
    'django',
    ...
    'horizon',
    'horizon.dash',
    'horizon.syspanel',
    'hardware_dashboard', # This is a custom dashboard application
    'my_standalone_monitoring_panel', # This adds a panel to a dashboard
)

Then you add a single line to your project's urls.py:

url(r'', include(horizon.urls)),

Those urls are automatically constructed based on the registered Horizon apps. If a different URL structure is desired it can be constructed by hand.

Pre-built template tags would generate navigation. In your nav.html template you might have the following:

{% load horizon %}

<div class='nav'>
    {% horizon_main_nav %}
</div>

And in your sidebar.html you might have:

{% load horizon %}

<div class='sidebar'>
    {% horizon_dashboard_nav %}
</div>

The dashboard nav could either implicitly render the panel navigation based on a current "active" dashboard in the template context, or it could be explicitly passed a dashboard as an argument.

An application would have the following structure (we'll use syspanel as an example):

syspanel/
|---__init__.py
|---dashboard.py <-----Registers the app with Horizon and sets dashboard properties
|---templates/
|---templatetags/
|---overview/
|---services/
|---images/
    |---__init__.py
    |---panel.py <-----Registers the panel in the app and defines panel properties
    |---urls.py
    |---views.py
    |---forms.py
    |---tests.py
    |---api.py <-------Optional additional API methods for non-core services
    |---templates/
    ...
...

Inside of dashboard.py you would have a class definition and the registration process:

import horizon


class Syspanel(horizon.Dashboard):
    name = "Syspanel" # Appears in navigation
    panels = ('overview', 'services', 'instances', 'flavors', 'images',
              'tenants', 'users', 'quotas',) # Optional to control ordering
    default_panel = 'overview'
    roles = ('admin',) # Provides RBAC at the dashboard-level
    ... # Other common attributes


horizon.register(Syspanel)

By default the Dashboard class would include any Panel obejcts that are registered with them, as in a panels.py:

import horizon

from syspanel import dashboard


class Images(horizon.Panel):
    name = "Images"
    urls = 'syspanel.images.urls'
    roles = ('admin', 'sysadmin',) # Fine-grained RBAC per-panel


# You could also register your panel with another application's dashboard
dashboard.register(Images)

The Panel.urls attribute allows the rollup of url patterns from panels to dashboards to Horizon, resulting in a wholly extensible, configurable URL structure.

The really hard problem lies in adding multi-step workflows that may involve steps from different panels and different apps. These steps may even have different access controls, which leads to the pathological case of a user who can complete all of a workflow except the last step and is left furious.

To solve this problem, Horizon would also provide a Workflow class that handles the high-level definition of the workflow and can be hooked into a panel's URLconf much like Django's class-based views. It would maintain a session-based state for managing data across steps.

Here is a rough usage example:

from django.contrib import messages
from django.core.urlresolvers import reverse

import horizon

class UploadAndLaunchImage(horizon.Workflow):
    """ A two-step workflow that allows the user to upload an image and launch it. """
    # List ALL roles required to complete the workflow
    roles = ('member', 'admin', 'sysadmin',)
    # Views that are the steps of the workflow (in order)
    views = ('syspanel.images.upload', 'dash.images.launch',)

    def complete(self):
        # What to do upon successful completion of the workflow
        return reverse('success_url')

    def cancel(self):
        # Any cleanup that needs to happen for a partial completion
        if self.state['uploaded_image']:
            self.state['uploaded_image'].delete()
        return reverse('cancel_url')

In your panel's urls.py you would then include the following:

from syspanel.images import workflows

patterns = ('',
    url(r'^upload_and_launch/$', workflows.UploadAndLaunchImage.as_view(), name='upload_and_launch'),
)

This way the workflow is picked up in the panel's normal URL structure.

What is the relationship between Dashboards, Panels, and navigation?

The navigational structure would be strongly encouraged to flow from Dashboard objects as top-level navigation items to Panel objects as sub-navigation items as in the current implementation. Template tags would be provided to automatically generate this structure.

That said, you are not required to use the provided tools and can write templates and URLconfs by hand with any desired structure.

Does a panel have to be an app in INSTALLED_APPS?

A panel can live in any Python module. It can be a standalone which ties into an existing dashboard, or it can be contained alongside others within a larger dashboard "app". There is no strict enforcement here. Python is "a language for consenting adults." An module containing a Panel does not need to be added to INSTALLED_APPS, but this would be a common way to load a standalone panel.

Could I hook an external service into a panel using, for example, an iFrame?

Panels are just entry-points to hook views into the larger dashboard navigational structure and enforce common attributes like RBAC. The view and corresponding templates can contain anything you would like, including iFrames.

What does this mean for visual design?

The ability to add an arbitrary number of top-level navigational items (Dashboard objects) poses a new design challenge. Designer Paul Tashima has taken on the challenge of providing a reference design for Horizon which supports this possibility.

Horizon

The OpenStack dashboard project. Also the name of the top-level Python singleton which handles registration for the app.

Dashboard

A Python class representing a top-level navigation item (e.g. "syspanel") which provides a consistent API for Horizon-compatible applications.

Panel

A Python class representing a sub-navigation item (e.g. "instances") which contains all the necessary logic (views, forms, tests, etc.) for that interface.

Workflow

A Python class which defines a multi-step set of tasks which may span multiple panels, dashboards or apps and may require multiple access controls.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment