Monday, November 30, 2009

Installing FreeBSD 8 using PXE and NFS mount

There are a few other blogs about this very topic, however, none I could find that would work perfectly for me. So I decided to gather some notes from the recent install of our development environment, a couple of SunFire X2200's.

The two blogs that really helped me and contained really well written information are:
Jeremy Chadwick and from High5!

If these instructions aren't working 100%, its worth going through these two blogs as well to get some extra insight.

Hardware wise you will need:

1. A Server, the machine with FreeBSD already installed. You can use other OS's as well, but then some commands and config files might differ.
2. A Client, The machine that you want to install FreeBSD onto.
3. Cross over cable to link the two, or else both located on the same subnet
4. Monitor on the client for your first install, it handy so see error messages

In order, here is what we will be going through together:

1. Set Server NIC to static IP address
2. Install and configure DHCP server
3. Configure TFTP
4. Configure NFS
5. Copy FreeBSD CD to NFS
6. Configure loader.conf of PXEBOOT
7. Workaround for a bug in mfs_root
8. Boot PXE
9. Installation
10. Problems and solutions


1. Set server NIC to static IP Address



First, we need to get the name of the network card. Type in the following command, and you will get similar output.

# ifconfig
msk0: flags=8863 mtu 1500
ether 00:17:f2:50:4a:d4
media: autoselect () status: inactive
supported media: autoselect
lo0: flags=8049 mtu 16384
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128

Now open /etc/rc.conf and add the following lines to set IP Address and subnet mask

ifconfig_msk0="inet 192.168.1.1 netmask 255.255.255.0"

Notice, now ifconfig_msk0 matches the name of the card. This is standard convention, you can read the doc's on it later!

2. Install and configure DHCP Server



Using Ports on FreeBSD, its quite easy to install a DHCP server
You will need to look up the documentation, if you don't already have ports installed on your machine. To install -

pkg_add -r isc-dhcp31-server

Now we should create /usr/local/etc/dhcpd.conf with the following config.

allow booting;
allow bootp;
default-lease-time 600;
max-lease-time 7200;
authoritative;
ddns-update-style ad-hoc;
log-facility local7;
option domain-name "home.lan";
option domain-name-servers 192.168.1.1;
server-name "192.168.1.1";
server-identifier 192.168.1.1;
next-server 192.168.1.1;
option routers 192.168.1.1;
option root-path "192.168.1.1:/usr/local/freebsd8";
filename "freebsd8/boot/pxeboot";

subnet 192.168.1.0 netmask 255.255.255.0 {
range 192.168.1.10 192.168.1.11;
}

The IP address is the static IP address of the servers network card which we set in the last step.
filename "freebsd8/boot/pxeboot" - This is the file that PXE will serve when requested

option root-path - This tells pxeboot where the root filesystem will be mounted from

3. Configure TFTP


In /etc/inetd.conf, find the line similar to

tftp dgram udp wait root /usr/libexec/tftpd tftpd -l -s /usr/local

Uncomment it, and change it so -s flag points to /usr/local

Add the following line to /etc/rc.conf

inetd_enable="YES"

Now you can start the service, which will also start tftpd

# /etc/rc.d/inetd start

4. Configure NFS


Add the following to /etc/rc.conf

rpcbind_enable="yes"
mountd_enable="yes"
nfs_server_enable="yes"

You will need to add an NFS export to /etc/exports

/usr/local/freebsd8 -alldirs -maproot=root 192.168.1.10

The ip address here, is the ipaddress of the client machine, so that it will have access to the nfs mount.
-maproot=root is required so that the client machine can log in as root if required.

Now lets make the exported directory so we can put the FreeBSD files there

# mkdir /usr/local/freebsd8
# chmod 755 /usr/local/freebsd8

Now we can start all the above services

/etc/rc.d/rpcbind start
/etc/rc.d/mountd start
/etc/rc.d/nfsd start

To see check if the mount is available,

# showmount-e
Exports list on localhost:
/usr/local/freebsd8 192.168.1.0

5. Copy FreeBSD 8 contents to the NFS mount



There are two ways of doing this,
i) copy from CD, if you have one
ii) download the iso and extract it

i) Copy from CD

# mount -t cd9660 /dev/acd0c /mnt
# cp -pR /mnt/* /usr/local/freebsd8

ii) Download ISO
Make sure your getting the correct architecture ISO.

