Exchange Store méretértesítő

A napokban egy nagyon kellemetlen élményben volt részem. Az egyik Exchange szerverünkön betelt a Mailbox Store. Minden előzetes figyelmeztetés nélkül lekapcsolt amikor elérte a 16GB méretet. Alapvetően én voltam az eszetlen mert nem néztem meg, hogy a törölt elemeket még 7 napig tárolja és meg voltam győződve róla, hogy nem telhet be hiszen most töröltek 3GB körüli levelet a felhasználók. Néhány órás munkával helyreraktam a dolgot, de ennek ellenére nem tetszik, hogy a rendszer nem figyelmeztet a közelgő veszélyre. Ezt a gondot kiküszöbölendő írtam egy scriptet ami időnként tájékoztat az állapotról.
Keresgéléseim során a következőre jutottam:
Nem találtam olyan funkciót a rendszerben ami le tudná kérdezni a store valós méretét (ha valaki tud ilyet az árulja el nekem, megköszönném). Végül egy megkerülő megoldást sikerült kitalálnom a dologra. Az Exchange Store időközönként végez egy online defragmentációt az adatbázisokon (alap beállítás szerint éjjel 2 és 6 között). Amikor ezt a műveletet befejezi berak egy bejegyzést az Application eventlog-ba ami valahogy így fog kinézni:

Ebben láthatunk egy szabad hely értéket. Nem kell megijedni attól, hogy ez az érték esetleg kicsi. Nem a 16GB-ig hátralévő helyet jelöli, hanem az adatbázis fájlokban (összetartozó .edb és .stm) fájlokban jelenleg meglévő üres helyet jelöli.
Az az időpillanat amikor a fenti eventlog bejegyzés megtörténik az egyetlen pillanat amikor valójában meg tudjuk állapítani, hogy mennyi adatunk van a store-ban.
Ennek alapján a megoldás a következőképpen működik:
Figyeli a rendszert, hogy mikor kerül be neki megfelelő eventlog bejegyzés az Application logba. Ekkor megkeresi a store-hoz tartozó adatbázisfájlokat, összeadja a méretüket és az összegből levonja a logbejegyzésben szereplő számot. Ez lesz az adott pillanatban a store valós mérete. Erről értesítést küld e-mailben a rendszergazdának. A megoldás két részből áll:
Az egyik egy .mof fájl amit a wmi repositoryba kell elhelyezni a mofcomp segítségével:
mofcomp storsize.mof
Ennek a kódnak az a feladata, hogy elindítsa a scriptünket amikor a fent látható logbejegyzés megtörténik.

storesize.mof
#pragma namespace ("\\\\.\\root\\subscription")
#pragma autorecover
 
instance of ActiveScriptEventConsumer as $Cons
{
    Name = "ExchStoreEventSink";
    ScriptingEngine = "JScript";
    ScriptFileName = "c:\\Scripts\\ExchStore\\es.js";
};
 
instance of __EventFilter as $Filt
{
    Name = "ExchStoreEventFilt";
    Query = "SELECT * FROM __InstanceCreationEvent WHERE TargetInstance ISA 'Win32_NTLogEvent' AND TargetInstance.Logfile = 'Application' AND TargetInstance.EventIdentifier = 1074136261";
    QueryLanguage = "WQL";
    EventNamespace = "root\\cimv2";
};
 
instance of __FilterToConsumerBinding
{
    Filter = $Filt;
    Consumer = $Cons;
};
A másik a JScript fájl ami a tulajdonképpeni script. A script futtatásához szükségünk lesz egy html template állományra, amiben a script küldéskor lecseréli a változó értékeket az aktuális állapotnak megfelelően (a letöltések között mellékeltem egy minta állományt).
A különböző template fájlban használható helyettesítő tag-ek és jelentésük:
tag leírás
<!--ONLINE_FREE--> Az online defragmentálás során talált üres terület
<!--STORE_NAME--> A Store neve <Storage Group>\<Store Name> formában
<!--EDB_PATH--> Az EDB adatbázisfájl elérési útvonala
<!--STM_PATH--> Az STM adatbázisfájl elérési útvonala
<!--EDB_SIZE--> Az EDB fájl mérete
<!--STM_SIZE--> Az STM fájl mérete
<!--FILE_SIZE--> Az adatbázisfájlok össz mérete
<!--STORE_SIZE--> A Store valós mérete
<!--STORE_FREE--> A Store-ban lévő valós szabad hely
A scripten belül állítható paraméterek:
változó neve értelmezése
MsgLocale Meghatározza, hogy milyen kódkészletet használjon a script a levélküldéshez
MailSubject A küldött levél címe
MailSender Annak a felhasználónak (rendszergazda) az e-mail címe akinek a nevében a leveleket küldjük
MailReceiver Annak a felhasználónak (rendszergazda) az e-mail címe akinek kapja a leveleket
FileDir Annak a könyvtárnak a neve, ahol a template htm fájlok találhatók

