root/usr.sbin/bsnmpd/modules/snmp_hostres/hostres_swrun_tbl.c
/*
 * Copyright (c) 2005-2006 The FreeBSD Project
 * All rights reserved.
 *
 * Author: Victor Cruceru <soc-victor@freebsd.org>
 *
 * Redistribution of this software and documentation and use in source and
 * binary forms, with or without modification, are permitted provided that
 * the following conditions are met:
 *
 * 1. Redistributions of source code or documentation must retain the above
 *    copyright notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * Host Resources MIB for SNMPd. Implementation for hrSWRunTable
 */

#include <sys/param.h>
#include <sys/proc.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#include <sys/linker.h>

#include <assert.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>

#include "hostres_snmp.h"
#include "hostres_oid.h"
#include "hostres_tree.h"

/*
 * Ugly thing: PID_MAX, NO_PID defined only in kernel
 */
#define NO_PID          100000

enum SWRunType {
        SRT_UNKNOWN             = 1,
        SRT_OPERATING_SYSTEM    = 2,
        SRT_DEVICE_DRIVER       = 3,
        SRT_APPLICATION         = 4

};

enum SWRunStatus {
        SRS_RUNNING             = 1,
        SRS_RUNNABLE            = 2,
        SRS_NOT_RUNNABLE        = 3,
        SRS_INVALID             = 4
};

/* Maximum lengths for the strings according to the MIB */
#define SWR_NAME_MLEN   (64 + 1)
#define SWR_PATH_MLEN   (128 + 1)
#define SWR_PARAM_MLEN  (128 + 1)

/*
 * This structure is used to hold a SNMP table entry
 * for both hrSWRunTable and hrSWRunPerfTable because
 * hrSWRunPerfTable AUGMENTS hrSWRunTable
 */
struct swrun_entry {
        int32_t         index;
        u_char          *name;          /* it may be NULL */
        const struct asn_oid *id;
        u_char          *path;          /* it may be NULL */
        u_char          *parameters;    /* it may be NULL */
        int32_t         type;           /* enum SWRunType */
        int32_t         status;         /* enum SWRunStatus */
        int32_t         perfCPU;
        int32_t         perfMemory;
#define HR_SWRUN_FOUND 0x001
        uint32_t        flags;
        uint64_t        r_tick;         /* tick when entry refreshed */
        TAILQ_ENTRY(swrun_entry) link;
};
TAILQ_HEAD(swrun_tbl, swrun_entry);

/* the head of the list with hrSWRunTable's entries */
static struct swrun_tbl swrun_tbl = TAILQ_HEAD_INITIALIZER(swrun_tbl);

/* last (agent) tick when hrSWRunTable and hrSWRunPerTable was updated */
static uint64_t swrun_tick;

/* maximum number of ticks between updates of SWRun and SWRunPerf table */
uint32_t swrun_tbl_refresh = HR_SWRUN_TBL_REFRESH * 100;

/* the value of the MIB object with the same name */
static int32_t SWOSIndex;

/**
 * Malloc a new entry and add it to the list
 * associated to this table. The item identified by
 * the index parameter must not exist in this list.
 */
static struct swrun_entry *
swrun_entry_create(int32_t idx)
{
        struct swrun_entry *entry;

        if ((entry = malloc(sizeof(*entry))) == NULL) {
                syslog(LOG_WARNING, "%s: %m", __func__);
                return (NULL);
        }
        memset(entry, 0, sizeof(*entry));
        entry->index = idx;

        INSERT_OBJECT_INT(entry, &swrun_tbl);
        return (entry);
}

/**
 * Unlink the entry from the list and then free its heap memory
 */
static void
swrun_entry_delete(struct swrun_entry *entry)
{

        assert(entry != NULL);

        TAILQ_REMOVE(&swrun_tbl, entry, link);

        free(entry->name);
        free(entry->path);
        free(entry->parameters);
        free(entry);
}

/**
 * Search one item by its index, return NULL if none found
 */
