Wednesday, February 24, 2010

Novell Identity Manager integration with BlackBerry Enterprise Server

Working with a customer who owns Novell Identity Manager who wanted to integrate their existing BlackBerry Enterprise Server infrastructure to be managed by Novell IDM. Currently, they have many users who are no longer working for the company that still have active BlackBerry accounts on the BES server, which is a huge compliance isue. By using Novell IDM, we can automatically provision and deprovision the BlackBerry accounts.

Upon my initial research, I had determined that the SOAP driver would be our best method for integrating the BES infrastructure, but after meetings with the customer, I discovered that they were on version 4.x f BlackBerry and the BlackBerry API that I had intended to use requires at least version 5.x of BES.

I did some additional research and found that BlackBerry has a CLI tool that can be leveraged in the BlackBerry Resource Kit. Our solution implemented the Resource Kit and Identity Manager, using the scripting systems driver, as to be used to pass events directly to the CLI.

Unfortunately, the customer did not own the scripting systems driver, so it was decide that we would use the CSV Driver to create a CSV file with all required attributes and an event identifier to pass events to a custom created windows service. The windows service monitors an input directory, and when the CSV file is placed into the directory, it consumes it, formats the CLI string using the attributes in the CSV file, then passes it on to the CLI. The return code text is then formatted back to an output CSV file, which is consumed by the Novell IDM CSV driver and the return value is stored into an auxiliary attribute on the user object.

The driver is used to create, enable, and disable BlackBerry accounts. A workflow was implemented so that BlackBerry's could be formally requested. The workflow requires a few levels of approval, then is passed to the team that manages the BES system. The BES team members then have the ability to select which BES server, IT Policy, the Activation Password, and ensure the value for the mailbox is correct in the directory prior to the create event occuring.

The BlackBerry accounts are controlled through entitlements. The entitlement requires the workflow to be completed, the required attributes to be present, and the user cannot be disabled. An additional workflow can be created to revoke a BlackBerry. If the user is terminated and has their account disabled, the BlackBerry will automatically be revoked as well.

Thursday, November 5, 2009

Restrict Logins with Likewise on Linux/Unix

I'm a pretty big fan of Likewise. I am currently using the likewise open version on many of my linux boxes so that I can authenticate against Active Directory. This makes my life easier in the fact that I don't have to deal with adding and removing users from these boxes.

Our standard build consists of a build that has sudo installed. Root cannot login over SSH and we have a default local account that the sysadmins know and can use if in a pinch. We do, however, use our own accounts so it can leave a sudo log trail.

Now, when we installed likewise-open on our default ubuntu image, we had one concern, any user with an Active Directory account can now login to our Ubuntu servers. While they will just be a dummy user with minimal privileges, what is to stop these users from using an exploit to escalate their privileges to root, or filling up their home directory and owning some of the boxes that don't keep those partitions separate. While I don't think any of my users would do anything like that, its always better to allow minimal privileges and lets face it, my sales, accounting, managers, and many others have absolutely no business in ever logging into any of the production linux/unix servers.

I did some strategic Googling query building and I was able to stumble on a thread (sorry I lost the original link) that pointed me to a specific line in a configuration file (/etc/security/pam_lwidentity.conf). By uncommenting this line and putting a group in there, I can restrict who can login to my servers. So I set it to the following and simply created a security group called 'unix admins' in Active Directory to hold the people who can admin these boxes. Piece of cake.

require_membership_of = GCA\unix^admins

Wednesday, November 4, 2009

Linux Kernel Vulnerability

Found the following article on slashdot and immediately jumped over to my linux boxes to ensure we aren't vulnerable.

http://linux.slashdot.org/story/09/11/04/0320254/Bug-In-Most-Linuxes-Can-Give-Untrusted-Users-Root?from=rss

More information can be found at securityfocus here:

http://www.securityfocus.com/bid/26831/info

From what I understand, you can fix this vulnerability by simply setting the vm.mmap_min_addr setting to a value of 4096 or greater ( in short, if this value is 0, you are vulnerable). I also read that if you install wine (winehq.com) that it will automatically set this value to 0 and make you vulnerable.