es.js
/* 
 THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
 EITHER EXPRESSED OR IMPLIED,  INCLUDING BUT NOT LIMITED TO THE IMPLIED
 WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.

 This code is free for both personal and commercial use, but you are
 expressly forbidden from selling.

 **************************************************************************

 FILE NAME:
   es.js

 DESCRIPTION:
   This scripts used to send Exchange Store size information to the
   administrative person.
   
 COPYRIGHT:
   Copyright (c) Zoltán Gömöri 2005.
   All rights reserved.
   
 NOTES:
   The original version of this source code and the article that
   includes this source code can be found at:
     http://www.gomori.hu

 CREATED:
   2005.04.05
   
 LAST MODIFIED:
   2005.04.29
   
 VERSION:
   v1.0 Build 3
   
TO DO:

 **************************************************************************
*/

// Declaration
var ForReading = 1;
var CONST_MB = 1024 * 1024;
// E-Mail parameters
var MsgLocale = "iso-8859-2";
var MailSubject = "Mailbox Store méret";
var MailSender = "administrator@domain.hu";
var MailReceiver = "administrator@domain.hu";
// HTML template
var FileDir = "c:\\Scripts\\ExchStore\\"
var FileTemplate = "storesize.htm";

// Initialization (Some special objects requires preinitialization)
Template_Init();

//Main code

var IsPublic;
var IsFound;
var InsertionStrArr;
var StoreParams;
var _ExStore;
var EDBSize;
var STMSize;
var FileSize;
var StoreSize;
var StoreFree;
var CompName;
var WshNetwork;

// Catch the Event Object from the mof file
var objEvent;
objEvent = TargetEvent.TargetInstance;


WshNetwork = new ActiveXObject("Wscript.Network");
CompName = WshNetwork.ComputerName;

// Determine if the sender of the 1221 Apllication event is the
// Public or the Mailbox Store
IsFound = false;
if(objEvent.SourceName == "MSExchangeIS Public Store")
{
    IsPublic = true;
    IsFound = true;
}
if(objEvent.SourceName == "MSExchangeIS Mailbox Store")
{
    IsPublic = false;
    IsFound = true;
}
// If we found a Store event
if(IsFound)
{
    StoreParams = new Template();
    if(objEvent.InsertionStrings != null)
    {
        InsertionStrArr = objEvent.InsertionStrings.toArray();
        // Find the store CDOEXM object
        _ExStore = FindStore(InsertionStrArr[1],IsPublic);
        // Get the physical file sizes
        EDBSize = GetFileSizeMB(_ExStore.DBPath);
        STMSize = GetFileSizeMB(_ExStore.SLVPath);
        // Calculate the physical file size
        FileSize = EDBSize + STMSize;
        // Calculate the logical store size
        StoreSize = FileSize - InsertionStrArr[0];
        // Add the various data to the template object
        StoreParams.Add(/<!--ONLINE_FREE-->/,InsertionStrArr[0]);
        StoreParams.Add(/<!--STORE_NAME-->/,InsertionStrArr[1]);
        StoreParams.Add(/<!--EDB_PATH-->/,_ExStore.DBPath);
        StoreParams.Add(/<!--STM_PATH-->/,_ExStore.SLVPath);
        StoreParams.Add(/<!--EDB_SIZE-->/,EDBSize.toString());
        StoreParams.Add(/<!--STM_SIZE-->/,STMSize.toString());
        StoreParams.Add(/<!--FILE_SIZE-->/,FileSize.toString());
        StoreParams.Add(/<!--STORE_SIZE-->/,StoreSize.toString());
        StoreParams.Add(/<!--COMP_NAME-->/,CompName);
        // These parameters can be used only if we have an Exchange Standard Edition
        StoreFree = 16384 - StoreSize;
        StoreParams.Add(/<!--STORE_FREE-->/,StoreFree.toString());
        // Send e-mail
        SendMail();
    }
}

