Skip to content. | Skip to navigation

Sections
Personal tools
What is this?
Hi, my name is Tom Lazar and I'm a Plone and Zope developer based in Berlin, Germany and this is my personal and professional (no big difference, really...) website.
 

Migrating Cyrus

Filed Under:

Recently I was faced with the challenge of moving several dozen of IMAP accounts wheighing in just about 10Gb from one machine to another.

When initially setting up the IMAP hosting business I was totally naive about the expected usage, especially since all of my then-new customers kept insisting that they didn't need much space. But there's a German proverb that more or less means, that you get your appetite from eating ("Der Appetit kommt beim Essen") - and that's exactly what happened ;-)

Another - in retrospect - unwise decision was to grant my clients quota on a per-client-basis and not on a per-account-basis (i.e. "You've got seven IMAP accounts and three gigabytes of space"). The problem was, of course, that as people started filling up their mail accounts, one client after another went over-quota - without anybody really noticing, because each of their accounts for itself was well within their limits.

That's when I decided to either move back or make a step forward. So one of the first things I did when teaming up with fuxdata was to rent additional hardware and set up a new cyrus server from scratch - and sofar everything has been working like a charm.

However, what still needed to be done, was to move the existing accounts on the old server to the new machine unless I wanted to risk that one day it would simply become utterly full (and we all know, how unpleasant that can get...)

The biggest problem, was that both servers are productions systems. So a simple "Let's move all spool files, change the IP for the host name and hope nobody notices!"-approach was out of the question. Also, over the past three years there has evolved quite some chaos in the naming convention of the cyrus users, something we wouldn't want to 'infect' the new server with. So in the end the problem was How to migrate cyrus accounts from one live machine to another without losing any mail that's being delivered during the transfer.

Again, instead of digging out my DocBook Editor and writing a new chapter for my cookbookk (which I then would have to upload) I'm simply putting the details in the extended section of this entry.