# fetch ftp://ftp.freebsd.org/pub/FreeBSD/ISO-IMAGES-amd64/8.0/8.0-RELEASE-amd64-disc1.iso
# tar -C /usr/local/freebsd8 -pxvf 8.0-RELEASE-amd64-disc1.iso

6. Configure loader.conf



/usr/local/freebsd7/boot/loader.conf needs to be updated to the client will know where to mount root from

# echo 'vfs.root.mountfrom="ufs:/dev/md0"' >> /usr/local/freebsd7/boot/loader.conf

This will tell the client machine where to mount the image from. A number of blogs suggest that you should use 'md0c' rather than 'md0', the 'c' is a convention to start at the first block of the mount. When I did this, I had troubles getting the sysinstall programming running on the client. When I removed it, it worked without any problems.

7. Workaround for bug in mfs_root



There is a bug in the mfs_root loader, where if it is gzipped, then it will make the clients reboot. Luckily its well documentated on the internet, so I avoided that problem. Here is the quick fix:

# cd /usr/local/freebsd8/boot
# gzip -d mfsroot.gz

8. Installation



Time to turn on the client!
Make sure that it boots up under PXE boot, you may have to configure the bios to do it.
Once PXE boot is started, it should find the DHCP server in a few seconds, if not, double check your settings

After PXE boot, it will load all the necessary boot files and land you with the Welcome to FreeBSD screen

Select 1. for normal boot, and install

Run through all the installation steps as normal, until you select the source to install BSD from. Here you will need to choose NFS.

If it asks you to configure NIC's, select the one that is currently plugged into the DHCP server, and configure that. Let it auto configure on DHCP, if it doesn't find the server, you've chosen the wrong NIC

Select NFS mount
Give the address 192.168.1.1:/usr/local/freebsd8
Install should be automatic from here on.

Well done!

Some head scratching errors:



Problem: I kept getting this error:
ROOT MOUNT ERROR:
If you have invalid mount options, reboot, and first try the following from the loader prompt:

set vfs.root.mountfrom.options=rw

and then remove invalid mount options from /etc/fstab.

Solution: This was because within /usr/local/freebsd7/boot/loader.conf
I had set vfs.root.mountfrom="ufs:/dev/md0c"
Solved by changing it to "ufs:/dev/md0"

Problem: After loading loader.conf on the client machine, the spinning '\' would hang.
This was because I had originally rebuilt pxeboot to accept a higher com port speed.
For some reason, if the com port is set up in any way, the client will hand here.
Solution: Dont configure the com port to accept the higher speed of 115200, just use the pxeboot that comes out of the box.

Problem: When picking NFS as the installation media, the client would hang when trying to mount the NFS drive.
Solution: Quite easy actually, make sure you pick the correct network card to mount from! After picking the card, the client will try nad configure it. Make sure to configure it by DHCP. If it doesn't find your DHCP server that you found on boot, then you've picked the wrong network card from the list. The list will only be present if there is more that one NIC on your machine.

Friday, August 28, 2009

has_many :through polymorphic fun

The only thing better than creating has_many :through relationships with extra attributes , is creating polymorphing relationships. It sounds a little complicated, but its a simple concept in practice.

Take the diagram below. A release can have many test cases in it, but it could also have bugs which need to be checked in the same release. Now we could create a join table between releases and cases as well as releases and bugs, or we could just use one join table that can link releases to both cases and bugs.



OK, time to install the plugin:
Installing using script/plugin install proved a problem, so you can also do this
Goto Github and download the plugin.

http://github.com/fauna/has_many_polymorphs/tree/master

You can then untar it, and move it to the folder

vendor/plugins/has_many_polymorphs


Now lets generate our models.
Keep an eye on the Releaseable model. It has only three columns
release_id - the release that the other models map to
runable_id - holds the id of the model that is joining to it
runable_type - holds the model name.
Using runable_id and runable_type, we know the model to look for, as well as the id of that model.


class Release < ActiveRecord::Base
has_many_polymorphs :runables, :from=>[:cases, :bugs], :through=>:releaseables
end

class Releaseable < ActiveRecord::Base
belongs_to :release
belongs_to :runable, :polymorphic=>true
end


class CreateFun < ActiveRecord::Migration
def self.up
create_table :releases do |t|
t.string :version
end

create_table :cases do |t|
t.string :title
t.text :steps
t.text :expected
end

create_table :bugs do |t|
t.string :title
t.text :description
end

create_table :releaseables do |t|
t.integer :release_id
t.integer :runable_id
t.string :runable_type # very important that this is a string!
end

end