/*
    FindStore(storename,ispublic)
    It will search for the Exchange Server CDOEXM Store object
    storename - Name of the store in <Storage Group>\ format
    ispublic - Determine if the Store what we looking for is a public store
             - or a Mailbox Store
*/
function FindStore(storename,ispublic)
{
    var i,j;
    
    var ExServer;
    var ExStorageGroup;
    var ExStore;
    
    var StorageGroupArr;
    var StoreArr;
    
    // Create an Exchange Server object
    ExServer = new ActiveXObject("CDOEXM.ExchangeServer");
    // Create a Storage Group object
    ExStorageGroup = new ActiveXObject("CDOEXM.StorageGroup");
    // Create a Store Object (Public or Mailbox depending on the function parameter)
    ExStore = new ActiveXObject(ispublic?"CDOEXM.PublicStoreDB":"CDOEXM.MailboxStoreDB");
    // Open the local computer's Exchange Server Object
    ExServer.DataSource.Open(CompName);
    StorageGroupArr = ExServer.StorageGroups.toArray();
    // iterate thru the Storage Groups
    for(i=0;i<StorageGroupArr.length;i++)
    {
        // Open the actual Storage Group
        ExStorageGroup.DataSource.Open(StorageGroupArr[i]);
        // Create a Store Array (Public or Mailbox depending on the function parameter)
        StoreArr = ispublic?ExStorageGroup.PublicStoreDBs.toArray():ExStorageGroup.MailboxStoreDBs.toArray();
        // iterate thru the Stores
        for(j=0;j<StoreArr.length;j++)
        {
            // If we found the Store, return it.
            if(CompareStoreName(storename,StoreArr[j]))
            {
                ExStore.DataSource.Open(StoreArr[i]);
                return ExStore;
            }
        }
    }
    // Store not found: return null
    return null;
}

/*
    GetFileSizeMB(filespec)
    Determine a file size in megabytes
*/
function GetFileSizeMB(filespec)
{
    var FSO,f;
    FSO = new ActiveXObject("Scripting.FileSystemObject");
    f = FSO.GetFile(filespec);
    return Math.floor(f.Size / CONST_MB);
}

/*
    CompareStoreName(name,dn)
    Compare an Exchange Store Object's two diferent type names
    name - Store name in <Storage Group>\ format
    dn - Active Directory Distingused name
*/
function CompareStoreName(name,dn)
{
    var _NameArr;
    var _Name;
    _NameArr = dn.split("CN=",3);
    _Name = _NameArr[2].substr(0,_NameArr[2].length-1) + "\\" + _NameArr[1].substr(0,_NameArr[1].length-1);
    return name == _Name;
}

// Template class definition
// Initializator
function Template_Init()
{
    Template.prototype.Add = _Template_Add;
    Template.prototype.ReplaceValue = _Template_ReplaceValue;
    Template.prototype.Substitute = _Template_Substitute;
}
// Constructor
function Template()
{
    this._SubstArr = new Array();
    this._SubstCount = 0;
}

// Add method
function _Template_Add(_regex,_value)
{
    this._SubstArr[this._SubstCount] = new Object();
    this._SubstArr[this._SubstCount].regex = _regex;
    this._SubstArr[this._SubstCount].value = _value;
    this._SubstCount++;
    return this._SubstCount -1;
}

// ReplaceValue method
function _Template_ReplaceValue(_index,_value)
{
    this._SubstArr[_index].value = _value;
}

// Substitute method
function _Template_Substitute(_string)
{
    var i;
    var _retvalue;
    _retvalue = _string;
    for(i=0;i<this._SubstArr.length;i++)
        _retvalue = _retvalue.replace(this._SubstArr[i].regex,this._SubstArr[i].value);
    return _retvalue;
}

/*
    SendMail()
    It sends a mail message.
*/
function SendMail()
{
    var Msg = CreateLocalizedCDOMessage(MsgLocale);
    Msg.Subject = StoreParams.Substitute(MailSubject);
    Msg.To = MailReceiver;
    Msg.From = MailSender;
    Msg.Sender = MailSender;
    Msg.HTMLBody = StoreParams.Substitute(LoadFile(FileDir + FileTemplate));
    Msg.Send();
}
/*
    LoadFile(FileSpec)
    It loads the content of a text file to a string
    (used to load the html templates to the memory)
*/
function LoadFile(FileSpec)
{
    var FSO = new ActiveXObject("Scripting.FileSystemObject");
    var f = FSO.OpenTextFile(FileSpec,ForReading);
    var retvalue = f.ReadAll();
    f.close();
    return retvalue;
}
/*
    CreateLocalizedCDOMessage(Locale)
    Creates a correctly localized empty CDO.Message object.
    When a CDO.Message object instantiated the default codepage will be iso-8859-1.
    Use this function if you want to change that.
*/
function CreateLocalizedCDOMessage(Locale)
{
    var _Msg = new ActiveXObject("CDO.Message");
    _Msg.Fields("urn:schemas:mailheader:content-type") = "text/plain; charset=\"" + Locale + "\"";
    _Msg.Fields("urn:schemas:httpmail:content-type") = "text/plain; charset=\"" + Locale + "\"";
    _Msg.Fields.Update();
    return _Msg;
}