Bulk Provisioning

Revision as of 05:05, 5 March 2008 by Plobbes (talk | contribs) (Passwd File to Zmprov)


Zmprov Command Files

The Zmprov command will accept commands from a file (or stdin) as input. Create a text file ("commands.zmp" for example) with the zmprov subcommands each on a line.

createDomain domain.com
createAccount andy@domain.com password displayName 'Andy Anderson' givenName Andy sn Anderson
createAccount betty@domain.com password displayName 'Betty Brown' givenName Betty sn Brown

Then send the contents of the file to zmprov.

As Zimbra:

 zmprov < commands.zmp

As Root:

 cat commands.zmp | su - zimbra -c zmprov


Below are some ways to create a command file from your existing account data.

Create Accounts

Passwd File to Zmprov

Following is a perl script to take a passwd file and turn it into a zmprov command file for account provisioning.

#!/usr/bin/perl
#
# $Id: passwd2zmprov,v 1.2 2008/03/05 05:01:29 phil Exp $

=head1 NAME

passwd2zmprov - create zmprov commands from a passwd file

=head1 SYNOPSIS

  usage: passwd2zmprov [options] [[passwd_file] ...] > commands.zmp
     -help   show a brief help message
     -man    show the full documentation

     -domain <domain>     [REQUIRED]
     -cosid <cos_id>      [default "Default COS"]
     -password <password> [default ""]

  Getting a COS id:
    zimbra$ zmprov gc <myCos> | grep ^zimbraId:

  Example converting CSV to zmprov commands:
    $ ./passwd2zmprov -domain example.moc /etc/passwd > commands.zmp

  Example provisioning ZCS accounts as 'zimbra' user:
    zimbra$ zmprov < commands.zmp

=head1 DESCRIPTION:

Tool to create commands suitable for zmprov from a UNIX passwd file.
We don't use getpwent etc., because we are likely working on a copy
and not running as root.

See Also:
  http://wiki.zimbra.com/index.php?title=Bulk_Create

=cut

use strict;
use warnings;
use File::Basename qw(basename);
use Getopt::Long qw(GetOptions);
use Pod::Usage qw(pod2usage);

my $prog = basename($0);
my ( @err, %option );

GetOptions( \%option, 'help|?', 'man', 'domain=s', 'cosid=s', 'password=s' )
  or pod2usage( -verbose => 0 );

pod2usage( -verbose => 1 ) if ( $option{help} );
pod2usage( -verbose => 2 ) if ( $option{man} );

push( @err, "-domain <domain> is required" )
  unless ( $option{domain} );

pod2usage( -verbose => 0, -message => map( "$prog: $_\n", @err ) )
  if (@err);

warn("$prog: using Default COS\n") unless ( $option{cos_id} );
warn("$prog: reading passwd like entries from STDIN\n") unless (@ARGV);

my $date     = localtime();
my $cosid    = $option{cosid};
my $domain   = $option{domain};
my $password = defined $option{password} ? $option{password} : "";
my $MIN_UID = 500;      # skip system accounts like httpd
my $MAX_UID = 60000;    # skip other system accounts like nfsnobody

# sanitize password
$password =~ s/\"/\\\"/g;

while (<>) {
    chomp;
    next if (/^\s*$/);    # skip empty lines

    my ( $uname, $x, $uid, $gid, $gecos, $dir, $shell ) = split( /:/, $_, 7 );

    if ( $uid < $MIN_UID or $uid > $MAX_UID ) {
        warn("$prog: skip $uname: $uid not between $MIN_UID and $MAX_UID\n");
        next;
    }

    # assuming gecos format is First [[MI] [Last]], sanitize a little
    $gecos =~ s/\"/\\\"/g;

    my ( $fullname, $description ) = split( /\s*,\s*/, $gecos, 2 );
    my ( $fname, $mname, $lname ) = split( " ", $fullname, 3 );
    unless ( defined($lname) ) {
        $lname = $mname;
        undef($mname);
    }
    my $displayname = $fname
      . ( defined($mname) ? " $mname" : "" )
      . ( defined($lname) ? " $lname" : "" );

    print(
        qq{ca "$uname\@$domain" "$password"},
        ( defined($cosid)       ? qq{ zimbraCOSid "$cosid"}       : () ),
        ( defined($fname)       ? qq{ givenName "$fname"}         : () ),
        ( defined($lname)       ? qq{ sn "$lname"}                : () ),
        ( defined($uname)       ? qq{ cn "$uname"}                : () ),
        ( defined($displayname) ? qq{ displayName "$displayname"} : () ),
        ( defined($description) ? qq{ description "$description"} : () ),
        qq{ zimbraNotes "Migrated $date"},
        qq{ zimbraPasswordMustChange TRUE},
        qq{\n},
    );
}

