#!/usr/bin/env python2.2
# -*- coding: iso-8859-1 -*-
# vim: set ft=python ts=3 sw=3 expandtab:
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
#              C E D A R
#          S O L U T I O N S       "Software done right."
#           S O F T W A R E
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Copyright (c) 2002-2003 Kenneth J. Pronovici.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# Version 2, as published by the Free Software Foundation.
#
# 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.
#
# Copies of the GNU General Public License are available from
# the Free Software Foundation website, http://www.gnu.org/.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Author   : Kenneth J. Pronovici <pronovic@ieee.org>
# Language : Python (>= 2.2)
# Project  : Cedar Backup
# Revision : $Id: cback,v 1.15 2002/11/10 03:21:32 pronovic Exp $
# Purpose  : Main user interface for Cedar Backup
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

# This file was created with a width of 132 characters, and NO tabs.

######################
# Pydoc documentation
######################

"""
Cedar Backup main front-end script.
"""

__author__  = "Kenneth J. Pronovici"


########################################################################
# Imported modules
########################################################################

import sys
try:
   import getopt
   import time
   import string
   import pwd
   import grp
   import os
   import CedarBackup.release
   import CedarBackup.config
   import CedarBackup.functional
except ImportError, e:
    print "Failed to import modules: %s" % e
    print "Please try setting the PYTHONPATH environment variable."
    sys.exit(1)


#######################################################################
# Script-wide configuration and constants
#######################################################################

DEFAULT_CONFIG    = "/etc/cback.conf"
DEFAULT_LOGFILE   = "/var/log/cback.log"
DEFAULT_OWNERSHIP = "root:adm"
DEFAULT_MODE      = 0640
TIME_FORMAT       = "%d-%b-%Y %H:%M:%S"


#######################################################################
# Functions
#######################################################################


###################
# usage() function
###################

def usage(error=None):

   """Provides usage information for the cback script."""

   if error is not None:
      print ""
      print " [Error - %s.]" % error

   print ""
   print " Usage: cback [switches]"
   print ""
   print " The following switches are accepted:"
   print ""
   print "   -h, --help     Display this usage/help listing"
   print "   -V, --version  Display version information"
   print "   -c, --config   Path to config file (default: %s)" % DEFAULT_CONFIG
   print "   -l, --logfile  Path to logfile (default: %s)" % DEFAULT_LOGFILE
   print "   -o, --owner    Logfile ownership, user:group (default: %s)" % DEFAULT_OWNERSHIP
   print "   -m, --mode     Logfile permissions mode (default: %o)" % DEFAULT_MODE
   print "   -v, --valid    Validate configuration, but take no other action"
   print "   -b, --verbose  Print verbose output as well as logging to disk"
   print "   -f, --full     Perform a full backup, regardless of configuration"
   print "   -a, --all      Take all actions (collect, stage, store, purge)"
   print "   -c, --collect  Take the collect action"
   print "   -s, --stage    Take the stage action"
   print "   -t, --store    Take the store action"
   print "   -p, --purge    Take the purge action"
   print "   -r, --rebuild  Rebuild \"this week's\" disc if possible"
   print ""
   print " You must specify at least one action (collect, stage, store,"
   print " purge or rebuild), or use the --all option to specify all"
   print " normal actions.  Specifying rebuild excludes all other actions."
   print ""
   print " See the cback(1) manpage for a more detailed explanation of usage."
   print ""
   print " Copyright (c) %s Kenneth J. Pronovici." % CedarBackup.release.COPYRIGHT
   print " Distributed under the GNU GPL; see http://www.gnu.org/ for info."
   print ""


#####################
# version() function
#####################

def version():

   """Provides version information for the cback script."""

   print " Cedar Backup version %s, released %s." % (CedarBackup.release.VERSION, CedarBackup.release.DATE)
   print " Use the --help option for usage information."


######################
# writelog() function
######################

error_with_logfile = False

