LDAP authentication in an Apache fronted Rails app
If you manage anything but the simplest of setups you've probably got an LDAP server setup providing directory services to your network. If you don't you should probably stop reading now ;)
Authenticate using LDAP
The first step to getting your Rails application authenticating using LDAP is to get Apache to authenticate all requests before they reach the application. This stuff is tricky and Apache already has a rather lovely module, mod_authnz_ldap, that does all the heavy lifting for us.
<VirtualHost 193.219.108.xxx:443>
# I've used port 443 above because I'm dealing with passwords.
# [...snip...]
<Directory /var/www/foo.example.com/current/public>
AuthType Basic
AuthName "Foo Application Control Panel"
AuthBasicAuthoritative off
AuthBasicProvider ldap
AuthLDAPUrl ldap://ldap.example.com/ou=people,dc=example,dc=com?userid?one
Require valid-user
</Directory>
# [...snip...]
# Your normal Rails HTTP configuration goes here
</VirtualHost>
Look up the user in Rails
Okay, so any request that hits your application is now authenticated against your LDAP directory. Next, tell Rails to look for the user. For authentication I wrote a rather funky (if I do say so myself) mixin, Xeriom::Acts::ProtectedSystem.
module Xeriom # :nodoc:
module Acts # :nodoc:
module ProtectedSystem # :nodoc:
def self.included(base)
base.send(:extend, ClassMethods)
end
module ClassMethods
def acts_as_protected_system
include InstanceMethods
send(:before_filter, :ensure_user_is_logged_in)
send(:helper_method, :current_user)
send(:helper_method, :logged_in?)
end
end
module InstanceMethods
def ensure_user_is_logged_in
if !logged_in?
authenticate_user
end
end
def logged_in?
!current_user.blank?
end
def current_user
@current_user ||= User.find_by_id(session[:user_id])
end
def current_user=(user)
@current_user = user
session[:user_id] = user.blank? ? nil : user.id
end
def authenticate_user
authenticate_or_request_with_http_basic("Protected Area") do |username, password|
# Lock your application servers down to listen to only
# the web tier or this will kick your ass.
send(:current_user=, User.find_by_username(username))
end
end
end
end
end
end
ActionController::Base.send(:include, Xeriom::Acts::ProtectedSystem)
Like the code licence section in the sidebar says: this code is totally public domain, just don't sue me. To use it just drop the code in your lib/ directory and then call acts_as_protected_system in your ApplicationController.
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
protect_from_forgery # because CSRF sucks!
acts_as_protected_system # lock the door
end
For bonus points...
If you found this article useful, give me some love over at Working With Rails.
Load-balanced highly available MySQL on Ubuntu 8.04
If you followed my previous post about high availability MySQL your application now has one less single point of failure. That's good, but what happens when your MySQL cluster begins to get overloaded? By load-balancing MySQL connections between hosts you can more easily accommodate a larger volume of queries.

