/*
 * raidreconf.c: RAID Reconfiguration Utility, for resizing and reconfiguration
 *               of RAID sets.
 *               (C) 1999,2000 by Jakob Oestergaard
 *
 * This code is based on the mkraid.c program by Ingo Molnar and others.
 *
 * This source is covered by the GNU GPL, the same as all Linux kernel
 * sources.
 */

#include "raidreconf.h"

/*
 * The strategy is:
 *
 * 1)  Read the raidtabs
 * 2)  Read the superblocks, and perform some consistency checks...
 * 3)  Find the difference(s) between the two, and decide what to do
 * 4)  If possible, convert array
 *
 * TOFIX:
 *          Add linear and raid-5
 *
 *          Test this on large arrays (100+GB)
 *
 *          Checkpoint file (to allow continuation without data loss
 *          after eg. power-surge)
 *
 */

void printcfg (md_cfg_entry_t * cfg);

level_driver_t *source_driver = 0, *sink_driver = 0;

static int raid_reconfigure (mdu_version_t *);

void
usage (void)
{
	printf
	    ("usage: raidreconf [--version] [--test] {-o <old_raidtab>|-i <import_device>}  {-n <new_raidtab>| -e<export device>} [-m <md device>]\n"
	     "examples:\n" "  Resize an existing array:\n"
	     "    raidreconf -o /etc/raidtab.old -n /etc/raidtab.new -m /dev/md3\n"
	     "  Import data on a disk into an array:\n"
	     "    raidreconf -i /dev/sda3 -n /etc/raidtab -m /dev/md3\n");
}

