-
Notifications
You must be signed in to change notification settings - Fork 55
Passenger Ruby Report App Tutorial
**Note: this is a DRAFT of a tutorial that will be moved to **
TODO
We will be starting with an example status app that displays the output of the ps command:
screenshot
We will be modifying this app to instead display the formatted output of the quota command:
screenshot
Login to Open OnDemand, click "Develop" and "My Sandbox Apps (Development)". Click "New Product" and "Clone Existing App". We will create an app that displays the formatted output of the quota command.
screenshot
- Directory name: quota
- Git remote: https://github.com/OSC/ood-example-ps
- Check "Create new Git Project from this?"
- Click Submit
screenshot
Launch the app by clicking the large blue Launch button and in a new browser window/tab you will see the output of a ps command filtered using grep.
screeenshot
The features of this app include:
- The app uses the custom branded Bootstrap 3 that My Jobs ande ActiveJobs apps use.
- The navbar contains a link back to the dashboard.
- On a request, the app runs a shell command, parses the output, and displays the result in a table.
This serves as a good starting point for any status app to build for OnDemand, because
- the app has the branding matching other OnDemand apps
- all status apps will do something similar on a request to the app:
- get raw data from a shell command or http request
- parse the raw data into an intermediate object representation
- use that intermediate object representation to display the data formatted as a table or graph
Go back to the Dashboard browser window/tab where the quota details page is displayed. Click the Files button to open this app in the File Explorer. Notice the structure of the app. It is a Ruby Passenger app that uses the Sinatra web framework:
-
config.ru
is the entry point for the app (as is for all Ruby Passenger apps) - the
Gemfile
andGemfile.lock
specify the Ruby gem dependencies, and those dependencies are installed in vendor/bundle -
public/
contains static css and js files. everything underpublic/
is automatically served up by NGINX -
views/
contains index.html which is the template for the body of the index page, and thelayout.html
which contains the erb tag<%== yield %>
that is replaced with the rendered contents ofindex.html
Select config.ru
and click "Edit" to open in the File Editor app to view. Here is part of the config.ru
file that defines a single route for '/':
get '/' do
# Define your variables that will be sent to the view.
@title = "Currently Running OnDemand Passenger Apps"
@command = "ps aux | grep App | grep -v grep"
# Run the command and capture the stdout, stderr, and exit code as separate variables.
stdout_str, stderr_str, status = Open3.capture3(@command)
# Parse the stdout of the command and set the resulting object array to a variable.
@app_processes = parse_ps(stdout_str)
# If there was an error performing the command, set it to an error variable.
@error = stderr_str unless status.success?
# Variables will be available in views/index.erb
erb :index
end
This does 3 things:
- Sets several instance variables for things like the title and the command to run
- Executes a shell command, calls a helper method to parse the output, and sets the resulting array of structs to an instance variable
- Renders the
views/index.erb
which makes uses of the instance variables to render the webpage
The parsing of the command's output is handled by a method that accepts a string and returns an array of structs. The struct has an attribute for each column of the command's output.
AppProcess = Struct.new(:user, :pid, :pct_cpu, :pct_mem, :vsz, :rss, :tty, :stat, :start, :time, :command)
helpers do
def parse_ps(ps_string)
ps_string.split("\n").map { |line| AppProcess.new(*(line.split(" ", 11))) }
end
end
The rendering of views/index.erb`
is done using an alternative ERB
implementation that by default escapes any HTML characters in the output of the
ERB tags. `See erubi for details<https://github.com/jeremyevans/erubi>`_.The
views/index.erb
displays the command used:
<pre>
$ <%= @command %>
</pre>
And then creates an HTML table to display the results, iterating over the array of structs the parsing method created and creating one row for each struct:
<% @app_processes.each do |app| %>
<tr>
<td><%= app.user %></td>
<td><%= app.pid %></td>
<td><%= app.pct_cpu %></td>
<td><%= app.pct_mem %></td>
<td><%= app.vsz %></td>
<td><%= app.rss %></td>
<td><%= app.tty %></td>
<td><%= app.stat %></td>
<td><%= app.start %></td>
<td><%= app.time %></td>
<td><%= app.command %></td>
</tr>
<% end %>
-
Update the route "/" to execute the quota command:
get '/' do # Define your variables that will be sent to the view. - @title = "Currently Running OnDemand Passenger Apps" - @command = "ps aux | grep App | grep -v grep" + @title = "Quota" + @command = "quota -spw" # Run the command and capture the stdout, stderr, and exit code as separate variables. stdout_str, stderr_str, status = Open3.capture3(@command) # Parse the stdout of the command and set the resulting object array to a variable. - @app_processes = parse_ps(stdout_str) + @volumes = parse_quota(stdout_str)
We use
quota -spw
because it will be easier to parse. -
Replace the parse_ps method and AppProcess struct with parse_quota method and Volume struct:
# A Struct is used to map each stdout column to an attribute. # There are 9 columns for each line of output from the quota command. Volume = Struct.new(:name, :blocks, :blocks_quota, :blocks_limit, :blocks_grace, :files, :files_quota, :files_limit, :files_grace) helpers do # This command will parse a string output from the `quota -spw` command and map it to # an array of Volume objects. # # Example output of the quota command: quota -spw # Disk quotas for user efranz (uid 10851): # Filesystem blocks quota limit grace files quota limit grace # 10.11.200.31:/PZS0562/ 99594M 500G 500G 0 929k 1000k 1000k 0 def parse_quota(quota_string) lines = quota_string.split("\n") lines.drop(2).map { |line| Volume.new(*(line.split)) } end end
-
Update
views/index.erb
to iterate over the Volume array and create table rows:<table class="table table-bordered"> <tr> <th>Volume</th> <th>Blocks</th> <th>Blocks Quota</th> <th>Blocks Limit</th> <th>Blocks Grace</th> <th>Files</th> <th>Files Quota</th> <th>Files Limit</th> <th>Files Grace</th> </tr> <% @volumes.each do |volume| %> <tr> <td><%= volume.name %></td> <td><%= volume.blocks %></td> <td><%= volume.blocks_quota %></td> <td><%= volume.blocks_limit %></td> <td><%= volume.blocks_grace %></td> <td><%= volume.files %></td> <td><%= volume.files_quota %></td> <td><%= volume.files_limit %></td> <td><%= volume.files_grace %></td> </tr> <% end %> </table>
Go back to the dashboard in the details view of the quota app. Click "Restart App" to force Passenger to restart the app the next time you access it. Then click the blue launch button to open the app again in a new window.
FIXME: should we be showing an error and how to fix it? and then restart again?
TODO
- Finish explaining how the ps app works
- Show what lines to change and how to restart the app to see the changes (so the functionality is working).
- (bonus) Add sparklines js file to the project and then update the table to show this.
- Update the manifest.yml for branding purposes, and explain how to see all of the Font Awesome icons
- Show publishing an app, including specifying its category (which should appear in the app details, along with specifying whether this is in the navbar whitelist) and configuring the whitelist for a new navbar menu.