Requirements
This article will build on the MySQL cluster introduced in my previous post. If you haven't already, set that up. You'll also need another two virtual machines, each with one IP address.
- 193.219.108.239 - lb-db-01 (lb-db-01.vm.xeriom.net)
- 193.219.108.240 - lb-db-02 (lb-db-02.vm.xeriom.net)
- * 193.219.108.241 - db-01 (db-01.vm.xeriom.net)
- * 193.219.108.242 - db-02 (db-02.vm.xeriom.net)
- * 193.219.108.243 - virtual IP address
IP addresses marked with a * are brought over from the previous article.
All boxes have been firewalled. It's just plain common sense.
We have the technology
Install Heartbeat and MySQL Proxy on both load balancer boxes.
sudo apt-get install heartbeat mysql-proxy --yes
Configure and run MySQL Proxy
Open the firewall on the database boxes to allow the load balancing boxes to connect.
# On db-01 and db-02
sudo iptables -I INPUT 4 -p tcp \
--dport mysql -s lb-db-01.vm.xeriom.net -j ACCEPT
sudo iptables -I INPUT 4 -p tcp \
--dport mysql -s lb-db-02.vm.xeriom.net -j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
If you followed the previous post you'll probably also want to remove the rule that allowed MySQL access from the test box to the floating IP address on the backend boxes. It's not hugely important at the moment, but it's nice to be neat. When you put this into production it will become much more important to control access to the database boxes.
# On db-01 and db-02
sudo iptables -D INPUT -p tcp --dport mysql -s 193.214.108.10 \
-d 193.214.108.243 -j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
Remember to swap 193.214.108.243 for your floating IP address and 193.214.108.10 for your test box IP address or you'll get a "bad rule" error.
You'll also need to open the MySQL port on the load balancer boxes. Note that MySQL Proxy listens on port 4040, not the regular MySQL port 3306. My test box here is 193.219.108.10 - it should be whichever IP address outside the database cluster that you're going to connect from to test the proxy works.
# On lb-db-01
sudo iptables -I INPUT 4 -p tcp \
--dport 4040 -d lb-db-01.vm.xeriom.net -s 193.219.108.10 -j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
# On lb-db-02
sudo iptables -I INPUT 4 -p tcp \
--dport 4040 -d lb-db-02.vm.xeriom.net -s 193.219.108.10 -j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
Run the proxy on both boxes, telling it the address of the real database servers, then try to connect from the test box.
sudo /usr/sbin/mysql-proxy \
--proxy-backend-addresses=db-01.vm.xeriom.net:3306 \
--proxy-backend-addresses=db-02.vm.xeriom.net:3306 \
--daemon
# On the test box
mysql -u some_user -p'some_other_password' -h lb-db-01.vm.xeriom.net
mysql> \q
mysql -u some_user -p'some_other_password' -h lb-db-02.vm.xeriom.net
mysql> \q
You may be told that your load balancer hosts don't have access to the MySQL server. If this happens, login to the MySQL hosts, add a user at the hostname that failed, and try again.
ERROR 1130 (00000): Host 'lb-db-01' is not allowed to connect to this MySQL server
# On db-01 and db-02
mysql -u root -p
Enter password: [Enter your MySQL root password]
mysql> grant all on my_application.* to 'some_user'@'lb-db-01'
identified by 'some_other_password';
mysql> grant all on my_application.* to 'some_user'@'lb-db-02'
identified by 'some_other_password';
mysql> \q
If you got MySQL prompts both times then both proxies are working. Remove the firewall rules that let your test box talk directly to each node and add rules that allow access only to the floating IP address.
# On lb-db-01
sudo iptables -D INPUT -p tcp \
--dport 4040 -d lb-db-01.vm.xeriom.net -s 193.219.108.10 \
-j ACCEPT
sudo iptables -I INPUT 4 -p tcp \
--dport 4040 -d 193.219.108.243 -s 193.219.108.10 \
-j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
# On lb-db-02
sudo iptables -D INPUT -p tcp \
--dport 4040 -d lb-db-02.vm.xeriom.net -s 193.219.108.10 \
-j ACCEPT
sudo iptables -I INPUT 4 -p tcp \
--dport 4040 -d 193.219.108.243 -s 193.219.108.10 \
-j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
Configure and run Heartbeat
Now it's time to configure Heartbeat on both boxes. Open up the firewall and then populate Heartbeat's configuration files.
# On lb-db-01
sudo iptables -I INPUT 4 -p udp \
--dport 694 -s lb-db-02.vm.xeriom.net -j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
# On lb-db-02
sudo iptables -I INPUT 4 -p udp \
--dport 694 -s lb-db-01.vm.xeriom.net -j ACCEPT
sudo sh -c "iptables-save -c > /etc/iptables.rules"
# On both load balancer boxes
sudo cp /usr/share/doc/heartbeat/authkeys /etc/ha.d/
sudo sh -c "zcat /usr/share/doc/heartbeat/ha.cf.gz > /etc/ha.d/ha.cf"
sudo sh -c "zcat /usr/share/doc/heartbeat/haresources.gz > /etc/ha.d/haresources"
The authkeys should be readable only by root because it's going to contain a valuable password.
sudo chmod go-wrx /etc/ha.d/authkeys
Edit /ec/ha.d/authkeys and add a password of your choice so that it looks like below.
auth 2
2 sha1 your-password-here
Configure ha.cf according to your network. In this case the nodes are lb-db-01.vm.xeriom.net and lb-db-02.vm.xeriom.net. To figure out what your node names are run uname -n on each of the nodes. These must match the values you use in the node directives in the configuration file.
logfile /var/log/ha-log
logfacility local0
keepalive 2
deadtime 30
initdead 120
bcast eth0
udpport 694
auto_failback on
node lb-db-01.vm.xeriom.net
node lb-db-02.vm.xeriom.net
Tell Heartbeat that it will be managing the floating IP address with lb-db-01 being the preferred node by editing /etc/ha.d/haresources. Remember that this file must be identical on both boxes.
lb-db-01.vm.xeriom.net 193.219.108.243
If you've had Heartbeat running on the database boxes (as will be the case from the last article) then nuke it now.
# On the database boxes
sudo apt-get uninstall heartbeat
Then remove the alias from eth0 on both boxes.
# On the database boxes
sudo ifconfig eth0 inet 193.219.108.243 -alias
Now we're ready to fire up Heartbeat on the load balancer boxes.
# On lb-db-01 then lb-db-02
sudo /etc/init.d/heartbeat restart
Testing, testing, testing
Fire up mysql on the test box and connect to the floating IP address. You should get the MySQL command prompt.
mysql -u some_user -p'some_other_password' -h 193.214.108.243 my_application
Typing out exactly what is done to test this would take a long time and, largely, would be a waste of space. Here's a summary of the procedure. At all stages you should get a result from your query.
- Run a query such as
show processlist; - Shutdown db-01
- Run the query again
- Start db-01
- Shutdown db-02
- Run the query again
- Start db-02
- Shutdown lb-db-01
- Run the query again
- Shutdown db-01
- Run the query again
- Start db-01
- Shutdown db-02
- Run the query again
- Start db-02
- Start lb-db-01
- Run the query again
If your query ran successfully each time then congratulations, you've now got a load balanced, highly available, MySQL instance.
Where now?
Being highly available and load balanced doesn't protect you from mistakes. Backup often, and check you can restore from your backups. You may be interested in building a MySQL binlog-only server to get point-in-time recovery.
MySQL Proxy talks Lua. Consider learning how to write it.
I've not yet documented how to take the cluster beyond two load balancers and two database nodes. It's possible, but it shouldn't be used as a solution to scaling the setup I've described without some research. Instead of expanding beyond two nodes in a master-master cluster it may be more suitable to setup several master-master nodes and shard or federate your data. It may be that you need to rearrange your schema or play with master-slave replication and do some tricks on the slave to make reads faster. How you scale your database depends on your data and how you use it. Do your homework... and be sure to blog about it and let me know how it goes.
Thinking of a title is the hardest part
If you found this article useful, give me some love over at Working With Rails. If I get 100 points then I get to live.
Avoiding auto_increment collision with High Availability MySQL
If you followed my previous post about high availability MySQL your application now has one less single point of failure. That's good, but as Graeme points out there's a possibility of data collision if the replication process fails.
If replication has stopped and a query inserts into db-01 while a second query inserts into db-02 then the value of any auto_increment columns will be the same. When you get replication running again this will cause a problem.
To avoid this situation we can use auto-increment-increment and auto-increment-offset. These variables affect the way that MySQL generates the next value in an auto-incrementing series.
# On db-01, in /etc/mysql/my.cnf
auto-increment-increment = 10
auto-increment-offset = 1
# On db-02, in /etc/mysql/my.cnf
auto-increment-increment = 10
auto-increment-offset = 2
Restart MySQL on both boxes and you should now be safe from this threat of data collision.
Love me!
If you've found this article useful I'd appreciate beer and / or recommendations at Working With Rails.
Getting started with CouchDB: A simple address book application
I've recently installed CouchDB but, still being pretty new to this whole document store thing, don't really know what they can do or how to make CouchDB do it.
The best way to learn, of course, is to do. I've decided that I'll implement a simple address-book implementation.
Investigation and technology choice
Since CouchDB talks JSON I figure that I'll write the address book in Javascript and HTML, and because CouchDB includes a web server I'll serve the application from the same place I store the data. I'll call the file that contains that addressbook application addressbook.html.
Taking a peek at the CouchDB configuration in /usr/local/etc/couchdb/couch.ini I see that the document root for the web server can be found at /usr/local/share/couchdb/www - that's where the addressbook.html file will go.
I'll need a database to store people's contact details in. There's a pretty nice interface to do this at /_utils/ which is accessible using a web browser by pointing it at the CouchDB server's IP address and port.
CouchDB comes with a Javascript wrapper which can be found at /_utils/script/couch.js but it only talks to a local server and I'm accessing the page across the internet so I'll steal some code from it and change it to work for my setup.
Implementation
First off, create the database. Jump into the interface at /_utils/ and create a database called "addressbook". That's where we'll store our data.
The user interface is going to be a webpage using Javascript which makes things pretty simple. I'll whip up a really simple page to start with.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- The javascript will live in addressbook.js -->
<script src="http://aaa.bbb.ccc.ddd:5984/_utils/addressbook.js"></script>
<title>Address Book</title>
</head>
<body>
<h1>Address Book</h1>
<div id="addressbook">
<p id="loading">Loading... please wait...</p>
</div>
</body>
</html>
Since I've been spoiled by ActiveRecord I want to be able to say something like var people = Person.find("all"); in my code and have it return all Person records. I also want to be able to say Person.find("123456-1234-1234-123456"); to find an individual person.
Person = {
// Push the implementation details of the database into a
// different object to keep Person clean.
//
database: AddressBook,
find: function(id) {
if(id == "all") {
return this.database.allCards();
} else {
return this.database.openCard(id);
}
}
}
I've chosen to implement an AddressBook object that will abstract the details of database connection from the Person object. It will provide two methods, allCards and openCard(id). These methods talk to the CouchDB server and handle any and all data marshalling or other tricky bits and pieces.
AddressBook = {
// Change this to point to your own CouchDB instance.
uri: "http://craig-01.vm.xeriom.net:5984/addressbook/",
_request: function(method, uri) {
var req = new XMLHttpRequest();
req.open(method, uri, false);
req.send();
return req;
},
// Fetch all address book cards.
allCards: function() {
var req = this._request("GET", this.uri + "_all_docs");
var result = JSON.parse(req.responseText);
if (req.status != 200)
throw result;
var allDocs = [];
for(var offset in result.rows) {
var id = result.rows[offset]["id"];
var doc = this.openCard(id);
allDocs[allDocs.length] = doc;
}
return allDocs;
},
// Fetch an individual address book card.
openCard: function(id) {
var req = this._request("GET", this.uri + id);
if (req.status == 404)
return null;
var result = JSON.parse(req.responseText);
if (req.status != 200)
throw result;
return result;
}
}
I push responsibility for parsing JSON off to another library. Luckily, Yahoo provide a rather nice JSON library that does just what I'm looking for - I don't have to implement it, but I do need to pull it into the webpage, and make it appear in the global namespace.
<!-- Add this to the head of addressbook.html -->
<script src="http://yui.yahooapis.com/2.5.2/build/yahoo/yahoo-min.js"></script>
<script src="http://yui.yahooapis.com/2.5.2/build/json/json-min.js"></script>
// Make YUI JSON available in the global namespace.
// Add this to addressbook.js
JSON = YAHOO.lang.JSON;
The last piece of Javascript I need to show the address book is something to load all people from the address book and add them to the page. This uses window.onload hook which is bad, but for this little application is a quick and easy to kick off some code.
// This is horrible, I know, but it's just a simple example.
window.onload = function() {
var addressbook = document.getElementById("addressbook");
var personList = document.createElement("ul");
for(var offset in people) {
var person = people[offset];
var personNode = document.createElement("li");
var name = document.createTextNode(person.name);
personNode.appendChild(name);
personList.appendChild(personNode);
}
addressbook.removeChild(document.getElementById("loading"));
addressbook.appendChild(personList);
}
That's it; the application is ready to go. Upload the addressbook.html and addressbook.js file to the document root of the CouchDB server, fire up your browser and navigate to http://aaa.bbb.ccc.ddd:5984/_utils/addressbook.html where aaa.bbb.ccc.ddd is the IP address of your CouchDB instance.
A blank page that says "Address Book" should greet you. Not very impressive, right? What went wrong? Actually, nothing went wrong. There's just no data in the database.
The interface that I pointed out before for browsing and creating databases can also be used to add documents to the database. Jump into it again, navigate to the addressbook database and add a document. When it asks you for an id, just leave the field blank: it'll create one automatically. Add a field to the document called name and click the little green checkbox beside the textbox, then double click on the value of the new field and set it to your own name in quotes eg "Craig Webster". Click the green arrow beside the textbox then click "save document", jump back to the address book and hit refresh. The new record should now show up.
Moving forward
I've shown how to retrieve data from CouchDB using Javascript, but currently the data still has to be input using the CouchDB interface. Watch this space for an upcoming article on manipulating the database using Javascript so we can add cards to the addressbook.
Did this article help?
If this article helped you, I appreciate beer if you meet me, or recommendations at Working With Rails.
Firewall a pristine Ubuntu 8.04 box
Follow these simple instructions to block all traffic but SSH to your box. Once you have these rules running you can punch more holes as required.
sudo apt-get install iptables
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -p tcp --dport ssh -j ACCEPT
sudo iptables -A INPUT -j DROP
sudo sh -c "iptables-save -c > /etc/iptables.rules"
If you'd like to save your current rules when you stop - or load the rules when you start the box, change your /etc/network/interfaces file so that it contains pre-up and post-down hooks to load / save the rules.
pre-up iptables-restore < /etc/iptables.rules
post-down iptables-save -c > /etc/iptables.rules
If you're hosted at Xeriom Networks and would like to be monitored by the monitoring service there, allow ICMP Type 8 from monitor.xeriom.net.
sudo iptables -I INPUT 4 -s 193.219.108.245 -p icmp -m icmp --icmp-type 8 -j ACCEPT
Remember to save the new rules to the iptables.rules.
sudo sh -c "iptables-save -c > /etc/iptables.rules"
Getting started with Rails 2.0
Rails has changed quite a lot since Agile Web Development with Ruby on Rails (2nd Ed) was released. A number of new best practice techniques are now used in favour of those described in the book.
To demonstrate these techniques it is necessary to have a new Rails application to build upon. In this article I'll cover the basics needed to setup and run a Rails 2 project on your desktop or laptop. Future articles can then build on this base.
In this article
- Installing Rails 2.0
- Starting your Rails 2.0 project
- The importance of version control
- Working with the database: Models
- Creating dynamic pages: Views
- Hooking up the view and the model: Controllers
- What next?
Installing Rails 2.0
Rails 2 now uses SQLite as it's development and test database so you needn't worry about setting up MySQL on the development machine. This makes installing Rails and starting a new project a lot easier: we only have to get Ruby and the Rails code onto our machines. To simplify this even further use MacPorts. If you don't have that installed, go install it now. Remember to install XcodeTools - you can get it from your OS X install media if you didn't install it the first time around.
To install Ruby with MacPorts open Terminal.app (it's in Applications / Utilities) and run sudo port install rb-rubygems. Technically this command will install RubyGems, but since Ruby is a dependency that'll get installed too. Once RubyGems has installed (it might take a while) use that to install Rails by running sudo gem install -y rails.
That's pretty much it. You now have a working Rails install on your development machine.
Starting your Rails 2.0 project
First, to keep your home directory tidy, create an area to keep all your projects under. I call mine sandbox. In the terminal run the command mkdir ~/sandbox to create the sandbox.
Under the sandbox directory we want to create our project. To do this use the rails command, letting it know the name of your project - in this case I call my project QuickBite.
cd ~/sandbox/
rails QuickBite
You'll see a bit out output scroll past as Rails builds a basic project for you. Let's run that Rails app and see what we get.
cd QuickBite
ruby ./script/server
Open a web browser and type http://localhost:3000/ into the address bar.

It's not much, but our project now has a decent starting point. Lets save our progress in case anything bad happens.
The importance of version control
Version control does just what the name suggests: it allows you to control various version of your project. It's useful because it allows you to see when a change was made, what files the change affected, who made it, and it provides you the option of undoing changes that you don't like.
Git!
The version control system I use is Git. It does lots of cool things, but most of them won't be used until later articles. For just now we're only interested in being able to add changes to our projects so we can undo them if something bad happens.
You are of course free to use whichever system you're comfortable with, but the commands given in this article will only cover Git use.
Installing
Installing Git is simple if you use MacPorts. Simply run sudo port install git-core from the Terminal. Grab a cup of coffee, this can take a while.
Configuring Git
Since Git has only just been installed it doesn't know who we are. Let's tell it.
git config --global user.name "Your Full Name"
git config --global user.email "you@domain.com"
This is useful when working in teams as it lets you check who made a change to the code but it's a good idea to get it set up correctly from the get-go. By using --global we tell git that we want to use these values for all projects, not just this one - you only have to set your name and email address once.
Adding your Rails project to Git
Version control is great, but sometimes we don't want to version certain kinds of file. In a file called .gitignore in the top-level directory of your project add the following directives to ignore development and test databases, generated documentation, logs and temporary files that may be generated while running or developing a Rails project.
.DS_Store
db/*.sqlite3
doc/api
doc/app
log/*.log
tmp/**/*
Git is different from most version control systems in that it tracks content rather than files. We've told Git to ignore all content in tmp/ and log/ but we do want those directories to exist - Rails complains if they don't - so you'll need to create two .gitignore files, one in tmp/ and one in log/.
touch tmp/.gitignore
touch log/.gitignore
Now that Git in only paying attention to the files we're interested in we can add the project.
# Create the repository
git init
# Add the project to the next commit
git add .
# Commit the changes
git commit -m "Setup a new Rails application."
Normally we won't add . as it adds all uncommitted files under the current directory to the next commit. It's preferable to pick the files we want to commit (called staging in Git-speak) by using git add <filename> then using git commit -m "Commit Message" to commit just the staged files.
Once that's committed we can start building our application.
Working with the database: Models
In order to make a useful application we need to model the problem domain that the application operates in. Most - though not all - models interact with a database. Models can be found in app/models/, although there won't be any there yet.
The application I've created is called QuickBite. It's going to be an application for exchanging sandwich recipes. Sandwiches usually have a name - BLT, New York Deli, that sort of thing. We can tell Rails about Sandwiches by generating a model.
ruby ./script/generate model Sandwich name:string
If you look at the output of this command you'll notice that it's created a file in app/models/sandwich.rb. That's our sandwich model. The generator has also create some files called migrations. Migrations are commands that tell Rails how to manipulate your database - adding, deleting and renaming tables, columns and indexes. By running the migrations we ask Rails to tell the database what we know about sandwiches.
# Run any pending migrations
rake db:migrate
Now that Rails knows about sandwiches we should commit our changes to the source code repository in case something bad happens. The generator created some tests for us. I'm going to be a bad man and ignore those for just now - I'll cover testing at a later date.
# Look to see what's been changed
git status
# Stage the models, migrations, schema and tests.
git add app/models/ db/migrate/ db/schema.rb test/
# Commit the changes with a message
git commit -m "Added a model to represent sandwiches."
Creating dynamic pages: Views
Rails knows about sandwiches, but that doesn't help us get information into the database or show the people visiting our application what we've added to the database. We want to create a page that allows us to enter sandwich details and to let people see those details.
Pages are made up of several views - in our case we're just going to use one per page to keep things simple. Views live in subfolders of app/views/. If you take a look you'll see that there's already one subfolder there: layouts. Ignore it for now - we'll play with layouts soon, but they're not needed to get a basic application up and running.
Create a file called app/views/sandwiches/new.html.erb containing the following simple form which will allow us to create sandwich records.
<form action="/sandwiches/create">
Name: <input type="text" name="sandwich[name]" />
<input type="submit" value="Save" />
</form>
Now that we have a way of creating records we should allow people to see them. Create a file called app/views/sandwiches/show.html.erb containing the following code.
<p>This sandwich is called <%=h @sandwich.name %>.</p>
There are three important things to notice about the view we've just created.
- ERb output blocks (
<%= ... %>) are used where something - such as the sandwich name - should appear in the output HTML. - The helper method h is used to make sure that the output is properly sanitised HTML ie it doesn't include any nasty HTML tags that could mess up our page.
- The variable we're using starts with an @ symbol. It's not important to understand why at this stage, but it's important to take note of.
Let's try to create a sandwich. Fire up a browser and visit http://localhost:3000/sandwiches/new.

Oh no, an error! Rails doesn't know what we mean by asking for /sandwiches/new. We should tell Rails what to do when we ask for our new sandwich form, but first we should commit our views in case we screw anything up.
# Check what's changed
git status
# Stage the views for the next commit
git add app/views/sandwiches
# Commit the changes
git commit -m "Added create and show views for sandwiches."
Hooking up the view and the model: Controllers
Controllers tell Rails what to do when the view wants to talk to the model - for example when a view sends data as the result of someone hitting the submit button of a form, or when a browser requests a view. Controllers live in app/controllers/. There will already be one there, the application controller, which we will use as a base for our other controllers.
Create a new file called app/controllers/sandwiches_controller.rb
class SandwichesController < ApplicationController
def new
end
def create
@sandwich = Sandwich.create(params[:sandwich])
redirect_to :action => 'show', :id => @sandwich
end
def show
# Notice that this variable starts with an @ to match the view.
@sandwich = Sandwich.find(params[:id])
end
end
Now that we have told Rails what to do when we submit data, fire up a browser and visit the new sandwich form again. This time when you should see your form, and when you hit save you will be shown the sandwich record you've just created as a webpage.

Success! Our application allows us to create sandwiches and share them with visitors. One last time (for this article), save your progress.
# Look to see what's been changed
git status
# Stage the sandwiches controller
git add app/controllers/sandwiches_controller.rb
# Commit the changes with a message
git commit -m "Added a controller for sandwiches."
What next?
You've seen how to install Rails 2 and create a basic project that allows you to create sandwich records and show them to people.
In the next article we'll flesh out some missing functionality by providing a sandwich index so visitors can find sandwiches and a way of editing and deleting sandwiches, and we'll start using layouts and partials to DRY up views and make them look good. Stay tuned!
If you've found this article useful I'd appreciate recommendations at Working With Rails.
You may also be interested in attending my Rails tutorial at Scotland on Rails Charity Day in Edinburgh, Scotland on April 3rd 2008.
I'm talking at Scotland on Rails
For three days (03 April to 05 April 2008) Scotland on Rails will host a ton of cool speakers covering a massive array of interesting subjects.
I'm talking at the charity tutorial day introducing Rails for those that are not already familiar with it. I'll be covering some best-practice techniques in the talk so even if you already know Rails, come join in, spread the love.
There's still time to register though places are limited so be quick!