int
main (int argc, const char *argv[])
{
	FILE *fp = 0;
	md_cfg_entry_t *p = 0;
	int exit_status = 0;
	int version = 0, help = 0;
	char *old_raidtab = 0, *import_device = 0,
	    *new_raidtab = 0, *export_device = 0, *md_device = 0;
	poptContext optCon;
	int i;
	struct poptOption optionsTable[] = {
		{"help", 'h', POPT_ARG_NONE, &help, 0},
		{"version", 'V', POPT_ARG_NONE, &version, 0},
		{"test", 't', POPT_ARG_NONE, &test, 0},
		{"old", 'o', POPT_ARG_STRING, &old_raidtab, 0},
		{"import", 'i', POPT_ARG_STRING, &import_device, 0},
		{"new", 'n', POPT_ARG_STRING, &new_raidtab, 0},
		{"export", 'e', POPT_ARG_STRING, &export_device, 0},
		{"mddev", 'm', POPT_ARG_STRING, &md_device, 0},
		{0, 0, 0, 0, 0}
	};

	optCon =
	    poptGetContext ("raidreconf", argc, argv, optionsTable, 0);
	if ((i = poptGetNextOpt (optCon)) < -1) {
		fprintf (stderr, "%s: %s\n",
			 poptBadOption (optCon, POPT_BADOPTION_NOALIAS),
			 poptStrerror (i));
		usage ();
		return EXIT_FAILURE;
	}
	if (i != -1) {
		fprintf (stderr, "Extra arguments given - giving up.\n");
		usage ();
		return EXIT_FAILURE;
	}

	if (!test && prepare_raidlib ())
		return EXIT_FAILURE;

	if (help) {
		usage ();
		return EXIT_FAILURE;
	}

	if (version) {
		printf ("raidreconf 0.1.2 for mkraid version %d.%d.%d\n",
			MKRAID_MAJOR_VERSION, MKRAID_MINOR_VERSION,
			MKRAID_PATCHLEVEL_VERSION);
		return EXIT_VERSION;
	}

	if (test) {
		printf
		    ("test mode - plain files are used, no kernel RAID interaction\n");
	}

	if (!test && getMdVersion (&ver))
		return EXIT_FAILURE;

	/*
	 * Check that mandatory arguments are given
	 */
	if (!md_device) {
		fprintf (stderr, "no md device given\n");
		usage();
		return EXIT_FAILURE;
	}
	/*
	 * Check for mutually exclusive arguments
	 */
	if (import_device && export_device) {
		fprintf (stderr, "cannot both import and export\n");
		return EXIT_FAILURE;
	}
	if (old_raidtab && import_device) {
		fprintf (stderr,
			 "cannot both have an import device and an old raidtab\n");
		return EXIT_FAILURE;
	}
	if (new_raidtab && export_device) {
		fprintf (stderr,
			 "cannot both have an export device and a new raidtab\n");
		return EXIT_FAILURE;
	}
	/*
	 * Check source -> destination path
	 */
	if (old_raidtab && !(new_raidtab || export_device)) {
		fprintf (stderr,
			 "old raidtab given, but no new raidtab and no export device\n");
		return EXIT_FAILURE;
	}
	if (import_device && !new_raidtab) {
		fprintf (stderr,
			 "import device given, but no new raidtab\n");
		return EXIT_FAILURE;
	}

	/*
	 * Good, now let's move on
	 */
	printf ("Working with device %s\n", md_device);

	/* 
	 * Treat import setup
	 */
	if (import_device) {
		printf ("I assume %s is a device you want to import\n",
			import_device);

		old_md_cfg = setup_single_config (import_device);
		if (!old_md_cfg) {
			fprintf (stderr, "Import of device failed!\n");
			return EXIT_FAILURE;
		}
	}

	if (old_raidtab) {
		fp = fopen (old_raidtab, "r");
		if (!fp) {
			fprintf (stderr, "Couldn't open %s -- %s\n",
				 old_raidtab, strerror (errno));
			return EXIT_FAILURE;
		}
		fprintf (stderr, "Parsing %s\n", old_raidtab);
		if (parse_config (fp)) {
			fprintf (stderr,
				 "Cannot parse old-array configuration file");
			return EXIT_FAILURE;
		}

		for (p = cfg_head; p; p = p->next) {
			if (strcmp (p->md_name, md_device))
				continue;
			old_md_cfg = p;
			break;
		}
		if (!p) {
			fprintf (stderr,
				 "device %s is not described in config file\n",
				 md_device);
			exit_status++;
		}
		fclose (fp);
	}

	/*
	 * Read the NEW configuration file 
	 */
	if (export_device) {
		printf ("I assume %s is a device you want to export to\n",
			export_device);

		new_md_cfg = setup_single_config (export_device);
		if (!new_md_cfg) {
			fprintf (stderr, "Export to device failed!\n");
			return EXIT_FAILURE;
		}
	}

	if (new_raidtab) {
		fp = fopen (new_raidtab, "r");
		if (!fp) {
			fprintf (stderr, "Couldn't open %s -- %s\n",
				 new_raidtab, strerror (errno));
			return EXIT_FAILURE;
		}
		fprintf (stderr, "Parsing %s\n", new_raidtab);
		if (parse_config (fp)) {
			fprintf (stderr,
				 "Cannot parse new-array configuration file.\n");
			return EXIT_FAILURE;
		}

		/* Seek from where we left. */
		for (p = (p ? p->next : cfg_head); p; p = p->next) {
			if (strcmp (p->md_name, md_device))
				continue;
			new_md_cfg = p;
			break;
		}
		if (!p) {
			fprintf (stderr,
				 "device %s is not described in config file\n",
				 md_device);
			exit_status++;
			return EXIT_FAILURE;
		}
	}

	if (raid_reconfigure (&ver)) {
		fprintf (stderr, "reconfiguration failed\n");
		return EXIT_FAILURE;
	}

	return 0;
}


#define P(x) printf("%18s: \t %d\n",#x,cfg->array.param.x)
#define DP(x) printf("%18s: \t %d\n",#x,cfg->array.disks[i].x)


void
printcfg (md_cfg_entry_t * cfg)
{
	int i;

	P (major_version);
	P (minor_version);
	P (patch_version);
	P (ctime);
	P (level);
	P (size);
	P (nr_disks);
	P (raid_disks);
	P (md_minor);

	P (utime);
	P (state);
	P (active_disks);
	P (working_disks);
	P (failed_disks);
	P (spare_disks);

	P (layout);
	P (chunk_size);

	for (i = 0; i < cfg->array.param.nr_disks; i++) {
		printf ("\n");

		DP (number);
		DP (major);
		DP (minor);
		DP (raid_disk);
		DP (state);
	}
}

