/*
 * kernel/lvm-fs.c
 *
 * Copyright (C) 2001 Sistina Software
 *
 * January-April 2001
 *
 * LVM driver is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * LVM driver is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GNU CC; see the file COPYING.  If not, write to
 * the Free Software Foundation, 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 */

/*
 * Changelog
 *
 *    11/01/2001 - First version (Joe Thornber)
 *    21/03/2001 - added display of stripes and stripe size (HM)
 *    04/10/2001 - corrected devfs_register() call in lvm_init_fs()
 *    11/04/2001 - don't devfs_register("lvm") as user-space always does it
 *    10/05/2001 - show more of PV name in /proc/lvm/global
 *    16/12/2001 - fix devfs unregister order and prevent duplicate unreg (REG)
 *
 */

#include <linux/config.h>
#include <linux/version.h>

#include <linux/kernel.h>
#include <linux/vmalloc.h>
#include <linux/smp_lock.h>

#include <linux/devfs_fs_kernel.h>
#include <linux/proc_fs.h>
#include <linux/init.h>
#include <linux/lvm.h>

#include "lvm-internal.h"


static int _proc_read_vg(char *page, char **start, off_t off,
			 int count, int *eof, void *data);
static int _proc_read_lv(char *page, char **start, off_t off,
			 int count, int *eof, void *data);
static int _proc_read_pv(char *page, char **start, off_t off,
			 int count, int *eof, void *data);
static int _proc_read_global(char *page, char **start, off_t off,
			     int count, int *eof, void *data);

static int _vg_info(vg_t *vg_ptr, char *buf);
static int _lv_info(vg_t *vg_ptr, lv_t *lv_ptr, char *buf);
static int _pv_info(pv_t *pv_ptr, char *buf);

static void _show_uuid(const char *src, char *b, char *e);

#if 0
static devfs_handle_t lvm_devfs_handle;
#endif
static devfs_handle_t vg_devfs_handle[MAX_VG];
static devfs_handle_t ch_devfs_handle[MAX_VG];
static devfs_handle_t lv_devfs_handle[MAX_LV];

static struct proc_dir_entry *lvm_proc_dir = NULL;
static struct proc_dir_entry *lvm_proc_vg_subdir = NULL;

/* inline functions */

/* public interface */
void __init lvm_init_fs() {
	struct proc_dir_entry *pde;

/* User-space has already registered this */
#if 0
	lvm_devfs_handle = devfs_register(
		0 , "lvm", 0, LVM_CHAR_MAJOR, 0,
		S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP,
		&lvm_chr_fops, NULL);
#endif
	lvm_proc_dir = create_proc_entry(LVM_DIR, S_IFDIR, &proc_root);
	if (lvm_proc_dir) {
		lvm_proc_vg_subdir = create_proc_entry(LVM_VG_SUBDIR, S_IFDIR,
						       lvm_proc_dir);
		pde = create_proc_entry(LVM_GLOBAL, S_IFREG, lvm_proc_dir);
		if ( pde != NULL) pde->read_proc = _proc_read_global;
	}
}

void lvm_fin_fs() {
#if 0
	devfs_unregister (lvm_devfs_handle);
#endif
	remove_proc_entry(LVM_GLOBAL, lvm_proc_dir);
	remove_proc_entry(LVM_VG_SUBDIR, lvm_proc_dir);
	remove_proc_entry(LVM_DIR, &proc_root);
}

void lvm_fs_create_vg(vg_t *vg_ptr) {
	struct proc_dir_entry *pde;

	vg_devfs_handle[vg_ptr->vg_number] =
		devfs_mk_dir(0, vg_ptr->vg_name, NULL);

	ch_devfs_handle[vg_ptr->vg_number] = devfs_register(
		vg_devfs_handle[vg_ptr->vg_number] , "group",
		DEVFS_FL_DEFAULT, LVM_CHAR_MAJOR, vg_ptr->vg_number,
		S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP,
		&lvm_chr_fops, NULL);

	vg_ptr->vg_dir_pde = create_proc_entry(vg_ptr->vg_name, S_IFDIR,
					       lvm_proc_vg_subdir);

	if((pde = create_proc_entry("group", S_IFREG, vg_ptr->vg_dir_pde))) {
		pde->read_proc = _proc_read_vg;
		pde->data = vg_ptr;
	}

	vg_ptr->lv_subdir_pde =
		create_proc_entry(LVM_LV_SUBDIR, S_IFDIR, vg_ptr->vg_dir_pde);

	vg_ptr->pv_subdir_pde =
		create_proc_entry(LVM_PV_SUBDIR, S_IFDIR, vg_ptr->vg_dir_pde);
}

