/*
 scsiadd - add/remove scsi devices from the linux scsi subsystem
 ----------------------------------------------------------------
 Copyright (C) 2000 by Dirk Jagdmann <doj@cubic.org>

 This program 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
 of the License, or (at your option) any later version.

 This program 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 this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <vector>
#include <iterator>
#include <iostream>

//#define DEBUG

class ScsiDev;
ostream& operator<<(ostream& os, const ScsiDev &s);

/**
   ScsiDev is used to compare SCSI devices. Hostnumber, Channelnumber,
   SCSI ID and LUN are used in comparison.
 */
class ScsiDev
{
 protected:
  /// variables to store host, channel, id and lun
  int h, c, i, l;

 public:
  /**
     constructs a ScsiDev object.
     @param hh the Host number
     @param cc the Channel number
     @param ii the SCSI ID
     @param ll the LUN
   */
  ScsiDev(int hh, int cc, int ii, int ll)
    : h(hh), c(cc), i(ii), l(ll)
    {
    }

  const bool operator==(const ScsiDev &s)
  {
    if(&s==this)
      return true;

    return (s.h==h && s.c==c && s.i==i && s.l==l);
  }

  /**
     returns whether s is greater or equal than this. comparision is only
     true, if host, channel match and the ID is greater.
     @param s ScsiDev object to compare
   */
  const bool operator>=(const ScsiDev &s)
  {
    if(&s==this)
      return false;
 
    return (s.h==h && s.c==c && i>=s.i);
  }

  /**
     returns whether s is lower than this. comparision os onyl true, if
     host, channel match and ID is lower.
     @param s ScsiDev object to compare
   */  
  const bool operator<(const ScsiDev &s)
  {
    return !(this->operator>=(s));
  }

  /**
     prints the objects state onto the Output Stream os.
     @param os the Output Stream
     @return the Output Stream os
   */
  ostream& dump(ostream& os) const
  {
    os << h << " " << c << " " << i << " " << l << " ";
    return os;
  }

 private:
  /// this constructor can not be used
  ScsiDev();
};

/**
   operator overloading of << for ScsiDev
   @param os the Output Stream s should be written to
   @param s the ScsiDev object to write
   @return the Output Stream os
*/
ostream& operator<<(ostream& os, const ScsiDev &s)
{
  return s.dump(os);
}

/// a convinience typedef for a list of ScsiDevs
typedef vector<ScsiDev> Devices_t;

/// a global list of ScsiDevs
Devices_t Devices;

/// the modes in which the program should operate
typedef enum
{
  SINGLE,
  SCAN
} scsiaddmode_t;

/// action the program should commit
typedef enum
{
  ADD,
  REMOVE
} scsiaddaction_t;

/// force add/remove even in dangerous situations
bool force=false;

/// print the help message and terminate program
static void help()
{
  printf(
	 "scsiadd %1.1f - add and remove devices from the scsi subsystem\n"
	 "---------------------------------------------------------------\n"
	 "syntax: scsiadd [-a|-r] <id>\n"
	 "        scsiadd [-a|-r] <id> <lun>\n"
	 "        scsiadd [-a|-r] <host> <id> <lun>\n"
	 "        scsiadd [-a|-r] <host> <channel> <id> <lun>\n"
	 "        scsiadd -s <host>\n"
	 "        scsiadd -s <host> <channel>\n"
         "        scsiadd -p\n"
	 "parameters not given are assumed 0\n"
	 "-a: add a device (default if no command given)\n"
         "-f: force\n"
	 "-r: remove device\n"
	 "-s: scan for devices\n"
         "-p: print scsi status\n"
         "-h: print this help message\n"
         "\nhttp://llg.cubic.org/misc - ftp://ftp.cubic.org/pub/llg\n",
	 VER
	 );

  exit(1);
}

/** 
    check if the calling user is root. If not try to make the program run as root.
    If program can not be made root, terminate.
*/
void becomeRoot() 
{
  uid_t me;

  me = getuid();
  if(me!=0) 
    {
#ifdef DEBUG     
      cerr << "Aha, I am not root. Therefore I will now try to become root" << endl;
#endif
      if(geteuid()!=0) 
	{
	  cerr << "I can not become root. scsiadd was most likely started by a normal user." << endl;
	  cerr << "If you want to make this program suid root use make install-suidroot when"<< endl;
	  cerr << "installing or (as root) type: chmod +s " << PATH << endl;
	  exit(1);
	}
      setuid(0);
    }
}

/** 
    perform the command to add/remove a scsi device.
    @param action action that will be performed. currently supported are ADD and REMOVE.
    @param host host number 
    @param channel channel number 
    @param id id number 
    @param lun lun number 
    @return 0 on success. -1 on error
*/
static int scsiadd(const scsiaddaction_t action, 
		   const int host, 
		   const int channel, 
		   const int id, 
		   const int lun)
{
  ScsiDev d(host, channel, id, lun);
  
  if(!force)
    {
      bool flag=true;
      for(Devices_t::iterator i=Devices.begin(); i<Devices.end(); ++i)
	if(d<*i)
	  flag=false;
      
      if(flag==false)
	{
	  cerr << "scsiadd:scsiadd(): warning, not operating on the last/greatest device!" << endl;
	  cerr << "See README for details. Use -f to override." << endl;
	  return -1;
	}
    }
  
  char *actionstr;
  switch(action)
    {
    case ADD: 
      actionstr="add"; 
      break;
    case REMOVE: 
      actionstr="remove"; 
      if(!force)
	{
	  // check if the device is managed
	  bool flag=false;
	  for(Devices_t::iterator i=Devices.begin(); i<Devices.end(); ++i)
	    if(d==*i)
	      flag=true;
	  
	  if(flag==false)
	    {
	      cerr << "scsiadd:scsiadd(): error! Device not managed by SCSI system. aborting..." << endl;
	      return -1;
	    }
	}
      break;
    default: 
      cerr << "scsiadd:scsiadd(): oops, action error" << endl; 
      return -1;
    }

  FILE *f=fopen("/proc/scsi/scsi", "w");
  if(f == NULL)
    {
      perror("scsiadd:scsiadd():could not open /proc/scsi/scsi (w)");
      return -1;
    }

  fprintf(f, "scsi %s-single-device %i %i %i %i\n", actionstr, host, channel, id, lun);

  if(fclose(f) < 0)
    {
#ifdef DEBUG
      perror("scsiadd:scsiadd():could not close /proc/scsi/scsi (w)");
#endif
    }

  return 0;
}