static struct swrun_entry *
swrun_entry_find_by_index(int32_t idx)
{
        struct swrun_entry *entry;

        TAILQ_FOREACH(entry, &swrun_tbl, link)
                if (entry->index == idx)
                        return (entry);
        return (NULL);
}

/**
 * Translate the kernel's process status to SNMP.
 */
static enum SWRunStatus
swrun_OS_get_proc_status(const struct kinfo_proc *kp)
{

        assert(kp != NULL);
        if(kp ==  NULL) {
                return (SRS_INVALID);
        }

        /*
         * I'm using the old style flags - they look cleaner to me,
         * at least for the purpose of this SNMP table
         */
        switch (kp->ki_stat) {

        case SSTOP:
                return (SRS_NOT_RUNNABLE);

        case SWAIT:
        case SLOCK:
        case SSLEEP:
                return (SRS_RUNNABLE);

        case SZOMB:
                return (SRS_INVALID);

        case SIDL:
        case SRUN:
                return (SRS_RUNNING);

        default:
                syslog(LOG_ERR,"Unknown process state: %d", kp->ki_stat);
                return (SRS_INVALID);
        }
}

/**
 * Make an SNMP table entry from a kernel one.
 */
static void
kinfo_proc_to_swrun_entry(const struct kinfo_proc *kp,
    struct swrun_entry *entry)
{
        char **argv = NULL;
        uint64_t cpu_time = 0;
        size_t pname_len;

        pname_len = strlen(kp->ki_comm) + 1;
        entry->name = reallocf(entry->name, pname_len);
        if (entry->name != NULL)
                strlcpy(entry->name, kp->ki_comm, pname_len);

        entry->id = &oid_zeroDotZero; /* unknown id - FIXME */

        assert(hr_kd != NULL);

        argv = kvm_getargv(hr_kd, kp, SWR_PARAM_MLEN - 1);
        if(argv != NULL){
                u_char param[SWR_PARAM_MLEN];

                memset(param, '\0', sizeof(param));

                /*
                 * FIXME
                 * Path seems to not be available.
                 * Try to hack the info in argv[0];
                 * this argv is under control of the program so this info
                 * is not realiable
                 */
                if(*argv != NULL && (*argv)[0] == '/') {
                        size_t path_len;

                        path_len = strlen(*argv) + 1;
                        if (path_len > SWR_PATH_MLEN)
                                path_len = SWR_PATH_MLEN;

                        entry->path = reallocf(entry->path, path_len);
                        if (entry->path != NULL) {
                                memset(entry->path, '\0', path_len);
                                strlcpy((char*)entry->path, *argv, path_len);
                        }
                }

                argv++; /* skip the first one which was used for path */

                while (argv != NULL && *argv != NULL ) {
                        if (param[0] != 0)  {
                                /*
                                 * add a space between parameters,
                                 * except before the first one
                                 */
                                strlcat((char *)param, " ", sizeof(param));
                        }
                        strlcat((char *)param, *argv, sizeof(param));
                        argv++;
                }
                /* reuse pname_len */
                pname_len = strlen(param) + 1;
                if (pname_len > SWR_PARAM_MLEN)
                        pname_len = SWR_PARAM_MLEN;

                entry->parameters = reallocf(entry->parameters, pname_len);
                strlcpy(entry->parameters, param, pname_len);
        }

        entry->type = (int32_t)(IS_KERNPROC(kp) ? SRT_OPERATING_SYSTEM :
            SRT_APPLICATION);

        entry->status = (int32_t)swrun_OS_get_proc_status(kp);
        cpu_time = kp->ki_runtime / 100000; /* centi-seconds */

        /* may overflow the snmp type */
        entry->perfCPU = (cpu_time > (uint64_t)INT_MAX ? INT_MAX : cpu_time);
        entry->perfMemory = kp->ki_size / 1024; /* in kilo-bytes */
        entry->r_tick = get_ticks();
}

/**
 * Create a table entry for a KLD
 */