void lvm_fs_remove_vg(vg_t *vg_ptr) {
	int i;

	devfs_unregister(ch_devfs_handle[vg_ptr->vg_number]);
	ch_devfs_handle[vg_ptr->vg_number] = NULL;

	/* remove pv's */
	for(i = 0; i < vg_ptr->pv_max; i++)
		if(vg_ptr->pv[i]) lvm_fs_remove_pv(vg_ptr, vg_ptr->pv[i]);

	/* remove lv's */
	for(i = 0; i < vg_ptr->lv_max; i++)
		if(vg_ptr->lv[i]) lvm_fs_remove_lv(vg_ptr, vg_ptr->lv[i]);


	/* must not remove directory before leaf nodes */
	devfs_unregister(vg_devfs_handle[vg_ptr->vg_number]);
	vg_devfs_handle[vg_ptr->vg_number] = NULL;

	if(vg_ptr->vg_dir_pde) {
		remove_proc_entry(LVM_LV_SUBDIR, vg_ptr->vg_dir_pde);
		vg_ptr->lv_subdir_pde = NULL;

		remove_proc_entry(LVM_PV_SUBDIR, vg_ptr->vg_dir_pde);
		vg_ptr->pv_subdir_pde = NULL;

		remove_proc_entry("group", vg_ptr->vg_dir_pde);
		vg_ptr->vg_dir_pde = NULL;

		remove_proc_entry(vg_ptr->vg_name, lvm_proc_vg_subdir);
	}
}


static inline const char *_basename(const char *str) {
	const char *name = strrchr(str, '/');
	name = name ? name + 1 : str;
	return name;
}

devfs_handle_t lvm_fs_create_lv(vg_t *vg_ptr, lv_t *lv) {
	struct proc_dir_entry *pde;
	const char *name = _basename(lv->lv_name);

	lv_devfs_handle[MINOR(lv->lv_dev)] = devfs_register(
		vg_devfs_handle[vg_ptr->vg_number], name,
		DEVFS_FL_DEFAULT, LVM_BLK_MAJOR, MINOR(lv->lv_dev),
		S_IFBLK | S_IRUSR | S_IWUSR | S_IRGRP,
		&lvm_blk_dops, NULL);

	if(vg_ptr->lv_subdir_pde &&
	   (pde = create_proc_entry(name, S_IFREG, vg_ptr->lv_subdir_pde))) {
		pde->read_proc = _proc_read_lv;
		pde->data = lv;
	}
	return lv_devfs_handle[MINOR(lv->lv_dev)];
}

void lvm_fs_remove_lv(vg_t *vg_ptr, lv_t *lv) {
	devfs_unregister(lv_devfs_handle[MINOR(lv->lv_dev)]);
	lv_devfs_handle[MINOR(lv->lv_dev)] = NULL;

	if(vg_ptr->lv_subdir_pde) {
		const char *name = _basename(lv->lv_name);
		remove_proc_entry(name, vg_ptr->lv_subdir_pde);
	}
}


static inline void _make_pv_name(const char *src, char *b, char *e) {
	int offset = strlen(LVM_DIR_PREFIX);
	if(strncmp(src, LVM_DIR_PREFIX, offset))
		offset = 0;

	e--;
	src += offset;
	while(*src && (b != e)) {
		*b++ = (*src == '/') ? '_' : *src;
		src++;
	}
	*b = '\0';
}