=head1 HISTORY

 2007/01/23, Version 1.0/1.1 Dlbewley
 2008/03/04, Version 1.2 Plobbes

=cut

To execute:

 perl passwd2zmprov -domain example.moc < passwd > mydata.zmp

CSV File to Zmprov

Following is a simple perl script to take a CSV file and turn it into the correct zmprov commands

#!/usr/bin/perl
 
# Lookup the valid COS (Class of Service) ID in the interface or like this
my $cosid = `su - zimbra -c 'zmprov gc Default |grep zimbraId:'`;
$cosid =~ s/zimbraId:\s*|\s*$//g;
  
while (<>) {
       chomp;
 
       # CHANGE ME: To the actual fields you use in your CSV file
       my ($email, $password, $first, $last) = split(/\,/, $_, 4);
         
       my ($uid, $domain) = split(/@/, $email, 2);
 
       print qq{ca $uid\@$domain $password\n};
       print qq{ma $uid\@$domain zimbraCOSid "$cosid"\n};
       print qq{ma $uid\@$domain givenName "$first"\n};
       print qq{ma $uid\@$domain sn "$last"\n};
       print qq{ma $uid\@$domain cn "$uid"\n};
       print qq{ma $uid\@$domain displayName "$first $last"\n};
       print qq{ma $uid\@$domain zimbraPasswordMustChange TRUE\n};
       print qq{\n};
}

The above is only a starting place, you will need to change other options (eg: the zimbraPasswordMustChange is an example only) and of course how you create and split the input data.

Also, some CSV files may contain quotes you want to remove.


Create Aliases

Aliases File to Zmprov

This script will parse a sendmail style aliases file including any aliases that are in an ":include:" directive. If you have appropriate comments in the included file they will be placed in the LDAP attributes of the distribution list object.

#!/usr/bin/perl
################################################################################
# $Id: aliases2zmprov,v 1.4 2007/03/07 22:18:56 dlbewley Exp $
#-------------------------------------------------------------------------------
# Description:
#   Tool to create commands suitable for zmprov from a unix aliases file.
#
#   Handles the case of an :include: construct.
#   - Comments in include file are parsed as follows
#   - "Foo" goes in LDAP description attribute, "Bar" in the displayname.
#     # Description: foo
#     # Name: Bar
#
#   Handles the case of remote and multiple recipients by making a dist list.
#   - A Zimbra alias must be associated with exactly one Zimbra account.
#   - A Zimbra distribution list may contain users on remote hosts.
#
# See Also:
#   http://wiki.zimbra.com/index.php?title=Bulk_Create
#
# Usage:
#    ./aliases2zmprov /etc/aliases 1> aliases.zmp 2> aliases.err
#    zmprov < aliases.zmp
#
################################################################################

my @CNAME_SKIP= qw( ftp news postmaster root webmaster );
my @ALIAS_SKIP= qw( abuse mailer-daemon postmaster  );
my $DOMAIN = 'foo.bar.com';
my $SOURCE_HOST = 'mail';

use File::Basename;

while(<>) {
    chomp;
    s/#.*$//; # skip comments
    next if /^\s*$/; # skip blank lines

    my ($description,$display_name,@cnames);
    my ($alias,$cname) = split(/:/,$_,2);

    $alias =~ s/\s*//g;
    # scrutinize the aliases
    if (grep /^$alias$/, @ALIAS_SKIP) {
        warn "skipping alias $alias -> $cname";
        next;
	}

    $cname =~ s/\s*//g; # remove all spaces

    # scrutinize the canonical names
    if ($cname =~ m/:include:/) {
        (($description, $display_name, $cnames_ref) = parse_include($cname)) || next;
        @cnames = @{$cnames_ref};
    }
    if ($cname =~ m/,/) {
        # multiple recipients make this a list instead of an alias
        @cnames = split(/,/,$cname);
    }

    # if more than one cname then it is a dist list
    if (length($cnames[0])) {
        print "\n";
        print "createDistributionList $alias\@$DOMAIN\n";
        if ($display_name) {
            print "modifyDistributionList $alias\@$DOMAIN displayname \"$display_name\"\n";
            $display_name = undef;
        }
        if ($description) {
            print "modifyDistributionList $alias\@$DOMAIN description \"$description\"\n";
            $description = undef;
        }
        foreach my $member (@cnames) {
            # skip aliases to certain users
            if (grep /^$member$/, @CNAME_SKIP) {
                warn "skipping cname $member <- $alias";
                next; # go to next member
            }
            # A Zimbra distribution list may contain users on remote hosts.
            if ($member =~ m/\@/) {
                print "addDistributionListMember $alias\@$DOMAIN $member\n";
            } else {
                print "addDistributionListMember $alias\@$DOMAIN $member\@$DOMAIN\n";
            }
        }
        print "\n";
        @cnames=();
        next; # go to next line of aliases file
    } 

    # skip aliases to certain users
    if (grep /^$cname$/, @CNAME_SKIP) {
        warn "skipping cname $cname <- $alias";
        next;
	}
    if ($cname =~ m/\/|\|/) {
        # alias to a file or a program. don't try to accomodate
        warn "WARNING skipping cname $cname <- $alias it is a file or pipe";
        next;
    }

    # A Zimbra alias must be associated with exactly one Zimbra account.
    if ($cname =~ m/\@/) {
        # alias to remote host. this could be created as a dist list with 1 member
        # that seems undersirable though.
        print "\n";
        print "createDistributionList $alias\@$DOMAIN\n";
        print "addDistributionListMember $alias\@$DOMAIN $cname\n";
        print "\n";
        next; # go to next line of aliases file
    }

    # are we sure that account $cname\@$DOMAIN exists?
    print "addAccountAlias $cname\@$DOMAIN $alias\@$DOMAIN\n";
}

