#!/usr/bin/env python

# Copyright (c) 2009, Purdue University
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 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.
# 
# Neither the name of the Purdue University nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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.

"""Tree sync tool for Roster"""


__copyright__ = 'Copyright (C) 2009, Purdue University'
__license__ = 'BSD'
__version__ = '0.17'


import sys
import shutil
import os
import tarfile
from fabric import api as fabric_api
from fabric import state as fabric_state
from fabric import network as fabric_network

from optparse import OptionParser
import ConfigParser

def findTarFile(file_list, backup_directory, temp_root_directory, tar_id):
  """Finds the Tar file with the specified ID in a directory.

  Inputs:
    file_list: list of files
    backup_directory: string of backup directory
    temp_root_directory: string of temp root directory
    tar_id: id of the roster state
  """
  error_files = []
  for file_name in file_list:
    if( not file_name.startswith('dns_tree') ):
      continue
    try:
      if( file_name.split('-')[1].split('.')[0] == tar_id ):
        tar = tarfile.open('%s/%s' % (
            backup_directory, file_name))
        try:
          tar.extractall()
        finally:
          tar.close()
        root_directory = '%s_%s' % (temp_root_directory, tar_id)
        if( root_directory.startswith('./') ):
          root_directory = '%s/%s' % (os.getcwd(),
                                      root_directory.lstrip('./'))
        shutil.move(temp_root_directory, root_directory)
        break
    except IndexError:
      error_files.append(file_name)
      continue
      # If this error occurs, skip if correct numbered file is found.
  else:
    # No break, could mean wrong filenames or nonexistant
    if( error_files ):
      print 'ERROR: Filenames: %s not found.' % ', '.join(error_files)
    else:
      print 'ERROR: ID %s not found.' % tar_id
    sys.exit(1)
  return root_directory

def findNewestTar(file_list, backup_directory, temp_root_directory, tar_id):
  """Finds the Tar file with the latest ID in a directory.

  Inputs:
    file_list: list of files
    backup_directory: string of backup directory
    temp_root_directory: string of temp root directory
    tar_id: id of the roster state
  """
  error_files = []
  newest_id = 0
  new_file_name = None
  for file_name in file_list:
    if( not file_name.startswith('dns_tree') ):
      continue
    try:
      tar_id = int(file_name.split('-')[1].split('.')[0])
    except IndexError:
      error_files.append(file_name)
      break
      # If this error occurs, error out completely
    if( tar_id > newest_id ):
      newest_id = tar_id
      new_file_name = file_name
  if( error_files ):
    print 'ERROR: Filenames: %s not named correctly' % ', '.join(error_files)
    sys.exit(1)
  if( new_file_name is None ):
    print ('ERROR: An unknown error has occurred trying to find the newest '
           'file.')
  tar = tarfile.open('%s/%s' % (backup_directory, new_file_name))
  try:
    tar.extractall()
  finally:
    tar.close()
  root_directory = '%s_%s' % (temp_root_directory, newest_id)
  shutil.move(temp_root_directory, root_directory)
  return root_directory

def pushFiles(ssh_port, ssh_id, user_name, destination_directory, rndc_exec,
              temp_root_directory, root_directory):
  """Pushes files to a remote directory over SSH, starts BIND, and
     refreshes rndc.

  Inputs:
    ssh_port: port to use over SSH
    ssh_id: id file to use over SSH
    user_name: the username to use over SSH
    destination_directory: remote directory to push files to
    rndc_exec: path to rndc
    temp_root_directory: temporary root directory
    root_directory: local directory
  """
  try:
    dns_server_sets = None
    file_dict = {}
    for server_set in os.listdir(root_directory):
      config_parser = ConfigParser.SafeConfigParser()
      config_file_handle = config_parser.read('%s/%s/named/%s' % (
          root_directory, server_set,
          server_set.replace('servers', 'config')))
      for dns_server in config_parser.get(
            'dns_server_set_parameters', 'dns_servers').split(','):
        rsync_dir = '%s/%s/*' % (os.path.expanduser(root_directory), server_set)
        rsync_url_parts = []
        if( user_name ):
          rsync_url_parts.append('%s@' % user_name)
        rsync_url_parts.append(dns_server)
        if( ssh_port ):
          rsync_url_parts.append(':%s' % ssh_port)
        rsync_url_string = ''.join(rsync_url_parts)

        print 'Connecting to "%s"' % dns_server
        fabric_api.env.warn_only = True
        if( ssh_id ):
          fabric_api.env.key_filename = ssh_id
        fabric_state.output['running'] = False
        fabric_state.output['warnings'] = False
        fabric_api.env.host_string = rsync_url_string
        fabric_api.put(rsync_dir, destination_directory)

        out = fabric_api.run('%s reload' % (rndc_exec))
  finally:
    fabric_network.disconnect_all()
    if( os.path.exists(root_directory) ):
      shutil.rmtree(root_directory)
    if( os.path.exists(temp_root_directory) ):
      shutil.rmtree(temp_root_directory)