void lvm_fs_create_pv(vg_t *vg_ptr, pv_t *pv) {
	struct proc_dir_entry *pde;
	char name[NAME_LEN];

	if(!vg_ptr->pv_subdir_pde)
		return;

	_make_pv_name(pv->pv_name, name, name + sizeof(name));
	if((pde = create_proc_entry(name, S_IFREG, vg_ptr->pv_subdir_pde))) {
		pde->read_proc = _proc_read_pv;
		pde->data = pv;
	}
}

void lvm_fs_remove_pv(vg_t *vg_ptr, pv_t *pv) {
	char name[NAME_LEN];

	if(!vg_ptr->pv_subdir_pde)
		return;

	_make_pv_name(pv->pv_name, name, name + sizeof(name));
	remove_proc_entry(name, vg_ptr->pv_subdir_pde);
}


static int _proc_read_vg(char *page, char **start, off_t off,
			  int count, int *eof, void *data) {
	int sz = 0;
	vg_t *vg_ptr = data;
	char uuid[NAME_LEN];

	sz += sprintf(page + sz, "name:         %s\n", vg_ptr->vg_name);
	sz += sprintf(page + sz, "size:         %u\n",
		      vg_ptr->pe_total * vg_ptr->pe_size / 2);
	sz += sprintf(page + sz, "access:       %u\n", vg_ptr->vg_access);
	sz += sprintf(page + sz, "status:       %u\n", vg_ptr->vg_status);
	sz += sprintf(page + sz, "number:       %u\n", vg_ptr->vg_number);
	sz += sprintf(page + sz, "LV max:       %u\n", vg_ptr->lv_max);
	sz += sprintf(page + sz, "LV current:   %u\n", vg_ptr->lv_cur);
	sz += sprintf(page + sz, "LV open:      %u\n", vg_ptr->lv_open);
	sz += sprintf(page + sz, "PV max:       %u\n", vg_ptr->pv_max);
	sz += sprintf(page + sz, "PV current:   %u\n", vg_ptr->pv_cur);
	sz += sprintf(page + sz, "PV active:    %u\n", vg_ptr->pv_act);
	sz += sprintf(page + sz, "PE size:      %u\n", vg_ptr->pe_size / 2);
	sz += sprintf(page + sz, "PE total:     %u\n", vg_ptr->pe_total);
	sz += sprintf(page + sz, "PE allocated: %u\n", vg_ptr->pe_allocated);

	_show_uuid(vg_ptr->vg_uuid, uuid, uuid + sizeof(uuid));
	sz += sprintf(page + sz, "uuid:         %s\n", uuid);

	return sz;
}

static int _proc_read_lv(char *page, char **start, off_t off,
			  int count, int *eof, void *data) {
	int sz = 0;
	lv_t *lv = data;

	sz += sprintf(page + sz, "name:         %s\n", lv->lv_name);
	sz += sprintf(page + sz, "size:         %u\n", lv->lv_size);
	sz += sprintf(page + sz, "access:       %u\n", lv->lv_access);
	sz += sprintf(page + sz, "status:       %u\n", lv->lv_status);
	sz += sprintf(page + sz, "number:       %u\n", lv->lv_number);
	sz += sprintf(page + sz, "open:         %u\n", lv->lv_open);
	sz += sprintf(page + sz, "allocation:   %u\n", lv->lv_allocation);
	if(lv->lv_stripes > 1) {
		sz += sprintf(page + sz, "stripes:      %u\n",
			      lv->lv_stripes);
		sz += sprintf(page + sz, "stripesize:   %u\n",
			      lv->lv_stripesize);
	}
	sz += sprintf(page + sz, "device:       %02u:%02u\n",
		      MAJOR(lv->lv_dev), MINOR(lv->lv_dev));

	return sz;
}