# read an included alias file. commens with Name: and Description:
# are placed into LDAP attributes for the list
sub parse_include {
    my $cname = shift;
    my @cnames;
    # need to pull in contents of file and make a dist
    # list instead of an alias
    my $fullfile = $cname;
    $fullfile =~ s/:include://;
    my ($file,$path) = fileparse($fullfile);
    if (! -e $file) {
        warn "WARNING skipping $alias -> $cname Please run 'scp $SOURCE_HOST:$fullfile .'";
        return; # go to next line of aliases file
    } else {
        # process include file
        open(F,$file) || warn "Can not read $file";
        while (<F>) {
            chomp;
            (m/#\s*Department:*\s*(.*)/) && ($ou = $1); # can't make use of this in zmprov mdl :(
            if (m/#\s*Description:*\s*(.*)/) {
                $description = $1;
                $description =~ s/"/'/g; # don't blow our command line
            }
            if (m/#\s*Name:*\s*(.*)/) {
                $display_name = $1;
                $display_name =~ s/"/'/g; # don't blow our command line
            }
            s/#.*$//; # skip comments
            next if /^\s*$/; # skip blank lines
            push @cnames, $_;
        }
    }
    return $description, $display_name, \@cnames;
}
# example data
__DATA__
admin: root
dudes: me,you
postmaster: sarah
wsu: :include:/etc/mail/lists/wsu.list
jon.doe: jdoe

Remove Accounts

Here is a script that can be used to remove all accounts and distribution lists, save for accounts you specify. By default it leaves the admin account, the wiki account, and the ham and spam accounts.

#!/bin/bash
################################################################################
# $Id: deprovision,v 1.2 2007/02/18 20:41:43 dlbewley Exp $
#-------------------------------------------------------------------------------
# Description:
#   Script to remove all Zimbra accounts and distribution lists.
#   This may be useful for getting back to square one if you are testing bulk 
#   provisioning during a migration.
#
# See Also:
#   http://wiki.zimbra.com/index.php?title=Bulk_Create
#
# Usage:
#    su - zimbra
#    ./deprovision > deprovision.zmp
#    zmprov < deprovision.zmp
#
################################################################################

# this will be passed to grep -vE which acts as an exclude list
KEEP_ACCOUNTS='^(admin@|ham\.|spam\.|wiki@)'
# this will be passed to grep -vE which acts as an exclude list
KEEP_LISTS=''
# files to hold distlist and account lists
ACCOUNTS='accounts.zmprov'
LISTS='distlists.zmprov'

TMPA=`mktemp "$ACCOUNTS.XXXXXX"`
TMPL=`mktemp "$LISTS.XXXXXX"`

# get accounts lists
zmprov gaa > "$TMPA"
if [ -n "$KEEP_ACCOUNTS" ]; then
    grep -vE "$KEEP_ACCOUNTS" "$TMPA" > "$ACCOUNTS"
    rm "$TMPA"
else
    mv "$TMPA" "$ACCOUNTS"
fi

# get distribution lists
zmprov gadl > "$TMPL"
if [ -n "$KEEP_LISTS" ]; then
    grep -vE "$KEEP_LISTS" "$TMPL" > "$LISTS"
    rm "$TMPL"
else
    mv "$TMPL" "$LISTS"
fi

# remove accounts
cat "$ACCOUNTS" | while read account; do
    echo "da $account"
done

# remove lists
cat "$LISTS" | while read list; do
    echo "ddl $list"
done
Jump to: navigation, search