def self.down
drop_table :releases
drop_table :cases
drop_table :bugs
drop_table :releasables
end
end


And that is it for a poylmorphic relationship. No methods need to be put in the Case or Bug models. So we can add as many relationships to releases by only editing the Release model.

Now lets use our new models. First we'll create our release, bug and case.

Release.create(:version=>1)
Bug.create(:title=>'Bug 1')
Case.create(:title=>'Case 1')
Case.create(:title=>'Case 2')


Joining them is as easy as using '<<'

Release.bugs << Bug.find(1)
Release.cases << Case.find(1)


If we look at our database now, we case see our runable_id, along with our runable_type so we know which table to look up to retrieve our record

+----+------------+------------+--------------+
| id | release_id | runable_id | runable_type |
+----+------------+------------+--------------+
| 1 | 1 | 1 | Bug |
| 2 | 1 | 1 | Case |
+----+------------+------------+--------------+


If we want to see what cases and bugs we have on a release, we can use

Release.find(:first).runable

to see all the bugs and cases.

Or, we can find the bugs or cases on their own

Release.find(1).bugs
Release.find(1).case


There is one other way to associate a case with a release, we can create a Releaseable record directly. This is really useful if we also want to set some extra attributes on the join table(Releaseable).

Releaseable.create(:release=> Release.find(:first), :runable=>Case.find(2))

Its as simple as that!

Sunday, August 16, 2009

has_many :through fun

There are a good number of different join models with Rails, some more complicated than others.
One of my favorite is has_many :through, it lets you join two models together using a separate join table which can also hold extra attributes for that join.

A simple test management system -



From the diagram, a release has many test cases, but a test case has many releases. A simple HABTM join would almost do here, except, when a test case is part of a release it should have a result (PASS, FAIL, NOT_RUN etc). So a join table is needed to store these extra attributes.
We also want a test case to have the result of NOT_RUN when its add to a release.

So diving into the models while keeping an eye on the diagram above

class Release < ActiveRecord::Base
has_many :cases, :through=>runs
has_many :runs
end

class Case < ActiveRecord::Base
has_many :releases, :through=>runs
has_many :runs
end

class Runs < ActiveRecord::Base
belongs_to :case
belongs_to :release
end

Now for the migrations.
The interesting one is the 'runs' table. It contains an identifier to Release and another
one to Case, as well as a third column to store the result. The Release and Case objects dont need any extra identifiers, as its all managed in the Run table.

class SetItUp < ActiveRecord::Migration
def self.up
create_table :releases do |t|
t.string :version
end

create_table :cases do |t|
t.string :title
t.string :steps
t.string :expected
end

create_table :runs do |t|
t.integer :release_id
t.integer :case_id
t.string :result
end
end


def self.down
drop_table :releases
drop_table :cases
drop_table :runs
end
end

Next on the agenda - using them in the console. First lets create a release and two test cases, c1 and c2.

>> r = Release.create(:version=>1)
>> c1 = Case.create(:title=>'A Title', :steps=>'A step', :expected=>'Expected Stuff')
>> c2 = Case.create(:title=>'Another Title', :steps=>'Another step', :expected=>'Expected Stuff')

There are two ways to add a case to a release now. First lets use the << operator, which will automatically create the association between the release and the case.

>> r.cases << c1

Now if you look at the runs table in mysql, you will see release_id and case_id are both filled but the result is empty. Not exactly what we're after.

+----+------------+---------+--------+---------------------+---------------------+
| id | release_id | case_id | result | created_at | updated_at |
+----+------------+---------+--------+---------------------+---------------------+
| 1 | 1 | 1 | NULL | 2009-08-16 15:51:34 | 2009-08-16 15:51:34 |
+----+------------+---------+--------+---------------------+---------------------+

The next option is to explicitly set the release, the case and the result in the join.

>> Run.create(:release => r, :case => c, :result=>'NOT_RUN')

Now if we take a look, we can see all the fields we want are filled! Great, now we can go on with testing our release.

+----+------------+---------+--------+---------------------+---------------------+
| id | release_id | case_id | result | created_at | updated_at |
+----+------------+---------+--------+---------------------+---------------------+
| 2 | 1 | 2 | NOT_RUN| 2009-08-16 15:53:15 | 2009-08-16 15:53:15 |
+----+------------+---------+--------+---------------------+---------------------+

However, when we want to change the result in the Run table, we will have to reference the Run object directly.

>> Run.find(1).update_attributes(:result=>'PASS')