static void
kld_file_stat_to_swrun(const struct kld_file_stat *kfs,
    struct swrun_entry *entry)
{
        size_t name_len;

        assert(kfs != NULL);
        assert(entry != NULL);

        name_len = strlen(kfs->name) + 1;
        if (name_len > SWR_NAME_MLEN)
                name_len = SWR_NAME_MLEN;

        entry->name = reallocf(entry->name, name_len);
        if (entry->name != NULL)
                strlcpy((char *)entry->name, kfs->name, name_len);

        /* FIXME: can we find the location where the module was loaded from? */
        entry->path = NULL;

        /* no parameters for kernel files (.ko) of for the kernel */
        entry->parameters = NULL;

        entry->id = &oid_zeroDotZero; /* unknown id - FIXME */

        if (strcmp(kfs->name, "kernel") == 0) {
                entry->type = (int32_t)SRT_OPERATING_SYSTEM;
                SWOSIndex = entry->index;
        } else {
                entry->type = (int32_t)SRT_DEVICE_DRIVER; /* well, not really */
        }
        entry->status = (int32_t)SRS_RUNNING;
        entry->perfCPU = 0;                     /* Info not available */
        entry->perfMemory = kfs->size / 1024;   /* in kilo-bytes */
        entry->r_tick = get_ticks();
}

/**
 * Get all visible processes including the kernel visible threads
 */
static void
swrun_OS_get_procs(void)
{
        struct kinfo_proc *plist, *kp;
        int i;
        int nproc;
        struct swrun_entry *entry;

        plist = kvm_getprocs(hr_kd, KERN_PROC_ALL, 0, &nproc);
        if (plist == NULL || nproc < 0) {
                syslog(LOG_ERR, "kvm_getprocs() failed: %m");
                return;
        }
        for (i = 0, kp = plist; i < nproc; i++, kp++) {
                /*
                 * The SNMP table's index must begin from 1 (as specified by
                 * this table definition), the PIDs are starting from 0
                 * so we are translating the PIDs to +1
                 */
                entry = swrun_entry_find_by_index((int32_t)kp->ki_pid + 1);
                if (entry == NULL) {
                        /* new entry - get memory for it */
                        entry = swrun_entry_create((int32_t)kp->ki_pid + 1);
                        if (entry == NULL)
                                continue;
                }
                entry->flags |= HR_SWRUN_FOUND; /* mark it as found */

                kinfo_proc_to_swrun_entry(kp, entry);
        }
}

/*
 * Get kernel items: first the kernel itself, then the loaded modules.
 */
static void
swrun_OS_get_kinfo(void)
{
        int fileid;
        struct swrun_entry *entry;
        struct kld_file_stat stat;

        for (fileid = kldnext(0); fileid > 0; fileid = kldnext(fileid)) {
                stat.version = sizeof(struct kld_file_stat);
                if (kldstat(fileid, &stat) < 0) {
                        syslog(LOG_ERR, "kldstat() failed: %m");
                        continue;
                }

                /*
                 * kernel and kernel files (*.ko) will be indexed starting with
                 * NO_PID + 1; NO_PID is PID_MAX + 1 thus it will be no risk to
                 * overlap with real PIDs which are in range of 1 .. NO_PID
                 */
                entry = swrun_entry_find_by_index(NO_PID + 1 + stat.id);
                if (entry == NULL) {
                        /* new entry - get memory for it */
                        entry = swrun_entry_create(NO_PID + 1 + stat.id);
                        if (entry == NULL)
                                continue;
                }
                entry->flags |= HR_SWRUN_FOUND; /* mark it as found */

                kld_file_stat_to_swrun(&stat, entry);
        }
}

/**
 * Refresh the hrSWRun and hrSWRunPert tables.
 */