def writelog(logfile, lines, user=None, group=None, mode=None):

   """
   Appends lines to a logfile.

   Arguments:

      - **logfile** : The filepath of the logfile

      - **lines** : Array of lines, suitable to be passed to writelines(), to be written to the logfile

      - **user** : User to own the logfile, if it must be created

      - **group** : Group to own the logfile, if it must be created

      - **mode** : Filesystem mode for the logfile (i.e. octal 0640)

   If the logfile already exists, and is writable, the existing logile
   is appended to, and if possible, the ownership and mode are changed
   to match the passed in values.

   If the logfile exists, but cannot be written to, an exception is raised.

   If the logfile does not exist, it's created with the passed-in
   ownership and mode, if possible.

   In any case, the default ownership and mode will be used if no values
   are passed in.  To change ownership, both the user and group values
   must be passed in.
   """

   #########################
   # Make globals available
   #########################

   global error_with_logfile


   #############################################
   # Check whether we can even write to the log
   #############################################

   if logfile is None or error_with_logfile:
      return


   #################################
   # Check passed-in user and group
   #################################

   if (user is not None and group is None) or (user is None and group is not None):
      print "Unable to open logfile '%s'.  User and group must be passed in as a pair." % (logfile)
      error_with_logfile = True
      return
   elif user is not None and group is not None:
      try:
         uid = pwd.getpwnam(user)[2]
         gid = grp.getgrnam(group)[2]
      except:
         print "Unable to open logfile '%s'.  Passed-in user '%s' group '%s' invalid." % (logfile, user, group)
         error_with_logfile = True
         return


   #################################
   # Write the lines to the logfile
   #################################

   # There's a bit of a race condition here, as I guess it's possible
   # for the file to be moved off in between the os.access() call and
   # the writelines() call... in which case, the file will have the
   # wrong ownership and permissions.  I am really not quite sure
   # what to do about that, because the only real alternative is to
   # do the chmod() and chown() each and every time we write, which
   # seems silly.  I guess we'll just assume that if something (say,
   # logrotate) moves off the log, that it re-creates the log with the
   # right ownership and permissions.  In that case, we'll be safe.

   try:
      if os.access(logfile, os.F_OK):
         open(logfile, "a").writelines(lines)
      else:
         open(logfile, "a").writelines(lines)
         try:
            if user is not None and group is not None:
               os.chmod(logfile, mode)
            if mode is not None:
               os.chown(logfile, uid, gid)
         except:
            pass
   except:
      print "Unable to write to logfile '%s'." % (logfile)
      error_with_logfile = True
      return


#########################################
# mark_start() and mark_stop() functions
#########################################

def mark_start(logfile, user, group, mode, opts):
   """Marks in the log that a backup run has started."""
   try:
      writelog(logfile, ["\nCedar Backup run started at %s.\nArguments were: %s\n" % (time.strftime(TIME_FORMAT), opts)], 
               user, group, mode)
   except:
      pass

def mark_stop(logfile, user, group, mode, status, message=None):
   """Marks in the log that a backup run has completed."""
   try:
      if message is not None:
         writelog(logfile, ["%s\n" % message], user, group, mode)
      writelog(logfile, ["Cedar Backup run completed at %s with status %d.\n" % (time.strftime(TIME_FORMAT), status)],
               user, group, mode)
   except:
      pass
   sys.exit(status)


############################
# handle_results() function
############################

