The Domino directory, (a.k.a. NAB, a.k.a names.nsf) contains configuration documents for servers and people. They store details on these resources registered within the Lotus Domino environment. A large part of the content of the Domino Directory can be made available via the Domino LDAP task which makes that data available to clients over the LDAP protocol.

Entries for people contain information such as the user's name, her email address and telephone numbers (if set), as well as the distinguished name (DN) of the user's primary server and the name of the user's mail file. A shortened LDIF output for user jdoe could produce the following:

dn: CN=John Doe,OU=marketing,O=fupps.com
    cn: John Doe
    mail: jdoe@fupps.com
    uid: jdoe
    uid: john.doe
    uid: john.q.doe
    mailserver: CN=JP510m,O=fupps.com
    mailfile: mail\jdoe
    objectclass: dominoPerson

Note how the name of the mail server is in the mailserver attribute type. Similarly, the path to the user's mail file in the mailfile attribute type.

The Domino directory also makes information about servers available via LDAP. If we query the same directory for the server's DN, we can glean some more valuable information:

dn: CN=JP510m,O=fupps.com
    cn: JP510m
    objectclass: dominoServer
    smtpfullhostdomain: domi.fupps.com
    mailserver: CN=JP510m,O=fupps.com

I've shortened this server entry very much; there is half a ton of other interesting data to be read for your servers, if you do an authenticated LDAP search on your Domino directory.

By combining the hostname contained in the smtpfullhostdomain attribute type with the mailfile attribute type, we get a path to the user's mailfile. This path can easily be used to construct an HTTP URL to the user's mail file, if I ensure that the filename ends in .nsf.

By now you'll be thinking: "so? Big deal". It isn't a big deal really, but it is quite interesting, because this shows that an authenticated query or two to the Domino directory can devulge information that we'll use to access the user's mail file.

But what about the replicas? In a clustered Lotus Domino environment, the administrator will have created at least another replica of the user's mail file so that the Lotus Notes clients can fail over to these replicas in the event the primary home server goes South. If that does happen, there is no easy way to determine the hostname of the user's replica host. Unfortunately, the name(s) of the replica servers are not published via the Domino Directory. Enter cldbdir.nsf.

The cldbdir.nsf database on Lotus Domino is the Lotus Domino Cluster Database Directory, which is managed and maintained by the Cluster Database Directory Manager. Its use is well documented, but in escense it contains documents describing which databases in a Domino cluster have replicas on which servers. So, if a user named joe has a mail file on server domin and its replica on jp510m, and both those servers are in a cluster, the cldbdir.nsf will contain two documents with the common names of those servers and the pathnames of the databases. CLDBDIR view

This information is normally only used by the cluster manager for failover of a mail file from one server to another, but I want access to that data in order to determine the names of the replica servers.

IMHO, the fastest wat to get a dump of the cluster manager database directory is with a Lotus Notes C API program, which I'm presenting here, although a LotusScript agent or a Java agent might be alternatives. The program as it stands will have to be run on one server per cluster because it accesses the cluster-specific cldbdir.nsf.

/*
 * replinfo.c (C)November 2006 by Jan-Piet Mens
 * for fupps.com. See documentation on 
 * http://jpmens.net/pages/gathering-replica-information-from-dominos-cldbdirnsf/
 *
 */

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <time.h>
#include <string.h>
#include <lapicinc.h>
#include <global.h>
#include <nsfdb.h>
#include <nsfdata.h>
#include <stdnames.h>
#include <nsfnote.h>
#include <nsfsearc.h>
#include <editods.h>
#include <osmem.h>
#include <nif.h>
#include <osmisc.h>
#include <ostime.h>
#include <nsferr.h>
#include <miscerr.h>
#include <nsfobjec.h>
#include <misc.h>
#include <lapiplat.h>
#include <osfile.h>
#include <idtable.h>

#include "map.h"

#define PROGNAME   "replinfo"
#define FILENAME   "replinfo-export.csv"
#define CLDBDIR        "cldbdir.nsf"
#define NAB        "names.nsf"