static void
refresh_swrun_tbl(void)
{

        struct swrun_entry *entry, *entry_tmp;

        if (this_tick - swrun_tick < swrun_tbl_refresh) {
                HRDBG("no refresh needed ");
                return;
        }

        /* mark each entry as missing */
        TAILQ_FOREACH(entry, &swrun_tbl, link)
                entry->flags &= ~HR_SWRUN_FOUND;

        swrun_OS_get_procs();
        swrun_OS_get_kinfo();

        /*
         * Purge items that disappeared
         */
        TAILQ_FOREACH_SAFE(entry, &swrun_tbl, link, entry_tmp)
                if (!(entry->flags & HR_SWRUN_FOUND))
                        swrun_entry_delete(entry);

        swrun_tick = this_tick;

        HRDBG("refresh DONE");
}

/**
 * Update the information in this entry
 */
static void
fetch_swrun_entry(struct swrun_entry *entry)
{
        struct kinfo_proc *plist;
        int nproc;
        struct kld_file_stat stat;

        assert(entry !=  NULL);

        if (entry->index >= NO_PID + 1) {
                /*
                 * kernel and kernel files (*.ko) will be indexed
                 * starting with NO_PID + 1; NO_PID is PID_MAX + 1
                 * thus it will be no risk to overlap with real PIDs
                 * which are in range of 1 .. NO_PID
                 */
                stat.version = sizeof(stat);
                if (kldstat(entry->index - NO_PID - 1, &stat) == -1) {
                        /*
                         * not found, it's gone. Mark it as invalid for now, it
                         * will be removed from the list at next global refersh
                         */
                         HRDBG("missing item with kid=%d",
                             entry->index -  NO_PID - 1);
                        entry->status = (int32_t)SRS_INVALID;
                } else
                        kld_file_stat_to_swrun(&stat, entry);

        } else {
                /* this is a process */
                assert(hr_kd != NULL);
                plist = kvm_getprocs(hr_kd, KERN_PROC_PID,
                    entry->index - 1, &nproc);
                if (plist == NULL || nproc != 1) {
                        HRDBG("missing item with PID=%d", entry->index - 1);
                        entry->status = (int32_t)SRS_INVALID;
                } else
                        kinfo_proc_to_swrun_entry(plist, entry);
        }
}

/**
 * Invalidate entry. For KLDs we try to unload it, for processes we SIGKILL it.
 */
static int
invalidate_swrun_entry(struct swrun_entry *entry, int commit)
{
        struct kinfo_proc *plist;
        int nproc;
        struct kld_file_stat stat;

        assert(entry !=  NULL);

        if (entry->index >= NO_PID + 1) {
                /* this is a kernel item */
                HRDBG("attempt to unload KLD %d",
                    entry->index -  NO_PID - 1);

                if (entry->index == SWOSIndex) {
                        /* can't invalidate the kernel itself */
                        return (SNMP_ERR_NOT_WRITEABLE);
                }

                stat.version = sizeof(stat);
                if (kldstat(entry->index - NO_PID - 1, &stat) == -1) {
                        /*
                         * not found, it's gone. Mark it as invalid for now, it
                         * will be removed from the list at next global
                         * refresh
                         */
                        HRDBG("missing item with kid=%d",
                            entry->index - NO_PID - 1);
                        entry->status = (int32_t)SRS_INVALID;
                        return (SNMP_ERR_NOERROR);
                }
                /*
                 * There is no way to try to unload a module. There seems
                 * also no way to find out whether it is busy without unloading
                 * it. We can assume that it is busy, if the reference count
                 * is larger than 2, but if it is 1 nothing helps.
                 */
                if (!commit) {
                        if (stat.refs > 1)
                                return (SNMP_ERR_NOT_WRITEABLE);
                        return (SNMP_ERR_NOERROR);
                }
                if (kldunload(stat.id) == -1) {
                        syslog(LOG_ERR,"kldunload for %d/%s failed: %m",
                            stat.id, stat.name);
                        if (errno == EBUSY)
                                return (SNMP_ERR_NOT_WRITEABLE);
                        else
                                return (SNMP_ERR_RES_UNAVAIL);
                }
        } else {
                /* this is a process */
                assert(hr_kd != NULL);

                plist = kvm_getprocs(hr_kd, KERN_PROC_PID,
                    entry->index - 1, &nproc);
                if (plist == NULL || nproc != 1) {
                        HRDBG("missing item with PID=%d", entry->index - 1);
                        entry->status = (int32_t)SRS_INVALID;
                        return (SNMP_ERR_NOERROR);
                }
                if (IS_KERNPROC(plist)) {
                        /* you don't want to do this */
                        return (SNMP_ERR_NOT_WRITEABLE);
                }
                if (kill(entry->index - 1, commit ? SIGKILL : 0) < 0) {
                        syslog(LOG_ERR,"kill (%d, SIGKILL) failed: %m",
                            entry->index - 1);
                        if (errno == ESRCH) {
                                /* race: just gone */
                                entry->status = (int32_t)SRS_INVALID;
                                return (SNMP_ERR_NOERROR);
                        }
                        return (SNMP_ERR_GENERR);
                }
        }
        return (SNMP_ERR_NOERROR);
}