def handle_results(success_message, error_message, verbose, logfile, user, group, mode, output, errors, warnings):

   ######################
   # Pydoc documentation
   ######################

   """
   Handles results from CedarBackup.functional functions.

   Arguments:

      - **success_message** : Message to print upon success

      - **error_message** : Message to print upon error

      - **verbose** : Flags verbose operation

      - **logfile** : Name of the logfile

      - **output** : Output list from called function

      - **errors** : Errors list from called function

      - **warnings** : Warnings list from called function
   """

   #########################
   # Make globals available
   #########################

   # This is set by writelog() if there's some sort of logging problem
   global error_with_logfile


   #######################
   # Write to the logfile
   #######################

   if output is not None:
      writelog(logfile, output, user, group, mode)
   if warnings is not None:
      writelog(logfile, warnings, user, group, mode)
   if errors is not None:
      writelog(logfile, errors, user, group, mode)


   ######################
   # Write to the screen
   ######################

   # Write output and warnings to the screen if verbose is set
   if verbose or error_with_logfile:
      if output is not None:
         sys.stdout.writelines(output)
      if warnings is not None:
         sys.stdout.writelines(warnings)
   
   # Write errors to the screen
   if errors is not None:
      sys.stdout.writelines(errors)


   ####################################
   # Get out if error(s) are indicated
   ####################################

   if errors is not None:
      if error_message is not None:
         print error_message
         writelog(logfile, ["%s\n" % error_message], user, group, mode)
      if not verbose and not error_with_logfile:
         print "See logfile %s for details." % logfile
      mark_stop(logfile, user, group, mode, 1)
   else:
      if success_message is not None:
         writelog(logfile, ["%s (%s)\n" % (success_message, time.strftime(TIME_FORMAT))], user, group, mode)


###############
# Main routine
###############