To check the value of this variable on your linux system to see if you are currently vulnerable, just run this command and ensure it is not set to a value of 0:

sysctl vm.mmap_min_addr

On debian based installations, check the following site for how to fix this for the current boot and how to make it persist a reboot.

http://wiki.debian.org/mmap_min_addr

For those that are really concerned right now, I checked my Ubuntu 8.04.3 LTS servers and they all are defaulted to 65535 for this value, so they should not be vulnerable to this bug. Keep in mind what I stated above though, if you install wine, you become vulnerable.

Tuesday, November 3, 2009

VMWare ESX Troubles

First a bit of background. We run some VDI (Virtual Desktop Infrastructure) here at GCA. We have our ESX 3.5 servers running and our Netapp SAN hooked up to it via NFS. On the ESX servers, along with our production 'server' vms, we also have a bunch of VDI vms. The VDI vms are just virtual machines running Windows XP for individual users to use. They access these vms using a few tools, such as Sun's Secure Global Desktop and Sunray Ultrathin clients. They can also be accessed with a regular RDP tool.

Let me also preface with the fact that we have nightly snapshots of the VM's and the developers check their code into subversion on a regular basis, so worst case scenario is a 20 min operation to copy the VM from last nights backup in the event one explodes.

Today I had an engineer who tried to reboot her Windows XP VDI and it would not reboot. I asked her if she had any open applications, to which she replied no, so I used VirtualCenter to send the VM a reboot command. That did not work, so I gave it a good amount of time, then got impatient and took the shotgun approach and tried the reset vm command instead. This still did not work, so I tried it again and I get an error that there is another operation in progress.

I did some googling and found information on the nuclear approach to the situation. First thing is to find the vm world id with the following command:

cat /proc/vmware/vm/*/names grep "vmname"

The result should start with vmid=####. With this number, you can browse over to /proc/vmware/vm/####/cpu and do:

less -S /proc/vmware/vm/###/cpu/status

Well, I had a problem. The cpu directory in this location was not present. I could not get the VM to stop at all. I also tried the other method of using ps -aux and grepping for the VM name, but it did not have a process either. I did not find a solution for this, but here is what I found in /var/log/vmkwarning log file. Look at the timestamps, it figured it out eventually and allowed me to do operations it, but it took well over an hour. See below for log output and please let me know if you know how I can fix this without waiting 90 minutes in the future:

Nov 3 14:54:50 gcaesx001 vmkernel: 40:00:33:25.587 cpu1:1042)WARNING: Swap: vm 1390: 7515: Swap sync read failed: status=195887193, retrying...
Nov 3 14:54:50 gcaesx001 vmkernel: 40:00:33:25.587 cpu0:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=1
Nov 3 14:54:50 gcaesx001 vmkernel: 40:00:33:25.638 cpu2:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=2
Nov 3 14:54:51 gcaesx001 vmkernel: 40:00:33:25.939 cpu2:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=4
Nov 3 14:54:54 gcaesx001 vmkernel: 40:00:33:29.144 cpu1:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=8
Nov 3 14:55:02 gcaesx001 vmkernel: 40:00:33:37.147 cpu2:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=16
Nov 3 14:55:18 gcaesx001 vmkernel: 40:00:33:53.155 cpu0:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=32
Nov 3 14:55:50 gcaesx001 vmkernel: 40:00:34:25.175 cpu3:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=64
Nov 3 14:56:54 gcaesx001 vmkernel: 40:00:35:29.224 cpu0:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=128
Nov 3 14:59:02 gcaesx001 vmkernel: 40:00:37:37.300 cpu0:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=256
Nov 3 15:03:18 gcaesx001 vmkernel: 40:00:41:53.473 cpu1:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=512
Nov 3 15:11:51 gcaesx001 vmkernel: 40:00:50:25.865 cpu2:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=1024
Nov 3 15:28:55 gcaesx001 vmkernel: 40:01:07:30.612 cpu3:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=2048
Nov 3 16:03:05 gcaesx001 vmkernel: 40:01:41:40.116 cpu1:1042)WARNING: Swap: vm 1390: 7528: Swap sync read retry failed: status=195887193, retry=4096
Nov 3 16:18:12 gcaesx001 vmkernel: 40:01:56:47.424 cpu3:1042)WARNING: Swap: vm 1390: 7550: Read failed: numPages = 1
Nov 3 16:18:12 gcaesx001 vmkernel: 40:01:56:47.424 cpu3:1042)WARNING: Alloc: vm 1390: 5226: unable to read from slot(0x102ac7c)
Nov 3 16:18:12 gcaesx001 vmkernel: 40:01:56:47.424 cpu3:1042)WARNING: World: vm 1390: 6870: vmm0:vdi-rlosaria:vmk: vcpu-0:Unable to read swapped out PPN(0xa1ef) from swap slot(0x102ac7c) for VM(1390)
Nov 3 16:18:12 gcaesx001 vmkernel: 40:01:56:47.426 cpu3:1042)WARNING: P2MCache: vm 1390: 450: Alloc_GetPhysMemRange failed for PPN 0xa1ef status = Permission denied
Nov 3 16:18:12 gcaesx001 vmkernel: 40:01:56:47.430 cpu2:1037)WARNING: World: vm 1390: 5160: Panic'd VMM world being reaped, but no core dumped.

Wednesday, October 7, 2009

Ubuntu MySQL 5.x Server

Spinning up new versions of servers at the office to host services. Last night I was tasked with migrating the MySQL server over to a new Ubuntu Server 8.04.3 LTS virtual machine. Lucky me, I have a template of that OS already, so I cloned one up and booted it.

Step 1:

After the cloning process, VMWare gives the virtual machine NIC a new MAC address. Why is this significant? Well, from what I understand, the MAC address is what Linux uses to associate eth0 with that NIC. So, since your NIC no longer has that old MAC address, you have to change /etc/network/interfaces to bring up eth1 instead of eth0. I am not sure how to get eth0 freed up, but it can probably be found with a carefully crafted Google query.

Step 2:

Next thing we want to do is update our Ubuntu Server. This is one of the main reasons why I like ubuntu, its really this simple:

sudo apt-get update
sudo apt-get dist-upgrade

Step 3:

Next order of business is authentication. We like to use integrated Active Directory authentication using Likewise. Its a very cool tool to make your Linux servers authenticate back to Active Directory so you do not have to maintain local /etc/passwd files, or a separate LDAP server.

As a matter of best practice, I always create a local /etc/passwd account. This is for two reasons, first, if AD cannot be found, you cannot logon (remember ubuntu does not give root a password), and second, its always industry best practice when authenticating against an LDAP server to leave a local admin account.

Once that account is added, I add likewise to the ubuntu server with the following command:
sudo apt-get install likewise-open
This is some pretty cool software. I ran a few commands next, the first puts likewise to automatically startup on a boot, so we do not have to do it manually. The second command is the command to join a domain (the 'administrator' can be replaced with any account with privileges to join a system to the domain).

sudo udate-rc.d likewise-open defaults
sudo domainjoin-cli join domain.local Administrator

Now, because I hate prepending my domain name to my username (IE domain\user) when logging in, I want it to just use the default domain name when I put in a username unless otherwise instructed. To do so, I added the following line to the /etc/samba/lwiauthd.conf file, then ran the command to restart likewise.
winbind use default domain = yes
sudo /etc/init.d/likewise-open restart
Then, the final order of business with likewise, I need my domain admins group to be able to sudo, similar to the way users in the admin group can in a default ubuntu installation. To do this, I editted the /etc/sudoers file and added the following line:
%domain^ admins ALL=(ALL) ALL
Step 4:

Next order of business is my nagios monitoring plugins. I use a monitoring tool call nagios to monitor my servers. Nagios has a plugin you can install called nrpe that runs as a local daemon or in xinetd so the monitoring server can send requests to run predefined scripts. This is helpful to obtain data you cannot get from remote, such as load average, disk utilization, etc. Note, I am only installing the client side plugins and remote pole daemon (nrpe), this is by no means a guide to setup a nagios server (maybe a future blog post though :P )

First thing I install is the nrpe-server package. This is the daemon (or xinetd process) that runs to accept remote connections to run predefined scripts and return the results.
sudo aptitude install nagios-nrpe-server
After this is installed, you will need to modify its configuration file located at /etc/nagios/nrpe.cfg. In this file you will setup two things, first there is a directive to specify which IP's are allowed to talk to your nrpe process (this will prevent bad people from monitoring your server without your permission). This is also where you setup your check commands. Nagios uses scripts called check commands to get perfdata from a server, such as disk space and load averages. You setup the scripts that run locally on command from the main nagios server in this file a well. Then, you simply tell your nagios server where this box is and what command to ask it to run.

I installed all the main check commands, then just chose which ones I wanted to use in the nrpe-server's configuration file. Here is how to install the plugins:

sudo apt-get install nagios-plugins nagios-plugins-basic
nagios-plugins-extra nagios-plugins-standard

Nagios should now be able to monitor your server.

Step 5:

Now its time to get our MySQL stuff installed. Once again, time for a shameless plug of "This is why I love Ubuntu":
sudo apt-get install mysql-server mysql-client
You have now installed MySQL server. It will probably ask you to set the mysql root password, try not to forget it.

Next, we'll install a really nifty MySQL admin tool called phpmyadmin. This usually requires you install a bunch of prerequisites, but in this case, its ubuntu-ized, so its just another one liner:
sudo apt-get install phpmyadmin
If you do not already have a web server installed, it will automatically install apache2 for you. After installation, it will launch an automatic configuration tool which will ask what web server to configure. Select apache2 if you did not have a web server installed prior. If you did, tell it which one.

The last couple of tasks are to enable some users for this SQL server and allow other hosts to connect to it.

We'll start with enabling some users. Here is the quick and dirty dev server way to do it. Please do not use this in real environments. I am not going to dive off into database security, just know this'll work for a nice little sandbox, not a production environment.
mysql -u root -p
CREATE USER 'user'@'%' IDENTIFIED BY
'password';
GRANT ALL PRIVILEGES ON *.* TO 'user'@'%';

Now that we have a user that can connect from any IP (thats what the % means) we need to tell the MySQL server to listen on an interface other than 127.0.0.1. By default, MySQL only listens on the localhost IP for security. In this instance, I need a database server to connect to it. There is a line that looks like this:
bind-address = 127.0.0.1

You are going to want this to be this server's (the MySQL server, not the web server) IP address. This is telling it what Ip addresses to listen on (do not mistake this with listen to). If you just comment this line out, MySQL will listen on all IP addresses of the host.

Step 6:

Now its migration time. What I do for migrations is do a mysqldump of the database, then import it on the other side from the dump file it produces. The mysqldump command will produce a file that goes one by one through your database tables, dumps them if they exist, creates the table, then inserts all the data into it. The output is really just one giant text file that can recreate your entire database.

If you are on a production system, its probably a good idea to stop any service leveraging the database, then bounce the database server to make sure all your writes are finished. Once this is done, we can use this command to dump a copy out:
mysqldump --add-locks --user=root --password=password --all -c dbname >
/path/to/dbname-backup.sql
Move this file over to the new MySQL server. We now add two lines to the top of the file:
create database dbname;
use dbname;
This will create our database and use it so we can create the tables and insert the data from the file. All we need to do is pipe the file into a mysql prompt:
mysql -u root -p < /path/to/dbname-backup.sql
Run some tests, everything should be happy. Our database is migrated over to our new server.

Step 7:

Finally, we need to ensure we are backing up our database. I created a little backup script that I use based off our migration command. Most people have solutions from their backup suites that work, I actually use both this and our suite. I think this is one of the easiest to restore backups (see Step 6 migration). Its adding 2 lines to a text file, then a 1 line command at a prompt to restore your database, can't beat that. I just run the script in cron. Please note, if you want to use the scp portion, the user running the script in cron needs a preshared RSA key with the remote backup server.
# set date format
date=`date '+%m%d%y'`
# dump database
mysqldump --add-locks --user=user --password=password --all -c dbname
> /path/to/dbname-backup-$date.sql
# compress backup file
gzip -9 /path/to/dbname-backup-$date.sql
# secure copy to backup server
scp /path/to/dbname-backup-$date.sql.gz user@backupserver.domain.local:/path/to/backup/dir
# if scp to fileserver failed, email me about it
if [ $? -gt 0 ]
then
mail -s "Development dbname
MySQL Backup Failed" emailalias@domain.local < /dev/null exit fi # remove local copy rm /path/to/dbname-backup-$date.sql.gz
Toss that into a script and put it in cron on an interval of choice. Its moving the backup off to another server and notifying you if the file does not move over correctly.

Thats all there is to it. We built a brand new server to host MySQL, have it authenticating against AD, monitored by our nagios server, backing itself up, and servicing our database needs. Funny part is, it took me longer to do the writeup than it did to do the actual work :)

Friday, October 2, 2009

Auto-start Virtual Machines on Linux

So here is my scenario. I have a redhat EL5 machine running vmware workstation 6.5. We had a power outage while I was out of town and these dev/test boxes are not on the UPS, DOH! Well, the developers at this point had to figure out how to start the virtual machines on this box by themselves. So, when I returned, my first task was to ensure that these virtual machines startup automatically within vmware workstation.

So, the first thing I did was figure out how to get the virtual machines to start with the vmrun command. The first thing I learned was that I needed a line added to the vmx file to ensure the question prompts will not hold up the virtual machine from starting without the GUI to see the question that Vmware wants answered. To do this, I added the following line to my vmx file so that vmware will automatically select the default answer to any questions it may have.

msg.autoAnswer = "TRUE"

So, now I was able to figure out using helpfiles and some carefully crafted google queries that you can start a virtual machine in vmware workstation with the following command in headless mode (IE, no DISPLAY variable set).

vmrun start nogui

After figuring this out, I need to create an init script to start this up. The first thing that popped in my mind is the fact that I do not want these VM's starting under the root user, so I adapted my vmrun command for the script to look like this:

su - rivey -c "vmrun start nogui"

This will use the switch user command to run the command in parenthesis as the user I specify. I then snatched up the sshd init script, stripped all the junk I didn't need out, and finished with the following:
#!/bin/bash
#
# Init file for start Virtual Machines
#
# will start the following VM's
# /vms/Solaris10/Solaris10.vmx
# /vms/DC01/DC01.vmx
# /backup/vms/oraresource/oraresource.vmx
# /backup/vms/WindowsMemberServer/WindowsMemberServer.vmx
#
# source function library.
/etc/rc.d/init.d/functions

RETVAL=0
prog="vmware-vms"

#Location of vmx files
VM1=/vms/Solaris10/Solaris10.vmx
VM2=/vms/DC01/DC01.vmx
VM3=/backup/vms/oraresource/oraresource.vmx
VM4=/backup/vms/WindowsMemberServer/WindowsMemberServer.vmx

VMRUN=/usr/bin/vmrun

runlevel=$(set -- $(runlevel); eval "echo \$$#" )

start()
{
echo -n $"Starting $prog: Solaris10 "
su - rivey -c "$VMRUN start $VM1 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo

echo -n $"Starting $prog: DC01 "
su - rivey -c "$VMRUN start $VM2 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo

echo -n $"Starting $prog: oraresource "
su - rivey -c "$VMRUN start $VM3 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo

echo -n $"Starting $prog: WindowsMemberServer "
su - rivey -c "$VMRUN start $VM4 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo
}

stop()
{

echo -n $"Stopping $prog: Solaris10 "
su - rivey -c "$VMRUN stop $VM1 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo

echo -n $"Stopping $prog: DC01 "
su - rivey -c "$VMRUN stop $VM2 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo

echo -n $"Stopping $prog: oraresource "
su - rivey -c "$VMRUN stop $VM3 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo

echo -n $"Stopping $prog: WindowsMemberServer "
su - rivey -c "$VMRUN stop $VM4 nogui"
RETVAL=$?
[ "$RETVAL" = 0 ]
echo
}

case "$1" in

start)
start
;;

stop)
stop
;;

*)
echo $"Usage: $0
{startstop}"

RETVAL=1
esac
exit $RETVAL

Now, a few things to point out. The first is pretty obvious, this script is starting and stopping 4 different VM's. The second, this is by no means the cleanest way of doing this and probably not the best script in the world, but it was a pretty nice little thrown together script to get the job done. Quick and dirty. Initial tests show the script is working like a champ. I tossed the script in /etc/init.d, threw the executable bit on, then sym linked it into rc3.d and rc5.d.

Friday, July 24, 2009

Symantec NBU for Windows Class Build

Here at GCA we run classes for multiple vendors. I was tasked with creating the class build for the Symantec Netbackup 6.5 for Windows class. The hardware we are using, however, is all virtual. We are using a Netapp VTL and VMWare ESX Server in order to create the student systems. The hardware we are virtualizing on lives at our Tampa office while the class is going to be run from our Seattle office.

The build starts out pretty simple. I spun up some instances of Windows server 2003, patched them up, and made them ready to be cloned for student systems. On the netapp VTL, I created a bunch of Robots and Tape devices, which are emulating a StorageTek T9840C. I added 16 tapes, 8 on one serial format and 8 on another (as requested by Symantec). I then dropped a couple HBA's into the ESX Server and connected them to the VTL (we had plenty of HBA's laying around so the Multipathing and redundancy is nice to have seeing as how we are going to share the HBA's for all of the systems).

There were a few things that threw me. The first one was how to match up the Robot and Tape drives on each machine. When I connected the HBA's and added a new SCSI device to the VM, the pulldown list just had them by name, so I saw 8 Robots and 16 drives with no information regarding which was which. They were also not in any specific order.

To figure this out, I SSH'd over to the service console of the ESX Server and did an ls -al on the devices directory that contained all of the targets. This matched a sym link that had the LUN target directly to a ridculously long unique number identifier. This long number also shows after you add the SCSI device to vmware if you hover over it in the edit settings window.

The next thing I tripped over a bit was making it to where two virtual machines had access to the same SCSI device. The way this is done is by modifying the SCSI controller used to a different type. In edit virtual machine settings, if you click on the SCSI controller, you can change it so that the SCSI devices can be shared. The only problem is this change is globally on the SCSI controller, so to make this not problematic for the other SCSI devices, such as the windows C: drive, I moved the VTL SCSI devices over to a different SCSI id (1:0 instead of 0:3). This will make vmware automatically add a new SCSI controller to the box. This one I made the change and everything was happy.

The next thing I bumped into was our firewall. For whatever reason our checkpoint vpn tunnels are not setup correctly. I could not figure out how to allow the Seattle DMZ network to talk to the Tampa DMZ network over tcp/3389 (RDP). After spending some time trying to figure it out, I used a work around. I had 4 consecutive external IP's available, so I simply NAT'd these boxes out to the edge, allowed our Seattle office's external IP to RDP in on those external IP's and had it work that way instead of figuring out the VPN tunnel madness.

The last headache to figure out was the driver for the tape devices. The funny part is this is a chicken and egg problem. In order to get the driver for the tape drives installed, I need netbackup installed (it uses a symantec halfinch.sys driver). Well, during the install for the configuration portion, it wants the tape devices to be installed to configure them. In order to remedy this, Symantec added a pause mechanism to their setup scripts so the user can pause the install after the drivers are present on the system, install the tape devices from device manager, then continue the install.

On the day of the class everything ran smoothly. The only thing I forgot to do was to populate the host files of each system with the IP information on the other machines in the 'classroom'. This wasn't a big deal, the instructor was able to figure it out and work through it.

Class ran, students seemed to be happy from what I understand, and now I have a template for this build as well as the procedure (this is why I blog this stuff!) to build it back up again.