/**
 * Populate the hrSWRunTable.
 */
void
init_swrun_tbl(void)
{

        refresh_swrun_tbl();
        HRDBG("done");
}

/**
 * Finalize the hrSWRunTable.
 */
void
fini_swrun_tbl(void)
{
        struct swrun_entry *n1;

        while ((n1 = TAILQ_FIRST(&swrun_tbl)) != NULL) {
                TAILQ_REMOVE(&swrun_tbl, n1, link);
                free(n1);
        }
}

/*
 * This is the implementation for a generated (by a SNMP tool)
 * function prototype, see hostres_tree.h
 * It handles the SNMP operations for hrSWRunTable
 */
int
op_hrSWRunTable(struct snmp_context *ctx __unused, struct snmp_value *value,
    u_int sub, u_int iidx __unused, enum snmp_op curr_op)
{
        struct swrun_entry *entry;
        int ret;

        refresh_swrun_tbl();

        switch (curr_op) {

          case SNMP_OP_GETNEXT:
                if ((entry = NEXT_OBJECT_INT(&swrun_tbl,
                    &value->var, sub)) == NULL)
                        return (SNMP_ERR_NOSUCHNAME);
                value->var.len = sub + 1;
                value->var.subs[sub] = entry->index;
                goto get;

          case SNMP_OP_GET:
                if ((entry = FIND_OBJECT_INT(&swrun_tbl,
                    &value->var, sub)) == NULL)
                        return (SNMP_ERR_NOSUCHNAME);
                goto get;

          case SNMP_OP_SET:
                if ((entry = FIND_OBJECT_INT(&swrun_tbl,
                    &value->var, sub)) == NULL)
                        return (SNMP_ERR_NO_CREATION);

                if (entry->r_tick < this_tick)
                        fetch_swrun_entry(entry);

                switch (value->var.subs[sub - 1]) {

                case LEAF_hrSWRunStatus:
                        if (value->v.integer != (int32_t)SRS_INVALID)
                                return (SNMP_ERR_WRONG_VALUE);

                        if (entry->status == (int32_t)SRS_INVALID)
                                return (SNMP_ERR_NOERROR);

                        /*
                         * Here we have a problem with the entire SNMP
                         * model: if we kill now, we cannot rollback.
                         * If we kill in the commit code, we cannot
                         * return an error. Because things may change between
                         * SET and COMMIT this is impossible to handle
                         * correctly.
                         */
                        return (invalidate_swrun_entry(entry, 0));
                }
                return (SNMP_ERR_NOT_WRITEABLE);

          case SNMP_OP_ROLLBACK:
                return (SNMP_ERR_NOERROR);

          case SNMP_OP_COMMIT:
                if ((entry = FIND_OBJECT_INT(&swrun_tbl,
                    &value->var, sub)) == NULL)
                        return (SNMP_ERR_NOERROR);

                switch (value->var.subs[sub - 1]) {

                case LEAF_hrSWRunStatus:
                        if (value->v.integer == (int32_t)SRS_INVALID &&
                            entry->status != (int32_t)SRS_INVALID)
                                (void)invalidate_swrun_entry(entry, 1);
                        return (SNMP_ERR_NOERROR);
                }
                abort();
        }
        abort();

  get:
        ret = SNMP_ERR_NOERROR;
        switch (value->var.subs[sub - 1]) {

          case LEAF_hrSWRunIndex:
                value->v.integer = entry->index;
                break;

          case LEAF_hrSWRunName:
                if (entry->name != NULL)
                        ret = string_get(value, entry->name, -1);
                else
                        ret = string_get(value, "", -1);
                break;

          case LEAF_hrSWRunID:
                assert(entry->id != NULL);
                value->v.oid = *entry->id;
                break;

          case LEAF_hrSWRunPath:
                if (entry->path != NULL)
                        ret = string_get(value, entry->path, -1);
                else
                        ret = string_get(value, "", -1);
                break;

          case LEAF_hrSWRunParameters:
                if (entry->parameters != NULL)
                        ret = string_get(value, entry->parameters, -1);
                else
                        ret = string_get(value, "", -1);
                break;

          case LEAF_hrSWRunType:
                value->v.integer = entry->type;
                break;

          case LEAF_hrSWRunStatus:
                value->v.integer = entry->status;
                break;

          default:
                abort();
        }
        return (ret);
}