STATUS LNPUBLIC enumpeople (void *, SEARCH_MATCH *, ITEM_TABLE *);
STATUS LNPUBLIC enum_servers (void *, SEARCH_MATCH *, ITEM_TABLE *);
void textlist2str(NOTEHANDLE nh, char *dfield, char *target, char *sep);
int load_servermap(DBHANDLE dbh);
char *domfield(NOTEHANDLE nh, char *dominofieldname);
char *cldblookup(DBHANDLE db, char *viewname, char *key, char *field, char *homeserver);
void bail(STATUS err);
void dlog(char *fmt, ...);
char *dns(char *srv);

static DBHANDLE cldb;
FILE *fpout;
static MAP *servermap;           // hash table of servers

int main(int argc, char **argv)
{
    DBHANDLE nab; 
    char *formula;        
    FORMULAHANDLE fh;
    WORD wdc; 
    STATUS error;
    time_t now;

        if (error = NotesInitExtended (argc, argv)) {
                fprintf(stderr, "Unable to initialize Notes.\n");
                return (1);
        }

    if ((fpout = fopen(FILENAME, "w")) == NULL) {
        perror(FILENAME);
        exit(1);
    }

    if (error = NSFDbOpen (NAB, &nab)) {
        bail(error);
    }
    if (error = NSFDbOpen (CLDBDIR, &cldb)) {
        bail(error);
    }

    fprintf(fpout, "# Generated by JP's %s\n", PROGNAME);
    time(&now);
    fprintf(fpout, "# Begin at %s", ctime(&now));
    fprintf(fpout, "#\n#Shortnames;Mailfile;DNS-names\n");

    load_servermap(nab);

    /* Compile the selection formula. */

    formula = "(Form = \"Person\")";
    if (error = NSFFormulaCompile (
        NULL,               /* name of formula (none) */
        (WORD) 0,           /* length of name */
        formula,            /* the ASCII formula */
        (WORD) strlen(formula),    /* length of ASCII formula */
        &fh,           /* handle to compiled formula */
        &wdc,               /* compiled formula length (don't care) */
        &wdc,               /* return code from compile (don't care) */
        &wdc, &wdc, &wdc, &wdc)) /* compile error info (don't care) */

    {
        NSFDbClose (nab);
        bail(error);
    }

    if (error = NSFSearch (nab, fh,
        NULL,           /* title of view in selection formula */
        0,              /* search flags */
        NOTE_CLASS_DATA,/* note class to find */
        NULL,           /* starting date (unused) */
        enumpeople,   /* call for each note found */
                &nab,     /* argument to enumpeople */
                NULL))          /* returned ending date (unused) */
    {
        NSFDbClose (nab);
        bail(error);
    }

    OSMemFree(fh);

    if (error = NSFDbClose(nab))
        bail(error);
    if (error = NSFDbClose(cldb))
        bail(error);
    
    time(&now);
    fprintf(fpout, "# End at %s", ctime(&now));
    fclose(fpout);

    mapfreeall(servermap);

    LAPI_RETURN (NOERROR);
}

STATUS LNPUBLIC enumpeople(void far *dbh,
            SEARCH_MATCH far *pSearchMatch,
            ITEM_TABLE far *summary_info)
{
    SEARCH_MATCH SearchMatch;
    NOTEHANDLE   nh;
    STATUS       error;
    char *homeserver, *hp, *mailfile, *p;
    char pathname[1024], *bp;
    char *dnslist;

    memcpy( (char*)&SearchMatch, (char*)pSearchMatch, sizeof(SEARCH_MATCH) );

    /* Skip this note if it does not really match the search criteria
    * (it is now deleted or modified). This is not necessary for
    * full searches, but is shown here in case a starting date was
    * used in the search.
    */

    if (!(SearchMatch.SERetFlags & SE_FMATCH))
        return (NOERROR);

    if (error = NSFNoteOpen(*(DBHANDLE far *)dbh,
        SearchMatch.ID.NoteID, 0, &nh)) {
            return (ERR(error));
    }

    /* Skip user if she has no mailfile */
    if ((bp =  domfield(nh, "mailfile")) != NULL) {
        char extra[5120];

        strcpy(pathname, bp);
        if (!strstr(pathname, ".nsf")) {
            strcat(pathname, ".nsf");
        }

        mailfile = strdup(pathname);

        hp = domfield(nh, "mailserver");
        homeserver = (hp && *hp) ? strdup(hp) : NULL;


        dnslist = cldblookup(cldb,
                    "($Pathname)", pathname,
                    "server", homeserver);
        if (dnslist != NULL) {
            /* This user has replicas on this server. Add
            * the DNS of the homeserver then the rest
            */

            textlist2str(nh, "shortname", extra, ",");
            strcat(extra, ";");

            /* Normalize mailfile path for URL */
            for (p = mailfile; p && *p; p++)
                if (*p == '\\')
                    *p = '/';
            strcat(extra, mailfile);
            strcat(extra, ";");
            strcat(extra, dns(homeserver));
            strcat(extra, dnslist);
            
            fprintf(fpout, "%s\n", extra);
        }

        if (homeserver && *homeserver)
            free(homeserver);
        if (mailfile && *mailfile)
            free(mailfile);
    }

    if (error = NSFNoteClose (nh))
        return (ERR(error));

    return (NOERROR);
}