static int
read_needed_entries (md_cfg_entry_t * cfg)
{
	int i;

	for (i = 0; i != cfg->array.param.nr_disks; i++) {
		int disk_num = cfg->array.disks[i].raid_disk;
		char *devname = cfg->device_name[disk_num];
		int fd = open (devname, O_RDWR);
		int nr_blocks;

		/* sb_block_offset code */
		if (fd == -1) {
			fprintf (stderr, "Cannot open %s for read/write\n",
				 devname);
			return 1;
		}
		if (test) {
			struct stat sbuf;

			if (fstat (fd, &sbuf)) {
				fprintf (stderr,
					 "Couldn't stat device %s -- %s\n",
					 devname, strerror (errno));
				return 1;
			}
			if (((sbuf.st_size >> 10) << 10) != sbuf.st_size) {
				fprintf (stderr,
					 "Sorry, device %s size %lu is bad\n",
					 devname, sbuf.st_size);
				return 1;
			}
			nr_blocks = sbuf.st_size >> 10;
		}
		else {
			if (ioctl
			    (fd, BLKGETSIZE,
			     (unsigned long) &nr_blocks) == -1) {
				fprintf (stderr,
					 "couldn't get device size for %s -- %s\n",
					 devname, strerror (errno));
				return 1;
			}
			nr_blocks >>= 1;
		}
		if (nr_blocks < MD_RESERVED_BLOCKS * 2) {
			fprintf (stderr, "%s: device too small (%dkB)\n",
				 devname, nr_blocks);
			return 1;
		}
		cfg->sb_block_offset[i] = MD_NEW_SIZE_BLOCKS (nr_blocks);

		close (fd);
	}
	return 0;
}


int
raid_get_size (md_cfg_entry_t * cfg, unsigned long *sum)
{
	int i;

	*sum = 0;
	for (i = 0; i != cfg->array.param.nr_disks; i++) {
		int dnum = cfg->array.disks[i].raid_disk;
		char *devname = cfg->device_name[dnum];
		int fd = open (devname, O_RDONLY);
		unsigned long nr_blocks;

		if (fd == -1) {
			fprintf (stderr, "Cannot open %s for read/write\n",
				 devname);
			return 1;
		}
		if (test) {
			struct stat sbuf;

			if (fstat (fd, &sbuf)) {
				fprintf (stderr,
					 "Couldn't stat device %s -- %s\n",
					 devname, strerror (errno));
				return 1;
			}
			if (((sbuf.st_size >> 9) << 9) != sbuf.st_size) {
				fprintf (stderr,
					 "Sorry, device %s size %lu is bad\n",
					 devname, sbuf.st_size);
				return 1;
			}
			nr_blocks = sbuf.st_size >> 9;
		}
		else {
			if (ioctl
			    (fd, BLKGETSIZE,
			     (unsigned long) &nr_blocks) == -1) {
				fprintf (stderr,
					 "couldn't get device size for %s -- %s\n",
					 devname, strerror (errno));
				return 1;
			}
		}
		*sum += nr_blocks;
		close (fd);
	}
	return 0;
}

/* The returned arrays oldarry and newarry holds filedescriptors and 
 * other info. on the raid-disk nr.   oldarry[i] refers to raid-disk i,
 * not necessarily device number i.
 */
int
setup_disk_arrays (md_cfg_entry_t * oldcfg, rrc_disk_t ** oldarry,
		   md_cfg_entry_t * newcfg, rrc_disk_t ** newarry)
{
	int i;
	char **didnames;
	int cid;

	/* allocate fdsets and open files */
	*oldarry =
	    (rrc_disk_t *) malloc (sizeof (rrc_disk_t) *
				   oldcfg->array.param.nr_disks);
	*newarry =
	    (rrc_disk_t *) malloc (sizeof (rrc_disk_t) *
				   newcfg->array.param.nr_disks);
	if (!*oldarry || !*newarry) {
		fprintf (stderr, "rrc_disk_t array malloc failure!\n");
		return 1;
	}

	didnames =
	    (char **) malloc (sizeof (char *) *

			      (oldcfg->array.param.nr_disks +
			       newcfg->array.param.nr_disks));
	if (!didnames) {
		fprintf (stderr, "error allocating didnames array\n");
		return 1;
	}

	cid = 0;
	didnames[cid] =
	    oldcfg->device_name[oldcfg->array.disks[0].raid_disk];

	for (i = 0; i != oldcfg->array.param.nr_disks; i++) {
		int dnum = oldcfg->array.disks[i].raid_disk;
		char *devname = oldcfg->device_name[dnum];

		(*oldarry)[i].fd = open (devname, O_RDONLY);
		if ((*oldarry)[i].fd == -1) {
			fprintf (stderr, "error opening %s for reading\n",
				 devname);
			return 1;
		}

		(*oldarry)[i].chunks =
		    oldcfg->sb_block_offset[dnum] /
		    (oldcfg->array.param.chunk_size / BLOCK_SIZE);
		(*oldarry)[i].blocks = oldcfg->sb_block_offset[dnum];
		fprintf (stderr,
			 "Old raid-disk %i has %lu chunks, %lu blocks\n",
			 dnum, (*oldarry)[i].chunks, (*oldarry)[i].blocks);
		/* Build new ID. We assume devices are unique, which should be a fair assumption */
		if (strcmp (didnames[cid], devname)) {
			cid++;
			didnames[cid] = devname;
		}
		(*oldarry)[i].disk_id = cid;
	}
	for (i = 0; i != newcfg->array.param.nr_disks; i++) {
		int dnum = newcfg->array.disks[i].raid_disk;
		char *devname = newcfg->device_name[dnum];
		int idi;

		(*newarry)[i].fd = open (devname, O_WRONLY);
		if ((*newarry)[i].fd == -1) {
			fprintf (stderr, "error opening %s for writing\n",
				 devname);
			return 1;
		}

		(*newarry)[i].chunks =
		    newcfg->sb_block_offset[dnum] /
		    (newcfg->array.param.chunk_size / BLOCK_SIZE);
		(*newarry)[i].blocks = newcfg->sb_block_offset[dnum];
		fprintf (stderr,
			 "New raid-disk %i has %lu chunks, %lu blocks\n",
			 dnum, (*newarry)[i].chunks, (*newarry)[i].blocks);
		/* Assign ID. Search the didnames table */
		for (idi = 0; idi <= cid; idi++) {
			if (!strcmp (didnames[idi], devname))
				break;
		}
		if (idi > cid) {
			cid++;
			didnames[cid] = devname;
			idi = cid;
		}
		(*newarry)[i].disk_id = idi;
	}

	free (didnames);

	return 0;
}