/**
 * Scalar(s) in the SWRun group
 */
int
op_hrSWRun(struct snmp_context *ctx __unused, struct snmp_value *value,
    u_int sub, u_int iidx __unused, enum snmp_op curr_op)
{

        /* only SNMP GET is possible */
        switch (curr_op) {

        case SNMP_OP_GET:
                goto get;

        case SNMP_OP_SET:
                return (SNMP_ERR_NOT_WRITEABLE);

        case SNMP_OP_ROLLBACK:
        case SNMP_OP_COMMIT:
        case SNMP_OP_GETNEXT:
                abort();
        }
        abort();

  get:
        switch (value->var.subs[sub - 1]) {

        case LEAF_hrSWOSIndex:
                value->v.uint32 = SWOSIndex;
                return (SNMP_ERR_NOERROR);

        default:
                abort();
        }
}

/*
 * This is the implementation for a generated (by a SNMP tool)
 * function prototype, see hostres_tree.h
 * It handles the SNMP operations for hrSWRunPerfTable
 */
int
op_hrSWRunPerfTable(struct snmp_context *ctx __unused,
    struct snmp_value *value, u_int sub, u_int iidx __unused,
    enum snmp_op curr_op )
{
        struct swrun_entry *entry;

        refresh_swrun_tbl();

        switch (curr_op) {

          case SNMP_OP_GETNEXT:
                if ((entry = NEXT_OBJECT_INT(&swrun_tbl,
                    &value->var, sub)) == NULL)
                        return (SNMP_ERR_NOSUCHNAME);
                value->var.len = sub + 1;
                value->var.subs[sub] = entry->index;
                goto get;

          case SNMP_OP_GET:
                if ((entry = FIND_OBJECT_INT(&swrun_tbl,
                    &value->var, sub)) == NULL)
                        return (SNMP_ERR_NOSUCHNAME);
                goto get;

          case SNMP_OP_SET:
                if ((entry = FIND_OBJECT_INT(&swrun_tbl,
                    &value->var, sub)) == NULL)
                        return (SNMP_ERR_NO_CREATION);
                return (SNMP_ERR_NOT_WRITEABLE);

          case SNMP_OP_ROLLBACK:
          case SNMP_OP_COMMIT:
                abort();
        }
        abort();

  get:
        switch (value->var.subs[sub - 1]) {

          case LEAF_hrSWRunPerfCPU:
                value->v.integer = entry->perfCPU;
                return (SNMP_ERR_NOERROR);

          case LEAF_hrSWRunPerfMem:
                value->v.integer = entry->perfMemory;
                return (SNMP_ERR_NOERROR);
        }
        abort();
}