static int _proc_read_pv(char *page, char **start, off_t off,
			 int count, int *eof, void *data) {
	int sz = 0;
	pv_t *pv = data;
	char uuid[NAME_LEN];

	sz += sprintf(page + sz, "name:         %s\n", pv->pv_name);
	sz += sprintf(page + sz, "size:         %u\n", pv->pv_size);
	sz += sprintf(page + sz, "status:       %u\n", pv->pv_status);
	sz += sprintf(page + sz, "number:       %u\n", pv->pv_number);
	sz += sprintf(page + sz, "allocatable:  %u\n", pv->pv_allocatable);
	sz += sprintf(page + sz, "LV current:   %u\n", pv->lv_cur);
	sz += sprintf(page + sz, "PE size:      %u\n", pv->pe_size / 2);
	sz += sprintf(page + sz, "PE total:     %u\n", pv->pe_total);
	sz += sprintf(page + sz, "PE allocated: %u\n", pv->pe_allocated);
	sz += sprintf(page + sz, "device:       %02u:%02u\n",
                      MAJOR(pv->pv_dev), MINOR(pv->pv_dev));

	_show_uuid(pv->pv_uuid, uuid, uuid + sizeof(uuid));
	sz += sprintf(page + sz, "uuid:         %s\n", uuid);

	return sz;
}