/** 
    prints the contents of /proc/scsi/scsi to output stream
    @param os the Output Stream
    @return 0 on success. -1 on error
*/
static int scsidump(FILE *os=stdout)
{
  if(os==NULL)
    return -1;

  FILE *f=fopen("/proc/scsi/scsi", "r");
  if(f == NULL)
    {
      perror("scsiadd:scsidump():could not open /proc/scsi/scsi (r)");
      return -1;
    }

  while(!feof(f))
    {
      char buf[85];
      int i=fread(buf, 1, 80, f);
      buf[i]=0;
      fprintf(os, "%s", buf);
    }

  if(fclose(f) < 0)
    {
#ifdef DEBUG
      perror("scsiadd:scsidump():could not close /proc/scsi/scsi (r)");
#endif
    }

  return 0;
}

/**
   scans devices on an SCSI bus. Devices found are added to the global Devices list.
   @param host host number of the bus
   @param channel channel number of the bus
   @return 0 on success. -1 on error
 */
int scsiScan(int host, int channel)
{
  FILE *f=fopen("/proc/scsi/scsi", "r");
  if(f == NULL)
    {
      perror("scsiadd:scsiScan():could not open /proc/scsi/scsi (r)");
      return -1;
    }

  while(!feof(f))
    {
      char buf[85];
      if(fgets(buf, 81, f) == NULL)
	break;

      int h, c, i, l;
      int r=sscanf(buf, "Host: scsi%i Channel: %i Id: %i Lun: %i", &h, &c, &i, &l);
      if(r==4)
	Devices.push_back(ScsiDev(h,c,i,l));
    }

  if(fclose(f) < 0)
    {
#ifdef DEBUG
      perror("scsiadd:scsiScan():could not close /proc/scsi/scsi (r)");
#endif
    }

#ifdef DEBUG
  clog << "Scan results:" << endl;
  for(Devices_t::iterator i=Devices.begin(); i<Devices.end(); ++i)
    clog << *i << endl;
#endif

  return 0;
}

/**
   main function.
   @param argc number of command line items
   @param argv pointer to command line items
   @return 0 on success. 1 on error
 */
int main(int argc, char **argv)
{
  int host=0, channel=0, id=0, lun=0;
  scsiaddmode_t scanmode=SINGLE;
  scsiaddaction_t mode=ADD;

  // parse command line
  int c=0;
  while((c=getopt(argc, argv, "afprs"))!=EOF)
    {
      switch(c)
	{
	case 'a': mode=ADD; break;
	case 'f': force=1; break;
	case 'p': scsidump(); return 0;
	case 'r': mode=REMOVE; break;
	case 's': scanmode=SCAN; break;
	case '?':
	case 'h':
	default: help();
	}
    }

  // convert the supplied numbers
  switch(scanmode)
    {
    case SINGLE:
      switch(argc-optind)
	{
	case 1: 
	  id=atoi(argv[optind]); 
	  break;
	case 2: 
	  id=atoi(argv[optind]); 
	  lun=atoi(argv[optind+1]); 
	  break;
	case 3: 
	  host=atoi(argv[optind]); 
	  id=atoi(argv[optind+1]); 
	  lun=atoi(argv[optind+2]); 
	  break;
	case 4: 
	  host=atoi(argv[optind]); 
	  channel=atoi(argv[optind+1]); 
	  id=atoi(argv[optind+2]); 
	  lun=atoi(argv[optind+3]); 
	  break;
	default:
	  help();
	}
      break;
    case SCAN:
      switch(argc-optind)
	{
	case 1:
	  host=atoi(argv[optind]);
	  break;
	case 2:
	  host=atoi(argv[optind]);
	  channel=atoi(argv[optind+1]);
	  break;
	}
      break;
    default:
      cerr << "scsiadd: oops! scanmode error." << endl; return 1;
    }

#ifdef DEBUG
  printf("command args: %i %i %i %i %i\n", mode, host, channel, id, lun);
#endif

  becomeRoot();

  if(scsiScan(host, channel) < 0)
    {
      cerr <<  "scsiadd: could not scan for SCSI devices. aborting..." << endl;
      exit(1);
    }

  int ret=0;
  switch(scanmode)
    {
    case SINGLE: 
      ret=scsiadd(mode, host, channel, id, lun); 
      break;
    case SCAN:
      for(int i=0; i<16; i++)
	{
#ifdef DEBUG
	  printf("scan: %i %i %i %i %i\n", mode, host, channel, id, lun);
#endif
	  ret=scsiadd(mode, host, channel, i, lun);
	}
      break;
    default: 
      cerr << "scsiadd: oops! scanmode error" << endl; 
      return 1;
    }

  if(ret>=0)
    ret=scsidump();
  
  return -ret;
}
