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.
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) */
¬es_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.