def main():

   """Main routine."""

   ############
   # Variables
   ############

   config_file    = None
   logfile        = None
   mode           = None
   user           = None
   group          = None
   valid          = False
   verbose        = False
   full           = False
   collect        = False
   stage          = False
   store          = False
   purge          = False
   rebuild        = False

   switches       = { }


   ####################
   # Check the version
   ####################
   
   try:
      if map(int, [sys.version_info[0], sys.version_info[1]]) < [2, 2]:
         print "Python version 2.2 or greater required, sorry."
         sys.exit(1)
   except:
      # sys.version_info isn't available before 2.0
      print "Python version 2.2 or greater required, sorry."
      sys.exit(1)


   #########################
   # Parse the command-line
   #########################
   # See usage() for a brief explanation of arguments 

   # First, get the options and arguments
   try:
      opts, args = getopt.getopt(sys.argv[1:], 
                                 "hVclvbfacstpr",
                                 [ 'help', "version",
                                   'config=', 'logfile=', "owner=", "mode=", 
                                   'valid', 'verbose', 'full', 'all',
                                   'collect', 'stage', 'store', 'purge', 'rebuild' ])
   except getopt.GetoptError:
      usage()
      sys.exit(2)


   # There should be no non-switch arguments
   if len(args) > 0 or len(opts) == 0:
      usage()
      sys.exit(2)

   # Put the switches in a hash for easy use (we don't allow multiple
   # occurrences of the same switch, so this is safe)
   for o,a in opts:
      switches[o] = a

   # Print help if needed
   if switches.has_key('-h') or switches.has_key('--help'):
      usage()
      sys.exit(0)

   # Print version if requested
   if switches.has_key('-V') or switches.has_key('--version'):
      version()
      sys.exit(0)

   # Pull of configuration, and set flags 
   if switches.has_key('-c') or switches.has_key('--config'):
      if switches.has_key('-c'):
         config_file = switches['-c']
      else:
         config_file = switches['--config']
   else:
      config_file = DEFAULT_CONFIG

   if switches.has_key('-l') or switches.has_key('--logfile'):
      if switches.has_key('-l'):
         logfile = switches['-l']
      else:
         logfile = switches['--logfile']
   else:
      logfile = DEFAULT_LOGFILE

   if switches.has_key('-o') or switches.has_key('--owner'):
      try:
         if switches.has_key('-o'):
            user, group = string.split(switches['-o'], ":", 1)
         else:
            user, group = string.split(switches['--owner'], ":", 1)
      except:
         usage("Invalid owner")
         sys.exit(2)
   else:
      user, group = string.split(DEFAULT_OWNERSHIP, ":", 1)

   if switches.has_key('-m') or switches.has_key('--mode'):
      try:
         if switches.has_key('-m'):
            mode = int("0%s" % switches['-m'], 8)
         else:
            mode = int("0%s" % switches['--mode'], 8)
      except:
         usage("Invalid mode")
         sys.exit(2)
   else:
      mode = DEFAULT_MODE

   if switches.has_key('-v') or switches.has_key('--valid'):
      valid = True
   else:
      valid = False

   if switches.has_key('-b') or switches.has_key('--verbose'):
      verbose = True
   else:
      verbose = False

   if switches.has_key('-f') or switches.has_key('--full'):
      full = True
   else:
      full = False

   if switches.has_key('-c') or switches.has_key('--collect'):
      collect = True
   else:
      collect = False

   if switches.has_key('-s') or switches.has_key('--stage'):
      stage = True
   else:
      stage = False

   if switches.has_key('-t') or switches.has_key('--store'):
      store = True
   else:
      store = False

   if switches.has_key('-p') or switches.has_key('--purge'):
      purge = True
   else:
      purge = False

   if switches.has_key('-a') or switches.has_key('--all'):
      collect = True
      stage = True
      store = True
      purge = True

   if switches.has_key('-r') or switches.has_key('--rebuild'):
      rebuild = True
      collect = False
      stage = False
      store = False
      purge = False
   else:
      rebuild = False

   # At least one action must be specified, unless they're just asking to validate configuration
   if not (collect or stage or store or purge or rebuild or valid):
      usage("No action specified")
      sys.exit(2)


   #############
   # Mark start
   #############

   mark_start(logfile, user, group, mode, opts)


   #####################
   # Read configuration
   #####################

   # Read the configuration
   results = { }
   results = CedarBackup.config.read_config(config_file, collect, stage, store, purge, rebuild)
   config = results['config']

   # Handle results (this could potentially exit the script, if an error is indicated)
   handle_results(None, "Error(s) were indicated while reading configuration.",
                  verbose, logfile, user, group, mode, None, results['errors'], results['warnings'])

   # Get out if all we're supposed to do is validate configuration
   if valid:
      print "Configuration is valid."
      mark_stop(logfile, user, group, mode, 0, "This was a validation run only.")


   ####################################
   # Take collect action, if indicated
   ####################################

   if collect:
      # Run the collect function
      results = { }
      results = CedarBackup.functional.run_collect(config, full)

      # Handle results (this could potentially exit the script, if an error is indicated)
      handle_results("Completed collect action succcessfully.",
                     "Error(s) were indicated while taking collect action.",
                     verbose, logfile, user, group, mode, results['output'], results['errors'], results['warnings'])


   ##################################
   # Take stage action, if indicated
   ##################################

   if stage:
      # Run the stage function
      results = { }
      results = CedarBackup.functional.run_stage(config)

      # Handle results (this could potentially exit the script, if an error is indicated)
      handle_results("Completed stage action successfully.",
                     "Error(s) were indicated while taking stage action.", 
                     verbose, logfile, user, group, mode, results['output'], results['errors'], results['warnings'])


   ##################################
   # Take store action, if indicated
   ##################################

   if store:
      # Run the store function
      results = { }
      results = CedarBackup.functional.run_store(config, full)

      # Handle results (this could potentially exit the script, if an error is indicated)
      handle_results("Completed store action successfully.",
                     "Error(s) were indicated while taking store action.",
                     verbose, logfile, user, group, mode, results['output'], results['errors'], results['warnings'])


   ##################################
   # Take purge action, if indicated
   ##################################

   if purge:
      # Run the purge function
      results = { }
      results = CedarBackup.functional.run_purge(config)

      # Handle results (this could potentially exit the script, if an error is indicated)
      handle_results("Completed purge action successfully.",
                     "Error(s) were indicated while taking the purge action",
                     verbose, logfile, user, group, mode, results['output'], results['errors'], results['warnings'])


   ####################################
   # Take rebuild action, if indicated
   ####################################

   if rebuild:
      # Run the rebuild function
      results = { }
      results = CedarBackup.functional.run_rebuild(config)

      # Handle results (this could potentially exit the script, if an error is indicated)
      handle_results("Completed rebuild action successfully.",
                     "Error(s) were indicated while taking the rebuild action",
                     verbose, logfile, user, group, mode, results['output'], results['errors'], results['warnings'])


   ################
   # Exit normally
   ################

   mark_stop(logfile, user, group, mode, 0)
   sys.exit(0)


########################################################################
# Module entry point
########################################################################

if __name__ == '__main__':
   main()