static int
raid_reconfigure (mdu_version_t *ver)
{
	int fd;
	unsigned char buffer[MD_SB_BYTES];
	mdp_super_t *phys_sb = (mdp_super_t *) buffer;
	md_cfg_entry_t *cptr;
	unsigned long oldsum = 0;
	unsigned long newsum = 0;
	int rc;
	const char *ret;

	assert (old_md_cfg && new_md_cfg);

#if 0

	printf ("new device: %s\n", new_md_cfg->md_name);
	printcfg (new_md_cfg);

	printf ("old device: %s\n", old_md_cfg->md_name);
	printcfg (old_md_cfg);

#endif

	/* Initialize the sb_block_offset fields in the old_md_cfg
	 * and new_md_cfg structs.
	 */
	for (cptr = old_md_cfg; cptr;) {

		/* We refuse to touch anything thats active */
		if (check_active (cptr))
			return 1;

		/* If there's no persistent superblock, we give up.  This utility relies
		 * on persistent superblocks. */
		if (cptr->array.param.not_persistent) {
			fprintf (stderr,
				 "Sorry, your array MUST use persistent superblocks for this utility to work\n");
			return 1;
		}

		if (read_needed_entries (cptr))
			return 1;

		/* ``iterate'' */
		if (cptr == new_md_cfg)
			cptr = 0;
		else
			cptr = new_md_cfg;
	}


	/* Read the superblock from the first disk in the (old) array, if we're not importing... */
	if (old_md_cfg->array.param.level != RRC_SINGLE_DISK_LEVEL) {
		assert (old_md_cfg->array.param.nr_disks);
		fd = open (old_md_cfg->device_name[0], O_RDONLY);
		if (fd == -1) {
			fprintf (stderr,
				 "Uhoh, cannot open first disk in array: %s\n",
				 *cfg->device_name);
			return 1;
		}
		if (raidseek (fd, old_md_cfg->sb_block_offset[0]) == -1)
			return 1;
		if ((read (fd, buffer, MD_SB_BYTES)) != MD_SB_BYTES)
			return 1;
		phys_sb = (mdp_super_t *) buffer;
		if (phys_sb->md_magic != MD_SB_MAGIC) {
			fprintf (stderr,
				 "Sorry, %s doesn't seem to hold a superblock.\n",
				 old_md_cfg->device_name[0]);
			return 1;
		}
		close (fd);
		/*
		 * A few generic checks:
		 *
		 * 1) Our on-disk array must be clean
		 *
		 * 2) We could check that the old config matches the superblock read, but this
		 *    is not all that important. Besides, it would be great if we could run this
		 *    without even having an old config.
		 *
		 */
		if (!(phys_sb->state & (1 << MD_SB_CLEAN))) {
			fprintf (stderr,
				 "Your on-disk array MUST be clean first.\n");
			return 1;
		}
	}

	/* Sum up the number of blocks in the new and the old layout */
	rc = raid_get_size (new_md_cfg, &newsum);
	rc += raid_get_size (old_md_cfg, &oldsum);
	if (rc) {
		fprintf (stderr, "Cannot get RAID sizes...\n");
		return 1;
	}
	printf
	    ("Size of old array: %lu blocks,  Size of new array: %lu blocks\n",
	     oldsum, newsum);

	/* Decide what we're supposed to do....

	 * What we currently CAN do:
	 *
	 * Expand/Shrink RAID0
	 * Expand/Shrink RAID5
	 * Import non-raid data onto RAID 0/5
	 * Convert RAID0 to RAID5 or vice-versa
	 *
	 */

	switch (old_md_cfg->array.param.level) {
	case RRC_SINGLE_DISK_LEVEL:
		source_driver = new_single_driver ();
		break;
	case 0:
		source_driver = new_raid0_driver ();
		break;
	case 5:
		source_driver = new_raid5_driver ();
		break;
	}
	switch (new_md_cfg->array.param.level) {
	case RRC_SINGLE_DISK_LEVEL:
		sink_driver = new_single_driver ();
		break;
	case 0:
		sink_driver = new_raid0_driver ();
		break;
	case 5:
		sink_driver = new_raid5_driver ();
		break;
	}

	if (!source_driver) {
		fprintf (stderr,
			 "No source driver available for level %i\n",
			 old_md_cfg->array.param.level);
		return 1;
	}
	if (!sink_driver) {
		fprintf (stderr, "No sink driver available for level %i\n",
			 new_md_cfg->array.param.level);
		return 1;
	}

	/* Just in case we're dealing with a single-disk device, set their chunk_size 
	 * to the chunk_size of the real RAID device */
	if (old_md_cfg->array.param.level == RRC_SINGLE_DISK_LEVEL
	    && new_md_cfg->array.param.level == RRC_SINGLE_DISK_LEVEL) {
		fprintf (stderr,
			 "Your're using raidreconf to copy data from one single disk to another ?\n"
			 "dd is a better tool for that, but nevermind.  Setting block size to 1K\n");
		old_md_cfg->array.param.chunk_size =
		    new_md_cfg->array.param.chunk_size = MD_BLK_SIZ;
	}
	else {
		if (old_md_cfg->array.param.level == RRC_SINGLE_DISK_LEVEL)
			old_md_cfg->array.param.chunk_size =
			    new_md_cfg->array.param.chunk_size;
		else if (new_md_cfg->array.param.level ==
			 RRC_SINGLE_DISK_LEVEL) new_md_cfg->array.param.
			    chunk_size =
			    old_md_cfg->array.param.chunk_size;
	}

	rc =
	    setup_disk_arrays (old_md_cfg, &old_rrc_cfg, new_md_cfg,
			       &new_rrc_cfg);
	if (rc) {
		fprintf (stderr,
			 "There was a fatal error while setting up the arrays for reconfiguration.\n");
		return 1;
	}

	if ((ret = run_reconfiguration ())) {
		fprintf (stderr, "Reconfiguration failed: \"%s\"\n", ret);
		return 1;
	}

	return 0;
}