The biggest obstacle was to ensure that both mailserves can continue to run during this process without risking that mails to the account being moved would get lost. (I.e. simply doing an rsync wouldn't work: what would happen, if mails would be delivered during the sync?)

I've decided to put the description of the process where it belongs: right into the code. So without further ado:

#!/usr/bin/perl -w

use Cyrus::IMAP::Admin;

#
# migrate a cyrus mail-account from one host to this one (localhost).
#
# usage: migrate-cyrus-account.pl old-user source-host new-user
#
# assumptions:
#
# 1. new-user already exists on target-host and is fully configured
# 2. both hosts run exim 4.3.x and cyrus 2.1.x or 2.2.x
# 3. local cyrus user has ssh login on source-host
# 4. local file 'migration-forwarding.txt' contains all forwardings
for old-user@sourcehost # to new-user@targethost
# 5. exim at sourcehost is configured to forward from /usr/local/etc/exim/forwardtable_cyrus,
# which is writeable for user cyrus.
# (this file will be added to with each run of this script)


if (!$ARGV[0]) {
die "Usage: $0 old-user source-host new-user target-host passwd \n";
} else {
$olduser = "$ARGV[0]";
$sourcehost = "$ARGV[1]";
$newuser = "$ARGV[2]";
}

#
# cyrus configuration
#
my $cyrus_server = "localhost";
my $cyrus_user = "cyrus";
my $cyrus_pass = "xxxxx";
my $hostname = "new.host.tld";
my $mechanism = "login";

#
# paths and files
#
my $spoolpath = "/var/spool/imap/user/";
my $eximetcpath = "/usr/local/etc/exim/";
my $forwardtable_cyrus = $eximetcpath."forwardtable_cyrus";
my $migration_forwarding = "/usr/local/cyrus/tools/migration-forwarding.txt";


#
# subroutines
#

#
# applies the rights to the users inbox
# the rights are analogous to the cyradm sam-command
#
sub setACL {
my ($user, $rights) = @_;
$mailbox = "user.". $user."";
print "Setting ACL for ", $mailbox, " to ", $rights, "\n";

@mailboxes = $cyrus->list('*', $mailbox);

foreach my $box (@mailboxes) {
$boxname = $box->[0];
$cyrus->setacl($boxname, $user=> $rights);
if ($cyrus->error) {
print STDERR "Error: ", $boxname," ", $cyrus->error, "\n";
} else {
print "Gave ACL ", $rights, " to $boxname for $user \n";
}
}
}

#
# convenience call for rsyncing
#
sub rsyncSpool {
my ($user, $host, $newuser) = @_;

print "syncing from ", $host, " for ", $user, " at ", $spoolpath, $newuser, "/\n";
print `rsync --delete -ae ssh $host:$spoolpath$user/ $spoolpath/$newuser/`;
}

#
# returns the quota for the given user
#
sub listQuotaRoot {
my ($user) = @_;
$mailbox = "user.". $user;

my ($root, %quota) = $cyrus->quotaroot($mailbox);
if ($cyrus->error) {
print STDERR "Error: ", $mailbox," ", $cyrus->error, "\n";
} else {
return $quota{"STORAGE"}->[1];
}

}

#
# sets the quota for the given user
#
sub setQuotaRoot {
my ($user, $quota) = @_;
$mailbox = "user.". $user;

$cyrus->setquota($mailbox, "STORAGE", $quota);
if ($cyrus->error) {
print STDERR "Error: ", $mailbox," ", $cyrus->error, "\n";
} else {
print "setting ", $quota, " for ", $mailbox,"\n";
}

}

#
# MAIN
#

#
# login

$cyrus = Cyrus::IMAP::Admin->new($cyrus_server);
$cyrus->authenticate($mechanism,'imap','',$cyrus_user,'0','10000',$cyrus_pass);

# first we sync the spoolfiles while the account is still active
# in order to minimize downtime later, once the account is shut down
#
print "performing initial sync. This may take a while...\n";
rsyncSpool($olduser, $sourcehost, $newuser);

# before we begin forwarding mail for the old user from the source
# host to the new user on the new host, we must ensure that the new
# host will a) accept mail for the new user but b) not deliver them
# yet, since we'll be replacing the new user's spool files.
# a) is part of the premises for this program, and b) is simply achieved
# by setting the users quota temporarily to zero.
#
print "setting quota to zero for new user (", $newuser, ")\n";
my $initquota = listQuotaRoot($newuser);
setQuotaRoot($newuser, 0);

# next, we copy the forwarding instructions to the old server
# and append it to its forwardtable
#
print "copying $migration_forwarding to $sourcehost\n";
print `scp $migration_forwarding $sourcehost:$migration_forwarding`;
print "setting up forwarding from old host to $sourcehost:$forwardtable_cyrus\n";
print `ssh $sourcehost 'less $migration_forwarding >> $forwardtable_cyrus'`;

# the forwardings are in effect immediatly (no need to HUP), so eventhough cyrus and
# exim continue to run on the old host, we can consider the spool files of the old
# user to be safe from modification during our next sync run. unless, of course, the
# user decides to log in on the old server and mess around. We prevent this by setting
# the password of the account to something super secret ;)
#
print "locking old-user out of sourcehost\n";
print `ssh $sourcehost "echo lockeDouT34 | saslpasswd2 -p $olduser"`;

# now we can safely sync again
#
print "performing second sync. This should be marginally faster ;-) ...\n";
rsyncSpool($olduser, $sourcehost, $newuser);

# with the spool files in place we can now reconstruct the mails
#
print "reconstructing from spool files.\n";
print `/usr/local/cyrus/bin/reconstruct -rf user.$newuser`;

# the newly recreated mailfolders still have accessrights for the old user (and none for
# the new one, which is why we're gonna reverse that situation now...
#
setACL($newuser, 'all');
setACL($olduser, 'none');

# now we can safely allow any queued mails to be delivered again by resetting the quota
#
print "resetting quota to ", $initquota, "\n";
setQuotaRoot($newuser, $initquota);


print "Done.\n";

race?

Posted by Joerg at Mar 23, 2007 07:27 PM

Hi Tom,

i like your script, only one thing i don't understand. If i get it right, then the forwarding rules for exim will result in all mails being forwarded to the new machine? If now a mail flys in between the two rsync runs, then the mail ends up on the new server. Now the second rsync runs and, due to --delete, the new mail will be deleted?

Any thought is appreciated as i am now adopting the script to run with postfix/sql virtual tables.

cheers, joerg

zero quota is the key

Posted by Tom Lazar at Mar 25, 2007 10:44 AM

the mail will be forwarded to new server but there it won't be delivered yet, but remains in exim's queue, since we're setting the new users quota to zero(!).

after the transfer is complete, the quota is reset and the emails in the queue will be delivered.

Postfix version

Posted by Patrick at Aug 30, 2007 11:52 AM

Is it possible for you to post the Postfix version of the script? Please.

I don't know postfix

Posted by admin at Aug 31, 2007 11:21 PM
sorry.