static int _proc_read_global(char *page, char **start, off_t pos, int count,
			     int *eof, void *data) {

#define  LVM_PROC_BUF   ( i == 0 ? dummy_buf : &buf[sz])

	int c, i, l, p, v, vg_counter, pv_counter, lv_counter, lv_open_counter,
		lv_open_total, pe_t_bytes, hash_table_bytes, lv_block_exception_t_bytes, seconds;
	static off_t sz;
	off_t sz_last;
	static char *buf = NULL;
	static char dummy_buf[160];	/* sized for 2 lines */
	vg_t *vg_ptr;
	lv_t *lv_ptr;
	pv_t *pv_ptr;


#ifdef DEBUG_LVM_PROC_GET_INFO
	printk(KERN_DEBUG
	       "%s - lvm_proc_get_global_info CALLED  pos: %lu  count: %d\n",
	       lvm_name, pos, count);
#endif

	if(pos != 0 && buf != NULL)
		goto out;

	sz_last = vg_counter = pv_counter = lv_counter = lv_open_counter = \
		lv_open_total = pe_t_bytes = hash_table_bytes = \
		lv_block_exception_t_bytes = 0;

	/* get some statistics */
	for (v = 0; v < ABS_MAX_VG; v++) {
		if ((vg_ptr = vg[v]) != NULL) {
			vg_counter++;
			pv_counter += vg_ptr->pv_cur;
			lv_counter += vg_ptr->lv_cur;
			if (vg_ptr->lv_cur > 0) {
				for (l = 0; l < vg[v]->lv_max; l++) {
					if ((lv_ptr = vg_ptr->lv[l]) != NULL) {
						pe_t_bytes += lv_ptr->lv_allocated_le;
						hash_table_bytes += lv_ptr->lv_snapshot_hash_table_size;
						if (lv_ptr->lv_block_exception != NULL)
							lv_block_exception_t_bytes += lv_ptr->lv_remap_end;
						if (lv_ptr->lv_open > 0) {
							lv_open_counter++;
							lv_open_total += lv_ptr->lv_open;
						}
					}
				}
			}
		}
	}

	pe_t_bytes *= sizeof(pe_t);
	lv_block_exception_t_bytes *= sizeof(lv_block_exception_t);

	if (buf != NULL) {
		P_KFREE("%s -- vfree %d\n", lvm_name, __LINE__);
		lock_kernel();
		vfree(buf);
		unlock_kernel();
		buf = NULL;
	}
	/* 2 times: first to get size to allocate buffer,
	   2nd to fill the malloced buffer */
	for (i = 0; i < 2; i++) {
		sz = 0;
		sz += sprintf(LVM_PROC_BUF,
			      "LVM "
#ifdef MODULE
			      "module"
#else
			      "driver"
#endif
			      " %s\n\n"
			      "Total:  %d VG%s  %d PV%s  %d LV%s ",
			      lvm_version,
			      vg_counter, vg_counter == 1 ? "" : "s",
			      pv_counter, pv_counter == 1 ? "" : "s",
			      lv_counter, lv_counter == 1 ? "" : "s");
		sz += sprintf(LVM_PROC_BUF,
			      "(%d LV%s open",
			      lv_open_counter,
			      lv_open_counter == 1 ? "" : "s");
		if (lv_open_total > 0)
			sz += sprintf(LVM_PROC_BUF,
				      " %d times)\n",
				      lv_open_total);
		else
			sz += sprintf(LVM_PROC_BUF, ")");
		sz += sprintf(LVM_PROC_BUF,
			      "\nGlobal: %lu bytes malloced   IOP version: %d   ",
			      vg_counter * sizeof(vg_t) +
			      pv_counter * sizeof(pv_t) +
			      lv_counter * sizeof(lv_t) +
			      pe_t_bytes + hash_table_bytes + lv_block_exception_t_bytes + sz_last,
			      lvm_iop_version);

		seconds = CURRENT_TIME - loadtime;
		if (seconds < 0)
			loadtime = CURRENT_TIME + seconds;
		if (seconds / 86400 > 0) {
			sz += sprintf(LVM_PROC_BUF, "%d day%s ",
				      seconds / 86400,
				      seconds / 86400 == 0 ||
				      seconds / 86400 > 1 ? "s" : "");
		}
		sz += sprintf(LVM_PROC_BUF, "%d:%02d:%02d active\n",
			      (seconds % 86400) / 3600,
			      (seconds % 3600) / 60,
			      seconds % 60);

		if (vg_counter > 0) {
			for (v = 0; v < ABS_MAX_VG; v++) {
				/* volume group */
				if ((vg_ptr = vg[v]) != NULL) {
					sz += _vg_info(vg_ptr, LVM_PROC_BUF);

					/* physical volumes */
					sz += sprintf(LVM_PROC_BUF,
						      "\n  PV%s ",
						      vg_ptr->pv_cur == 1 ? ": " : "s:");
					c = 0;
					for (p = 0; p < vg_ptr->pv_max; p++) {
						if ((pv_ptr = vg_ptr->pv[p]) != NULL) {
							sz += _pv_info(pv_ptr, LVM_PROC_BUF);

							c++;
							if (c < vg_ptr->pv_cur)
								sz += sprintf(LVM_PROC_BUF,
									      "\n       ");
						}
					}

					/* logical volumes */
					sz += sprintf(LVM_PROC_BUF,
						      "\n    LV%s ",
						      vg_ptr->lv_cur == 1 ? ": " : "s:");
					c = 0;
					for (l = 0; l < vg_ptr->lv_max; l++) {
						if ((lv_ptr = vg_ptr->lv[l]) != NULL) {
							sz += _lv_info(vg_ptr, lv_ptr, LVM_PROC_BUF);
							c++;
							if (c < vg_ptr->lv_cur)
								sz += sprintf(LVM_PROC_BUF,
									      "\n         ");
						}
					}
					if (vg_ptr->lv_cur == 0) sz += sprintf(LVM_PROC_BUF, "none");
					sz += sprintf(LVM_PROC_BUF, "\n");
				}
			}
		}
		if (buf == NULL) {
			lock_kernel();
			buf = vmalloc(sz);
			unlock_kernel();
			if (buf == NULL) {
				sz = 0;
				return sprintf(page, "%s - vmalloc error at line %d\n",
					       lvm_name, __LINE__);
			}
		}
		sz_last = sz;
	}

 out:
	if (pos > sz - 1) {
		lock_kernel();
		vfree(buf);
		unlock_kernel();
		buf = NULL;
		return 0;
	}
	*start = &buf[pos];
	if (sz - pos < count)
		return sz - pos;
	else
		return count;

#undef LVM_PROC_BUF
}

/*
 * provide VG info for proc filesystem use (global)
 */