/* Read the text list `dfield' and copy each value separated by `sep'
 * into `target'.
 */
void textlist2str(NOTEHANDLE nh, char *dfield, char *target, char *sep)
{
    WORD nents, n, len;
    char buf[2048];

    if (!NSFItemIsPresent(nh, dfield,
                (WORD) strlen(dfield)))
        return;

    nents = NSFItemGetTextListEntries(nh, dfield);
    for (n = 0, *target = 0; n < nents; n++) {
        len = NSFItemGetTextListEntry(nh,
                    dfield, n, buf, sizeof(buf)-1);
        buf[len] = '\0';

        strcat(target, buf);
        if (n < (nents - 1))
            strcat(target, sep);
    }
}

/*
 * Get a list of all Domino servers with servername and their
 * hostname, and stash them away for later use.
 */

int load_servermap(DBHANDLE dbh)
{
    char *formula;
    FORMULAHANDLE fh;
    WORD wdc;
    STATUS error;

    if ((servermap = map_new()) == NULL) {
        fprintf(stderr, "Can't create servermap");
        exit(1);
    }

    formula = "(Form = \"Server\")";
    if (error = NSFFormulaCompile (
        NULL,               /* name of formula (none) */
        (WORD) 0,           /* length of name */
        formula,            /* the ASCII formula */
        (WORD) strlen(formula),    /* length of ASCII formula */
        &fh,    /* handle to compiled formula */
        &wdc,               /* compiled formula length (don't care) */
        &wdc,               /* return code from compile (don't care) */
        &wdc, &wdc, &wdc, &wdc)) /* compile error info (don't care) */

    {
        LAPI_RETURN (ERR(error));
    }

    if (error = NSFSearch (dbh, fh,
        NULL,           /* title of view in selection formula */
        0,              /* search flags */
        NOTE_CLASS_DATA,/* note class to find */
        NULL,           /* starting date (unused) */
        enum_servers,   /* call for each note found */
                &dbh,     /* argument to enumpeople */
                NULL))          /* returned ending date (unused) */
    {
        LAPI_RETURN (ERR(error));
    }

    OSMemFree(fh);

    return (0);
}

STATUS LNPUBLIC enum_servers(void *dbh, SEARCH_MATCH *pSearchMatch,
            ITEM_TABLE *summary_info)
{
    SEARCH_MATCH SearchMatch;
    NOTEHANDLE   nh;
    STATUS       error;
    char server[1024], dns[1024], *sp, *dp;

    memcpy( (char*)&SearchMatch, (char*)pSearchMatch, sizeof(SEARCH_MATCH) );

    if (!(SearchMatch.SERetFlags & SE_FMATCH))
        return (NOERROR);

    if (error = NSFNoteOpen(*(DBHANDLE far *)dbh,
        SearchMatch.ID.NoteID, 0, &nh)) {
            return (ERR(error));
    }

    strcpy(server, (sp = domfield(nh, "servername")) ? sp : "");
    strcpy(dns,    (dp = domfield(nh, "smtpfullhostdomain")) ? dp : "");

    map_stash(servermap, server, dns, 1);

    if (error = NSFNoteClose (nh))
        return (ERR(error));

    return (NOERROR);
}

