[comp.os.vms] HOG.C -- Who is hogging disk space

HOLLINGE@SASK.BITNET.UUCP (06/05/87)

This is my first contribution to world-wide public domain utilities.
(That means don't flame me too hard if I screw it up.)  The program I
have written is designed to be used in system administration.  It takes
a disk and communicates with the disk ACP to determine who is using how
much space and reports it in a (hopefully) useful fashion.  I have
tested it on VMS 4.3 and 4.5 with DEC RA81s and RM03s on VAX 78Xs and
86XXs.  I see no reason why it shouldn't work on any VAX running VMS 4.X
and any disk using the standard disk ACP.  The source code is written in
VAX-C (since I was originally a UNIX-hacker).  Please give me some
feedback on how useful/bad/idiotic/entertaining this program is to you.

Glenn Hollinger,                   hollinger@sask.BITNET
Systems Programmer,                glenn@sask.UUCP
University of Saskatchewan.        ihnp4!sask!glenn

$!-----------------------Cut Here--------------------------------
$! This is a shar file for VMS.  Cut on this line and feed it to
$! the DCL command interpreter.  Have a nice day.
$!
$! Start of HOG.C
$ Create HOG.C
/*
 * HOG -- Who's hogging all the disk space?
 *
 *    By Glenn L. Hollinger, University of Saskatchewan
 *
 * This program is provided free for the use of anyone who happens to lay hands
 * upon a copy.  It may be freely distributed except for profit.  A copy of
 * this notice must be included in all copies.
 *
 * Briefly, This program uses QIOs to talk to the disk ACP about quotas.
 * To get any info, quotas must be enabled on the disk in question.  To
 * retrieve infomation about quotas, the user must also have read access
 * to the quota file on that disk.  Queries are specified by disk and by
 * how many biggest users to report.  Reporting may be limited to users
 * over or under a certain usage.  Output may be redirected on the command
 * line.
 *
 * Output includes the identifier (or UIC), disk usage, permanent quota,
 * Overdraft allowed, and usage as a percentage of total disk space, used
 * disk space, and permanent quota.
 */

/*
 * Instructions: Compile this module, use set command on the cld and link
 *   A few simple steps, add water and stir:
 *      $ cc hog.c
 *      $ set command /object hog_table.cld
 *      $ link hog.obj,hog_table.obj,sys$input:/opt
 *        sys$library:vaxcrtl/l
 *   Optionally, add the help file to the system help library
 *      $! Change this command for your particular help file setup
 *      $! library /replace sys$help:vms.hlb hog.hlp
 *   Set up a symbol and try it out:
 *      $ hog :== $ disk:[directory]hog.exe
 *      $ hog disk
 */

#include <stdio.h>
#include <iodef.h>
#include <dvidef.h>
#include <fibdef.h>
#include <ssdef.h>
#include <descrip.h>
#include <climsgdef.h>

#define PERCENT(x,y)        (y ? ((float) x * 100) / y : 100.0)
#define NULL                0
#define EOS                '\0'
#define TRUE                1
#define FALSE                0

/* Slightly modified fibdef1 from <fibdef.h> */
struct fibdef2
{
        unsigned char fibdef$$_fill_14 [16];
        int fib$l_wcc;
        char fibdef$$_file_15 [2];
        unsigned short int fib$w_cntrlfunc;
        union
        {
                unsigned long int fib$l_cntrlval;
                struct
                {
                        unsigned fib$v_all_mem : 1;
                        unsigned fib$v_all_grp : 1;
                        unsigned fib$v_mod_use : 1;
                        unsigned fib$v_mod_perm : 1;
                        unsigned fib$v_mod_over : 1;
                        unsigned fib$v_fill_4 : 3;
                } fib$r_cntrlval_bits;
        } fib$r_cntrlval_overlay;
};

int        sys$assign(),
        sys$qiow(),
        sys$dassgn(),
        sys$idtoasc(),
        cli$parse(),
        cli$present(),
        cli$get_value(),
        lib$getdvi();
        lib$get_foriegn(),
        lib$get_input();

int        hog(),
        note_usage(),
        display_usage();

extern char hog_table();

/* How many of the top users to report */
static int top,
/* Flag /OVER and /UNDER qualifiers */
        over_lim,
        under_lim;
/* Report users over this limit */
static unsigned int over_val,
/* Report users under this limit */
        under_val;

/* Number of blocks on the disk examined */
static unsigned int max_blocks;

main(/* argc,argv unused */)
{
        /* System call return status */
        unsigned int status;
        /* Max command line length */
        unsigned short int command_l = 1024,
        /* Length of value returned by cli$get_value */
                value_l;
        /* Temporary signed-unsigned conversion variable */
        int i;

        /* The disk to work on */
        char        disk[1025],
        /* The output file (or device) */
                output[1025],
        /* The foriegn command line */
                command_s[1025],
        /* The item to look up in cli$present, etc */
                item_s[1030],
        /* Value returned by cli$get_value */
                value_s[1025];

        /* Fixed length string desc. */
        $DESCRIPTOR(command,command_s);
        $DESCRIPTOR(item,item_s);
        $DESCRIPTOR(value,value_s);

        /* Get the foriegn command line (if any) */
        status = lib$get_foreign(&command,NULL,&command_l,NULL);
        if (status != SS$_NORMAL)
        {
                printf("Error: cannot parse command line\n");
                exit(1);
        }
        command_s[command_l] = EOS;

        /* Build the real command, "HOG 'foreign command'" */
        strcpy(item_s,"HOG ");
        strcat(item_s,command_s);
        item.dsc$w_length = strlen(item_s);

        /* Run the command through the parser */
        status = cli$dcl_parse(&item,hog_table,
                                        lib$get_input,lib$get_input,NULL);
        if (status != CLI$_NORMAL)
        {
                printf("Error: cannot parse command line\n");
                exit(status);
        }

        /* Now the important stuff, which disk? */
        strcpy(item_s,"DISK");
        item.dsc$w_length = strlen(item_s);
        status = cli$present(&item);
        if (status == CLI$_PRESENT)
        {
                status = cli$get_value(&item,&value,&value_l);
                if (status != SS$_NORMAL)
                {
                        printf("Error: cannot determine disk\n");
                        exit(status);
                }
                value_s[value_l] = EOS;
                strcpy(disk,value_s);
        }
        else
                strcpy(disk,"SYS$DISK:");

        /* Which output file? */
        strcpy(item_s,"OUTPUT");
        item.dsc$w_length = strlen(item_s);
        status = cli$present(&item);
        if (status == CLI$_PRESENT)
        {
                status = cli$get_value(&item,&value,&value_l);
                if (status != SS$_NORMAL)
                {
                        printf("Error: cannot determine /OUTPUT value\n");
                        exit(status);
                }
                value_s[value_l] = EOS;
                strcpy(output,value_s);
        }
        else
                strcpy(output,"SYS$OUTPUT:");

        /* How many users to report? */
        strcpy(item_s,"TOP");
        item.dsc$w_length = strlen(item_s);
        status = cli$present(&item);
        if (status == CLI$_PRESENT)
        {
                status = cli$get_value(&item,&value,&value_l);
                if (status != SS$_NORMAL)
                {
                        printf("Error: cannot determine /TOP value\n");
                        exit(status);
                }
                value_s[value_l] = EOS;
                top = atoi(value_s);
        }
        else
                top = 0;

        /* Over what usage limit */
        strcpy(item_s,"OVER");
        item.dsc$w_length = strlen(item_s);
        status = cli$present(&item);
        if (status == CLI$_PRESENT)
        {
                status = cli$get_value(&item,&value,&value_l);
                if (status != SS$_NORMAL)
                {
                        printf("Error: cannot determine /OVER value\n");
                        exit(status);
                }
                value_s[value_l] = EOS;
                i = atoi(value_s);
                if (i >= 0)
                {
                        over_val = i;
                        over_lim = TRUE;
                }
                else
                {
                        over_val = 0;
                        over_lim = FALSE;
                }
        }
        else
        {
                over_val = 0;
                over_lim = FALSE;
        }

        /* Under what usage limit */
        strcpy(item_s,"UNDER");
        item.dsc$w_length = strlen(item_s);
        status = cli$present(&item);
        if (status == CLI$_PRESENT)
        {
                status = cli$get_value(&item,&value,&value_l);
                if (status != SS$_NORMAL)
                {
                        printf("Error: cannot determine /UNDER value\n");
                        exit(status);
                }
                value_s[value_l] = EOS;
                i = atoi(value_s);
                if (i >= 0)
                        under_val = i;
                else
                        under_val = 0;
                under_lim = TRUE;
        }
        else
        {
                under_val = 0;
                under_lim = FALSE;
        }

        /* Now, the whole task is done in one step (;-) */
        hog(disk,output);

        /* Done. */
        return;
}

/* Here is where all the work gets done. */
hog(disk,output)
        /* Disk to report */
char        *disk,
        /* Output device */
        *output;
{
        /* Output file descriptor */
        FILE *lis;
        /* SYS$ function return codes */
        unsigned int status,
        /* Item to check in SYS$GETDVI */
                dvi_item;
        /* Device I/O channel */
        unsigned short int chan,
        /* Length of returned QFTB block */
                len,
        /* SYS$QIOW fuction request */
                func;

        /* ACPCONTROL success status */
        struct
        {
                short int status;
                short int iosb$$unused1;
                int iosb$$unused2;
        } iosb;

        /* Quota File Transfer Block (QFTB) */
        struct qftb
        {
                unsigned int dqf$l_flags,
                /* User */
                        dqf$l_uic,
                /* Current usage of user */
                        dqf$l_usage,
                /* Permanent Quota of user */
                        dqf$l_permquota,
                /* Overdraft Quota of user */
                        dqf$l_overdraft,
                        dqf$$l_spare[3];
        }
        /* Input QFTB (unused) */
                in_qftb_v = {0},
        /* Output QFTB */
                out_qftb_v = {0};

        /* ACPCONTROL FIB block */
        struct fibdef2 fib_s;

        /* Fixed length string descriptors */
        $DESCRIPTOR(device,disk);
        $DESCRIPTOR(in_qftb,&in_qftb_v);
        $DESCRIPTOR(out_qftb,&out_qftb_v);
        $DESCRIPTOR(fib,&fib_s);

        /* Ensure correct lengths */
        in_qftb.dsc$w_length = sizeof(in_qftb_v);
        out_qftb.dsc$w_length = sizeof(out_qftb_v);
        fib.dsc$w_length = sizeof(fib_s);
        device.dsc$w_length = strlen(disk);

        /* Access the disk to report on */
        status = sys$assign(&device,&chan,NULL,NULL);
        if (status != SS$_NORMAL)
        {
                 printf("Error: cannot access device %s\n",disk);
                exit(status);
        }

        /* How many disk blocks are available? */
        dvi_item = DVI$_MAXBLOCK;
        status = lib$getdvi(&dvi_item,&chan,NULL,&max_blocks,NULL,NULL);
        if (status != SS$_NORMAL)
        {
                printf("Error: cannot access device %s\n",disk);
                exit(status);
        }

        /* Establish the output file */
        if ((lis = fopen(output,"w")) == NULL)
        {
                printf("Error: Cannot open %s for output\n",output);
                exit(1);
        }

        /* We want to control the acp, */
        func = IO$_ACPCONTROL;

        /* and examine all quotas... */
        fib_s.fib$w_cntrlfunc = FIB$C_EXA_QUOTA;

        /* and iterate on all UICs, */
        fib_s.fib$l_wcc = 0;
        fib_s.fib$r_cntrlval_overlay.fib$r_cntrlval_bits.fib$v_all_mem = 1;
        fib_s.fib$r_cntrlval_overlay.fib$r_cntrlval_bits.fib$v_all_grp = 1;

        for (;;)
        {
                /* OK, ask for a piece of quota info */
                status = sys$qiow(NULL,chan,func,&iosb,NULL,NULL,
                        &fib,&in_qftb,&len,&out_qftb,NULL,NULL);

                /* Check for transmission errors */
                if (status != SS$_NORMAL)
                {
                        printf("Error: cannot access diskquota information\n");
                        exit(status);
                }

                /* Check ACPCONTROL return status */
                if (iosb.status == SS$_NODISKQUOTA)
                        break;
                if (iosb.status != SS$_NORMAL)
                {
                        printf("Error: cannot access Quota File\n");
                        exit((int) iosb.status);
                }

                /* Write it down for later reporting */
                note_usage(out_qftb_v.dqf$l_uic,out_qftb_v.dqf$l_usage,
                        out_qftb_v.dqf$l_permquota,out_qftb_v.dqf$l_overdraft);
        }

        /* Write the output */
        display_usage(lis);

        /* That's the listing file */
        fclose(lis);

        /* And let go of the device */
        status = sys$dassgn(chan);
        if (status != SS$_NORMAL)
        {
                printf("Error: cannot deassign channel\n");
                exit(status);
        }
};

/* Linked list block definition */
struct q_list
{
        union
        {
                /* remap the long UIC to 2 shorts */
                unsigned int full;
                struct
                {
                        unsigned short int member;
                        unsigned short int group;
                } sub;
        } uic;
        unsigned int usage,
                perm,
                over;
        struct q_list *next;
};

/* The list head pointer, NULL for no list.
 * Note the list is in increasing
 * order, not decreasing as most would expect.
 */
static struct q_list *list_head = (struct qlist *) NULL;

/* How many are currently in the list? */
static int count = 0;

/* Track total disk block usage */
static int total_usage = 0;

/* Look at this piece of the usage info.  If we may want to display it,
 * keep it for later.  If not, discard.  Also discard any info we kept
 * before and are now certain we don't want to display
 */
int note_usage(uic,usage,perm,over)
unsigned int uic, usage, perm, over;
{
        /* Tango down the linked list */
        register struct q_list *p, *q;

        /* Note the additional disk block usage */
        total_usage += usage;

        /* If we have too much or too little usage, ignore them */
        if ((over_lim && usage <= over_val)||(under_lim && usage >= under_val))
                        return;

        /* Find the place in the list to insert the new info
         * Itterate down the list until the node after the current node
         * is greater than the new usage and the current node is less
         */
        p = list_head;
        if (p != NULL)
        {
                while (p -> next != NULL)
                {
                        if (p -> next -> usage > usage) break;
                        p = p -> next;
                }
        }

        /* p is either NULL (no list), or points to the first node, */
        /* or points to the one to insert after */
        if (top != NULL && count == top)
        {
                /* There must be at least one node in the list at this point */

                /* Skip the insert if we have enough nodes and this is */
                /* less than the minimum usage in the list. */
                if (usage <= list_head -> usage)
                        return;
                /* strip the first node off and reuse it */
                q = list_head;
                if (p == list_head)
                        p = list_head -> next;
                list_head = list_head -> next;
        }
        else
        {
                /* make a new node and insert it */
                q = (struct q_list *) malloc(sizeof(struct q_list));
                count ++;
        }

        /* lace into list */
        if (p == NULL)
        {
                        /* There is no list yet, make one */
                q -> next = NULL;
                list_head = q;
        }
        else if (p -> usage > usage)
        {
                        /* usage is less than listhead (p == listhead) */
                q -> next = p;
                list_head = q;
        }
        else
        {
                        /* usage is more than p, and less than p -> next */
                q -> next = p -> next;
                p -> next = q;
        }

        /* fill in the new data */
        q -> uic.full = uic;
        q -> usage = usage;
        q -> perm = perm;
        q -> over = over;

        return;
};

/* Take the list of usage to display and display it */
int display_usage(lis)
FILE *lis;
{
        /* Move down the list printing items */
        struct q_list *p;
        /* Length of translated identifier name */
        unsigned short int namlen;
        /* System call return status */
        unsigned int status;
        /* The identifier name returned */
        char nambuf_s[50];
        /* Percentage calculation variables */
        float percent_used,
                percent_total,
                percent_perm;

        /* Fixed length string descriptor */
        $DESCRIPTOR(nambuf,nambuf_s);

        /* This should really by paginated and page-headed (frown)  */
        fprintf(lis,
        "Identifier\t\t   Usage   Perm     Over  %% Used %% Total  %% Perm\n");

        /* Look down the list (smallest usage to largest) */
        p = list_head;
        while (p != NULL)
        {
                /* Calculate usage percentages, watch for divide by 0 */
                percent_used = PERCENT(p -> usage, total_usage);
                percent_total = PERCENT(p -> usage, max_blocks);
                percent_perm = PERCENT(p -> usage, p ->perm);

                /* Try to find an identifier to name this uic with */
                status = sys$idtoasc(p -> uic.full,&namlen,&nambuf,
                        NULL,NULL,NULL);

                /* Use identifier if it exists, else use the numeric format */
                if (status == SS$_NORMAL)
                {
                        nambuf_s[namlen] = EOS;
                        fprintf(lis,"%-24s %7d %7d %7d %7.2f %7.2f %7.2f\n",
                                nambuf_s,
                                p -> usage, p -> perm, p -> over,
                                percent_used,percent_total,percent_perm);
                }
                else
                {
                fprintf(lis,"[%06o,%06o] \t %7d %7d %7d %7.2f %7.2f %7.2f\n",
                                p -> uic.sub.group, p -> uic.sub.member,
                                p -> usage, p -> perm, p -> over,
                                percent_used, percent_total, percent_perm);
                }

                /* And the next... */
                p = p -> next;
        }
};
$! Start of HOG_TABLE.CLD
$ Create HOG_TABLE.CLD
module hog_table
define verb hog
        routine hog
        parameter p1 label=disk value(default=sys$disk)
        qualifier top value(default=20)
        qualifier output value(default=hog.lis)
        qualifier over value(default=4000)
        qualifier under value(default=4001)
$! Start of HOG.HLP
$ Create HOG.HLP
1 HOG
    HOG is a utility to display who is using how much disk space on the
disk drives.  It will take one optional parameter and two optional
qualifiers.  The general action is to examine the quotas on a disk and
report them ordered by usage.  The parameter names the disk to be
examined.  The qualifiers can be used to specify an output file and
limit the display to a number of top users.  Usage is as follows;

        $ HOG [disk] [/OUTPUT[=file]] [/TOP[=count]]
                [/OVER[=blk_cnt]] [/UNDER=[=blk_cnt]]
2 Usage_Notes
    HOG will only report on disk volumes which have quotas enabled on
them.  This is a restriction because of how HOG works.  To change this,
a different approach must be used, either reading QUOTA.SYS or
INDEXF.SYS directly.  For a user to get information from HOG, they must
either 1. own the disk volume queried or 2. have read access to the
quota file on the disk volume queried.

    Do not install this program with privileges, since it does not
bother to check whether or not it is OK to write the output file as the
user specifies.  It could trash any file on the system.

    Potentially, a very large pagefile quota could be required to do a
complete HOG listing of a disk with very many quotas on it.  However, we
have a disk with over 1200 quota entries which requires a pagefile quota
of 3000 blocks, so we expect only the most extreme cases will have
problems with this.
2 Sample_Output
    HOG output is in 7 columns.  The columns are as follows, in order;

        UIC     -- The UIC this line of output pertains to
        Usage   -- The disk space used in blocks
        Perm    -- The disk space permanently allocated to the UIC
        Over    -- The temporary overdraft allowed
        % Used  -- The percentage Usage is of the total used space
        % Perm  -- The percentage Usage is of the uic's permanent allocation
        % Total -- The percentage Usage is of total disk space

    See Examples for sample output
2 Defaults
    The defaults for HOG are to report on all user quotas on the current
(default) disk.  This is the same as the command:

        $ HOG SYS$DISK /TOP=0 /OVER=-1 /UNDER=9999999
2 Examples
    As an example, consider a disk with the following quotas:

        Identifier           Usage     Permanent Overdraft
        [000000,000000]          0        1000       100
        [SYSTEM]             40000      100000      1000
        [USER1]               5000       30000      1000
        [USER2]               3000       30000      1000
        [USER3]             100000      400000      1000

    Listed below are a number of commands and the users those commands
would include in the report generated.

        $ HOG                                     All UICs
        $ HOG /TOP=2                              [USER3],[SYSTEM]
        $ HOG /TOP=3 /UNDER=90000                 [SYSTEM],[USER1],[USER3]
        $ HOG /OVER=10000                         [USER3],[SYSTEM]
        $ HOG /TOP=3 /UNDER=90000 /OVER=10000     [SYSTEM]

    To redirect the report to a file, use the /OUTPUT qualifier;

        $ HOG /OUTPUT=HOG.LIS

    The following is a sample of output from the HOG command

Identifier                   Usage   Perm     Over  % Used % Total  % Perm
[000000,000000]                0    1000     100    0.00    0.00    0.00
HOLLINGER                      0  999999   99999    0.00    0.00    0.00
[000001,000001]                9    1000     100    0.00    0.00    0.90
SYSTEM                     40003  999999   99999    5.65    4.49    4.00
VLSI                      668571  999999   99999   94.35   75.03   66.86

2 DISK
    The disk you specify as the parameter to HOG determines the disk
reported on.  If no disk is specified, the default disk, SYS$DISK:, is
assumed.
2 /OUTPUT[=HOG.LIS]
    The default output is to the terminal via SYS$OUTPUT.  If /OUTPUT is
specified without a value, the output is directed to the file HOG.LIS in
the current directory.  A value may be given with the /OUTPUT qualifier
to change the name and directory of the file created.
2 /TOP[=count]
    The default action of HOG is to report all user quotas on the disk
being examined.  The /TOP qualifier can be used to restrict the analysis
to the biggest users on the disk being examined.  If /TOP is used
without a value, the value 20 is assumed.  Specifying the value of /TOP
as 0 will cause all user quotas to be reported.  The /OVER and /UNDER
qualifiers are used to limit the users reported on before the /TOP
qualifier is considered.
2 /OVER[=blk_cnt]
    The default action of HOG is to report user quotas regardless of the
usage of each UIC.  The /OVER qualifier can be used to restrict the
report to UICs with usage over a certain limit.  The unit for blk_cnt is
disk blocks.  If /OVER is used without a value, 4000 is assumed.  The
qualifiers /OVER and /UNDER are used to limit the users reported on
before the /TOP qualifier is considered.
2 /UNDER[=blk_cnt]
    The default action of HOG is to report user quotas regardless of the
usage of each UIC.  The /UNDER qualifier can be used to restrict the
report to UICs with usage under a certain limit.  The unit for blk_cnt
is disk blocks.  If /UNDER is used without a value, 4001 is assumed.
The /OVER and /UNDER qualifiers are used to limit the users reported on
before the /TOP qualifier is considered.
$! Start of MAKE.COM
$ Create MAKE.COM
 $!        Compile the image.  Requires VAX-C, Set Command, Linker
 $ cc pak:[local.src.hog]hog.c
 $ set command/obj hog_table.cld
 $ link pak:[local.src.hog]hog.obj,hog_table.obj,sys$input:/opt
 sys$library:vaxcrtl.exe/share
 $!        Liraryize the help file
 $ library/create/help hog.hlb hog.hlp
 $ exit
$! End of Shar file