md_cfg_entry_t *
setup_single_config (const char *devname)
{
	/* This piece here is modelled after the code in parser.c */
	/* It will fill in the fields in the md_cfg_entry_t that we
	 * need to treat the new non-RAID drive as an array (for imports)
	 */
	md_cfg_entry_t *ret =

	    (md_cfg_entry_t *) malloc (sizeof (md_cfg_entry_t));
	if (!ret) {
		fprintf (stderr,
			 "I cannot allocate memory for the new-drive cfg_entry.\n");
		exit (1);
	}
	memset (ret, 0, sizeof (md_cfg_entry_t));
	/* We need fields:
	 * .array.param.level
	 * .array.param.nr_disks
	 * .array.disks[i].raid_disk
	 * .device_name[i]
	 *
	 * Later these fields are filled in:
	 * .sb_block_offset[i]
	 * .array.param.chunk_size  (must be zero from here)
	 */
	ret->array.param.level = RRC_SINGLE_DISK_LEVEL;
	ret->array.param.nr_disks = 1;
	ret->array.param.chunk_size = 0;
	ret->array.disks[0].raid_disk = 0;
	ret->md_name = ret->device_name[0] = strdup (devname);
	if (!ret->md_name) {
		fprintf (stderr,
			 "Cannot allocate memory for strdup in single_drive config\n");
		exit (1);
	}

	return ret;
}