def main(args):
  """Collects command line arguments. Exports tree.

  Inputs:
    args: list of arguments from the command line
  """
  usage = ('\n'
           '\n'
           'To sync bind trees:\n'
           '\t%s -i <audit-id> [-c <config-file>] [-d <dest-directory>]\n'
           '\t[-u <rsync-user>] [-p <rsync-port>]\n' % sys.argv[0])

  parser = OptionParser(version='%%prog (Roster %s)' % __version__, usage=usage)

  parser.add_option('-c', '--config-file', action='store', dest='config_file',
                    help='Config File Location', metavar='<config-file>',
                    default='/etc/roster/roster_server.conf')
  parser.add_option('-d', '--destination-directory',
                    dest='destination_directory',
                    help='Destination directory on dns servers.',
                    metavar='<destination-directory>',
                    default=None)
  parser.add_option('-i', '--id', dest='id',
                    help='ID of tarfile output from Roster tree export.',
                    metavar='<id>', default=None)
  parser.add_option('-u', '--user-name', action='store', dest='user_name',
                    help='Rsync username.', metavar='<user-name>',
                    default=None)
  parser.add_option('--ssh-port', action='store', dest='ssh_port',
                    help='SSH port number.', metavar='<ssh-port>',
                    default=None)
  parser.add_option('--ssh-id', action='store', dest='ssh_id',
                    help='SSH id file.', metavar='<ssh-id>',
                    default=None)
  parser.add_option('-r', '--rsync-exec', action='store', dest='rsync_exec',
                    help='Rsync executable location.', metavar='<rsync-exec>',
                    default='rsync')
  parser.add_option('--rndc-exec', action='store', dest='rndc_exec',
                    help='Rndc executable location.', metavar='<rndc-exec>',
                    default='/usr/sbin/rndc')
  parser.add_option('--rndc-key', action='store', dest='rndc_key',
                    help='Rndc key file.', metavar='<rndc-key>',
                    default=None)
  parser.add_option('--rndc-port', action='store', dest='rndc_port',
                    help='Rndc communication port.', metavar='<rndc-port>',
                    default=None)
  parser.add_option('--rndc-conf', action='store', dest='rndc_conf',
                    help='Rndc conf file.', metavar='<rndc-conf>',
                    default=None)

  (globals()["options"], args) = parser.parse_args(args)

  
  server_config_file = ConfigParser.SafeConfigParser()

  server_config_file.read(options.config_file)


  backup_directory = os.path.abspath(os.path.expanduser(
      server_config_file.get('exporter', 'backup_dir').rstrip('/')))
  temp_root_directory = os.path.abspath(os.path.expanduser(
      server_config_file.get('exporter', 'root_config_dir').rstrip('/')))
  if( not os.path.exists(temp_root_directory) ):
    os.makedirs(temp_root_directory)
  if( not options.destination_directory ):
    options.destination_directory = os.path.abspath(os.path.expanduser(
        server_config_file.get('exporter', 'named_dir')))
  if( options.rndc_key ):
    if( options.rndc_key.startswith('./')
        or not options.rndc_key.startswith('/') ):
      options.rndc_key = '%s/%s' % (os.getcwd(), options.rndc_key.lstrip('./'))
    options.rndc_exec = '%s -k %s' % (
        options.rndc_exec, os.path.expanduser(options.rndc_key))
  if( options.rndc_conf ):
    if( options.rndc_conf.startswith('./')
        or not options.rndc_conf.startswith('/') ):
      options.rndc_conf = '%s/%s' % (os.getcwd(), options.rndc_conf.lstrip('./'))
    options.rndc_exec = '%s -c %s' % (
        options.rndc_exec, os.path.expanduser(options.rndc_conf))
  if( options.rndc_port ):
    options.rndc_exec = '%s -p %s' % (
        options.rndc_exec, options.rndc_port)

  file_list = os.listdir(backup_directory)
  if( options.id ):
    root_directory = findTarFile(file_list, backup_directory, temp_root_directory, options.id)
  else:
    root_directory = findNewestTar(file_list, backup_directory, temp_root_directory, options.id)

  pushFiles(options.ssh_port, options.ssh_id,
      options.user_name, options.destination_directory,
      options.rndc_exec, temp_root_directory, root_directory)

if __name__ == "__main__":
    main(sys.argv[1:])