char *domfield(NOTEHANDLE nh, char *dominofieldname)
{
    BOOL found;
    WORD len = 0;
    static char buf[2048];

    found = NSFItemIsPresent(nh, dominofieldname,
                (WORD) strlen(dominofieldname));

    if (found) {
        len = NSFItemGetText(nh, dominofieldname, buf, sizeof(buf) - 1);
    }

    return (len > 0 ? buf : NULL);
}

void bail(STATUS err)
{
    char str[2048], gmsg[5120];

    OSLoadString(NULLHANDLE, ERR(err), str, sizeof(str)-1);
    sprintf(gmsg, "Notes-err=0x%X [%s]",
            err, str);
    fprintf(stderr, "%s\n", gmsg);
    exit(1);
}

char *cldblookup(DBHANDLE db, char *viewname, char *key, char *field, char *homeserver)
{
    STATUS err;
    NOTEID viewid, *id_list;
    HCOLLECTION coll_handle;
    DWORD match_size, note_count = 0, notes_found, i;
    WORD signal_flag;
    HANDLE buffer_handle;
    COLLECTIONPOSITION coll_pos;
    DWORD FirstTime = TRUE;
    NOTEHANDLE nh;
    static char dnslist[5120];
    char *value;

    if (err = NIFFindView (db, viewname, &viewid)) {
        dlog("cldblookup: can't findview %s", viewname);
        return (NULL);
    }

    *dnslist = '\0';

    /* Get a collection of notes using this view. */

    if (err = NIFOpenCollection(
        db,        /* handle of db with view */
        db,         /* handle of db with data */
        viewid,        /* note id of the view */
        0,              /* collection open flags */
        NULLHANDLE,     /* handle unread ID list (input and return) */
        &coll_handle,   /* collection handle (return) */
        NULLHANDLE,     /* handle to open view note (return) */
        NULL,           /* universal note id of view (return) */
        NULLHANDLE,     /* handle to collapsed list (return) */
        NULLHANDLE))    /* handle to selected list (return) */
    {
        dlog("cldblookup: can't opencollection");
        return (NULL);
    }

    /* Look for notes that have the given primary sort key
    * (which must be of type text). We get back a COLECTIONPOSITION
    * structure describing where the first such note is in the
    * collection, and a count of how many such notes there are.
    * Check the return code for "not found" versus a real error.
    */

    err = NIFFindByName (
        coll_handle,       /* collection to look in */
        key,            /* string to match on */
        FIND_CASE_INSENSITIVE, /* match rules */
        /* another FIND_ option to add to experiment with is
       FIND_PARTIAL - to do wildcard searches             */
        &coll_pos,         /* where match begins (return) */
        &match_size);      /* how many match (return) */

    if (err || ERR(err) == ERR_NOT_FOUND) {
        // dlog("cldblookup: can't findbyname %s", key);
        NIFCloseCollection(coll_handle);
        return (NULL); 
    }

    /* Get a buffer of all the note IDs that have this key. */

    do
    {
        if (err = NIFReadEntries(
            coll_handle,         /* handle to this collection */
            &coll_pos,           /* where to start in collection */
            (WORD) (FirstTime ? NAVIGATE_CURRENT : NAVIGATE_NEXT),
            /* order to use when skipping */
            FirstTime ? 0L : 1L, /* number to skip */
            NAVIGATE_NEXT,       /* order to use when reading */
            match_size - note_count,    /* max number to read */
            READ_MASK_NOTEID,    /* info we want */
            &buffer_handle,      /* handle to info (return)   */
            NULL,                /* length of buffer (return) */
            NULL,                /* entries skipped (return) */
            &notes_found,        /* entries read (return) */
            &signal_flag))       /* signal and share warnings (return) */

        {
            dlog("cldblookup: can't readentries");
            NIFCloseCollection (coll_handle);
            return (NULL);
        }

        /* Check to make sure there was a buffer of information
        * returned.  (This is just for safety. We know that
        * some notes matched the search key.)
        */ 

        if (buffer_handle == NULLHANDLE) {
            NIFCloseCollection (coll_handle);
            return (NULL); 
        }

        /* Lock down (freeze the location) of the buffer of notes
        * IDs. Cast the resulting pointer to the type we need. */

        id_list = (NOTEID *) OSLockObject (buffer_handle);

        /* Cycle through the list of note IDs found by this search. */

        for (i=0; i < notes_found; i++) {
            if (err = NSFNoteOpen(db, id_list[i], 0, &nh)) {
                dlog("cldblookup: can't noteopen");
                return (NULL);
            }
            value = domfield(nh, field);

            if (value && homeserver && strcmp(value, homeserver)) {
                strcat(dnslist, ",");
                strcat(dnslist, dns(value));
            }

            NSFNoteClose(nh);
        }

        /* Unlock the list of note IDs. */

        OSUnlockObject (buffer_handle);

        /* Free the memory allocated by NIFReadEntries. */

        OSMemFree (buffer_handle);

        break;    /* JPM */

        if (FirstTime)
            FirstTime = FALSE;

    }  while (signal_flag & SIGNAL_MORE_TO_DO);

    /* Close the collection. */

    if (err = NIFCloseCollection(coll_handle)) {
        return (NULL);
    }

    return (dnslist);
}

