Lakshan Perera

Building Modular Web Apps with Rack & Sinatra

Working on OpenNebula's Administration tool in last Google Summer of Code, was one of the best development experiences I had during 2010. The project has been successfully completed and awaiting to be released with a future version of OpenNebula.

In this post, I would like to give some insights on its development, since I believe it stands as a good case study on how to build modular web apps, especially using Rack & Sinatra.

Background

Main objective of OpenNebula's Admin Tool is to enable easy management & monitoring of your OpenNebula cloud setup via a web based interface. Basically, this includes management of users, hosts, virtual networks and virtual machines(VM). It's planned to be extended further to offer features like infrastructure checks, installation and configuration tweaking of an OpenNebula setup (which are already in development).

Also, it is expected to be self hosted, interfacing to an OpenNebula front-end server. It interacts with the OpenNebula using its Ruby API.

In order to achieve these requirements, the application needed to be modular, self-contained and easily customizable. If we used an opinionated framework like Rails, we would be spending majority of the development time on tweaking the framework for the problem domain, rather than focusing on the problem domain itself. However, on the other hand, building such fully featured app from the scratch within a 3-month timeline was not also realistic.

In this background I started exploring the possibilities of using a mini-frameworks(web DSLs), specifically Sinatra. From my mentor, I got to know that they have used Sinatra for certain parts of the OpenNebula project. So it was a safe bet to try for this context.

Since, Sinatra inherently follows the concepts of Rack, its rich middleware stack can be used to bridge the functionality of the apps.

Collection of Mini-Apps

Looking at the overall app, it is composed of loosely coupled resource modules, which have minimal interaction or dependency between them. This made possible to contain each resource module in it's own mini app; which means adding, removing or customization of a module can be done without affecting the behavior of others.

class HostsApp < Sinatra::Base

  #define the model
  require 'models/host'

  #define the views (based on mustache)
  register Mustache::Sinatra
  require 'views/hosts/layout'
  set :mustache, {
    :views => 'views/hosts',
    :templates => 'templates/hosts'
  }

  set :sessions, true

  get '/list', :provides => :json do
    Host.all.to_json
  end

  get '/list' do
    @hosts = Host.all
    @flash = flash

    unless @hosts.include?(:error)
      mustache :list
    else
      puts "Error: "[email protected][:error]
      "<h1>Oops..An error occurred.</h1><p>[email protected][:error]}</p>"
    end
  end
end

Above, is an simplified example of how a mini-app is defined. It extends Sinatra::Base class and follows an explicitly defined MVC pattern. API calls are wrapped in a separate model class, while output generation is done using a Mustache based view templates. So it is basically similar to a controller in Rails.

In above code block, you may notice there are two routes defined for GET /list path. Only difference is one route has a condition: provides. Which means it only responds to requests accepting JSON as the content type. This way we can offer different response types for same resource (i.e. an API) in Sinatra.

Template Rendering

As I mentioned earlier I used Mustache for generating views of the project. This was also the first time I used Mustache and I was really hooked with its flexibility.

Mustache defers from traditional language specific templating schemes, by defining it's own logic-less template format. This makes it possible to reuse the same template on different contexts. For example, in this project I used the same template for server-side rendering with Sinatra and also again on client-side (with JavaScript), when data are loaded via a AJAX.

Exploring Mustache's capabilities in detail would take a post of it's own, so I leave it for a future post.

Routing

In order to form a one high-level application, individual mini-apps with different end-points, needed to be mapped to a single address space.

For this purpose, I used Rack::Mount, library written by Josh Peek, which also powers Rails3' default routing. It simply routes requests to individual Rack apps based on the path.

This is how the route set for the Admin Tool looks like (which I hope is self-explanatory):

# route paths to different apps
Routes = Rack::Mount::RouteSet.new do |set|
  set.add_route UserSessionApp, { :path_info => %r{^/user_session*} }, {}, :user_session
  set.add_route HostsApp, { :path_info => %r{^/host*} }, {}, :host
  set.add_route VirtualNetworksApp, { :path_info => %r{^/vnet*} }, {}, :vnet
  set.add_route VirtualMachinesApp, { :path_info => %r{^/vm*} }, {}, :vm
  set.add_route UsersApp, { :path_info => %r{^/user*} }, {}, :user
  set.add_route DashboardApp, { :path_info => %r{^/$} }, {}, :dashboard

  #public file routes
  set.add_route Rack::File.new(File.dirname(__FILE__) + "/public"), { :path_info => %r{^/public*} }, {}, :public
end

# run the routeset
run Routes

User Authentication

Another important concern of this project was how to enforce user authentication. Admin console access needed to be restricted by the login credentials defined by One Client of OpenNebula.

There are several authentication middleware libraries available for Rack. Out of those, Warden seems to be the most flexible and well documented. Ability to define custom authentication strategies easily, also made it more suitable for our requirement.

This is how the authentication strategy based on one_client was defined using Warden:

Warden::Strategies.add(:password) do
  def valid?
    params["user_name"] || params["password"]
  end

  def authenticate!
    u = get_one_client
    (u.one_auth == "#{params["user_name"]}:#{Digest::SHA1.hexdigest(params["password"])}") ? success!(u) : fail!("Could not log in")
  end
end

Another interesting thing about Warden is it only invokes when we explicitly calls it. Otherwise it just remains as an object in Rack environment, without getting in the way of application execution. In order invoke Warden, we can call it within a before filter in Sinatra. Request processing will continue or halt depending on the authentication result.

before do
  #check for authentication
  unless env['warden'].authenticated?
    session["return_to"] = request.path
    redirect "/user_session/new"
  end
end

Other essential Rack Middleware

There are couple of other Rack middleware, that were used in this project, which provides some of the essential conveniences we have in Rails.

One such middleware is Rack::NestedParams (available in Rack Contrib package), which is used handle nested form parameters properly. Also, Rack::Flash is useful, which gives the option of adding flash messages (success, errors and warnings) to the app.

Source Code

You can view the full source code of the OpenNebula's Admin Tool from its repository at http://dev.opennebula.org/projects/one-admin-tool/repository