static int _vg_info(vg_t *vg_ptr, char *buf) {
	int sz = 0;
	char inactive_flag = ' ';

	if (!(vg_ptr->vg_status & VG_ACTIVE)) inactive_flag = 'I';
	sz = sprintf(buf,
		     "\nVG: %c%s  [%d PV, %d LV/%d open] "
		     " PE Size: %d KB\n"
		     "  Usage [KB/PE]: %d /%d total  "
		     "%d /%d used  %d /%d free",
		     inactive_flag,
		     vg_ptr->vg_name,
		     vg_ptr->pv_cur,
		     vg_ptr->lv_cur,
		     vg_ptr->lv_open,
	     	     vg_ptr->pe_size >> 1,
		     vg_ptr->pe_size * vg_ptr->pe_total >> 1,
		     vg_ptr->pe_total,
		     vg_ptr->pe_allocated * vg_ptr->pe_size >> 1,
	     	     vg_ptr->pe_allocated,
		     (vg_ptr->pe_total - vg_ptr->pe_allocated) *
	     	     vg_ptr->pe_size >> 1,
		     vg_ptr->pe_total - vg_ptr->pe_allocated);
	return sz;
}


/*
 * provide LV info for proc filesystem use (global)
 */
static int _lv_info(vg_t *vg_ptr, lv_t *lv_ptr, char *buf) {
	int sz = 0;
	char inactive_flag = 'A', allocation_flag = ' ',
		stripes_flag = ' ', rw_flag = ' ', *basename;

	if (!(lv_ptr->lv_status & LV_ACTIVE))
		inactive_flag = 'I';
	rw_flag = 'R';
	if (lv_ptr->lv_access & LV_WRITE)
		rw_flag = 'W';
	allocation_flag = 'D';
	if (lv_ptr->lv_allocation & LV_CONTIGUOUS)
		allocation_flag = 'C';
	stripes_flag = 'L';
	if (lv_ptr->lv_stripes > 1)
		stripes_flag = 'S';
	sz += sprintf(buf+sz,
		      "[%c%c%c%c",
		      inactive_flag,
	 rw_flag,
		      allocation_flag,
		      stripes_flag);
	if (lv_ptr->lv_stripes > 1)
		sz += sprintf(buf+sz, "%-2d",
			      lv_ptr->lv_stripes);
	else
		sz += sprintf(buf+sz, "  ");

	/* FIXME: use _basename */
	basename = strrchr(lv_ptr->lv_name, '/');
	if ( basename == 0) basename = lv_ptr->lv_name;
	else                basename++;
	sz += sprintf(buf+sz, "] %-25s", basename);
	if (strlen(basename) > 25)
		sz += sprintf(buf+sz,
			      "\n                              ");
	sz += sprintf(buf+sz, "%9d /%-6d   ",
		      lv_ptr->lv_size >> 1,
		      lv_ptr->lv_size / vg_ptr->pe_size);

	if (lv_ptr->lv_open == 0)
		sz += sprintf(buf+sz, "close");
	else
		sz += sprintf(buf+sz, "%dx open",
			      lv_ptr->lv_open);

	return sz;
}


/*
 * provide PV info for proc filesystem use (global)
 */
static int _pv_info(pv_t *pv, char *buf) {
	int sz = 0;
	char inactive_flag = 'A', allocation_flag = ' ';
	char *pv_name = NULL;

	if (!(pv->pv_status & PV_ACTIVE))
		inactive_flag = 'I';
	allocation_flag = 'A';
	if (!(pv->pv_allocatable & PV_ALLOCATABLE))
		allocation_flag = 'N';
	pv_name = strchr(pv->pv_name+1,'/');
	if ( pv_name == 0) pv_name = pv->pv_name;
	else               pv_name++;
	sz = sprintf(buf,
		     "[%c%c] %-21s %8d /%-6d  "
		     "%8d /%-6d  %8d /%-6d",
		     inactive_flag,
		     allocation_flag,
		     pv_name,
		     pv->pe_total * pv->pe_size >> 1,
		     pv->pe_total,
		     pv->pe_allocated * pv->pe_size >> 1,
		     pv->pe_allocated,
		     (pv->pe_total - pv->pe_allocated) *
		     pv->pe_size >> 1,
		     pv->pe_total - pv->pe_allocated);
	return sz;
}

static void _show_uuid(const char *src, char *b, char *e) {
	int i;

	e--;
	for(i = 0; *src && (b != e); i++) {
		if(i && !(i & 0x3))
			*b++ = '-';
		*b++ = *src++;
	}
	*b = '\0';
}