void dlog(char *fmt, ...)
{
    va_list ap;
    char buf[8192];

    va_start(ap, fmt);

    sprintf(buf, "%s: ", PROGNAME);
    vsprintf(buf + strlen(buf), fmt, ap);

    fprintf(stderr, "%s\n", buf);
    va_end(ap);
}

char *dns(char *servername)
{
    char *s;

    if (!servername || !*servername)
        return ("na");

    if (!(s = map_find(servermap, servername)))
        s = "na";
    return (s);
}

The processing is as follows:

replinfo opens the Domino Directory names.nsf and the Cluster Database Directory cldbdir.nsf. It starts off by preloading a list of all servers defined in the Domino directory (servername) with their DNS names (smtpfullhostname) into a map. This list is later used as a fast cache, utilizing my wrapper for MapKit.

The program then searches for all people in the Domino Directory with the specified selection formula (Form = "Person"). For each found person in the directory, the enumpeople() function is invoked to process the person document.

enumpeople() opens the person document and extracts the user's mailfile (mail/jdoe.nsf) and mailserver (i.e. home server). With the information of the mailfile and home server, a lookup is performed in the cldbdir.nsf to find the server names of the replica servers. If this information cannot be determined (probably because this person has no replica on this server), the person is ignored, and the next document from the NAB is processed, until there are no further documents to handle.

Do note once again, that cldbdir.nsf contains replicas in the current cluster; that is the reason for which the program must be run on one partner of each cluster, in order to determine the information for all users of the domain.

If a user read from the Domino directory has mailfile replicas on this cluster (as determined by the current cldbdir.nsf, an entry for this user is created in the output file. This entry consists of a comma- separated list of shortname(s), the path to the user's mail file, and a list of the DNS domain names for the user's mail server replicas. These three entities are semicolon-separated.

# Generated by JP's replinfo
    # Begin at Fri Nov 17 23:45:31 2006
    #
    #Shortnames;Mailfile;DNS-names
    jdoe,john.doe,john.q.doe;mail/jdoe.nsf;domin.fupps.com,jp510m.fupps.com
    john;mail/john.nsf;domi.fupps.com,na
    ...
    # End at Fri Nov 17 23:45:38 2006

User jdoe has three short names in the Domino Directory. His mail file is named mail/jdoe.nsf (the backslashes typical on Windows have been normalized) and there is a replica on both servers domin and jp510m. User john has a single shortname and also has a replica on two servers, the second one is marked as na (not available) because the server document holds no smtpfullhostdomain field.

At this point it is important to realize that the information might not be complete. If a user has a replica of her mail file on a server distant to this cluster, the information will not be contained in the output. Furthermore, the "formula" depends on the paths to the user's mail files being identical on each and every server in the cluster (IMHO there is no valid reason not to adhere to that convention).

The resulting output contains information about the users on the cluster the program is run on. I'll be storing this information in a relational database (RDBMS) for easy and fast access and later copying the data into custom made LDAP attribute types of an LDAP directory. This trivial task is left as an excercise to the reader (hint: use Perl).

In a subsequent article, I will be discussing how I use the data gathered by replinfo to build a resilient jump to database.

Comments

blog comments powered by Disqus