#!/usr/bin/python
#

# Copyright (C) 2007, 2008 Google Inc.
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Tool to upgrade the configuration file.

This code handles only the types supported by simplejson. As an example, "set"
is a "list". Old Pickle based configurations files are converted to JSON during
the process.

"""


import os
import os.path
import sys
import re
import optparse
import tempfile
import simplejson

from ganeti import utils
from ganeti import cli


options = None
args = None


class Error(Exception):
  """Generic exception"""
  pass


# {{{ Support for old Pickle files
class UpgradeDict(dict):
  """Base class for internal config classes.

  """
  def __setstate__(self, state):
    self.update(state)

  def __getstate__(self):
    return self.copy()


def FindGlobal(module, name):
  """Wraps Ganeti config classes to internal ones.

  This function may only return types supported by simplejson.

  """
  if module == "ganeti.objects":
    return UpgradeDict
  elif module == "__builtin__" and name == "set":
    return list

  return getattr(sys.modules[module], name)


def ReadPickleFile(f):
  """Reads an old Pickle configuration.

  """
  import cPickle

  loader = cPickle.Unpickler(f)
  loader.find_global = FindGlobal
  return loader.load()


def IsPickleFile(f):
  """Checks whether a file is using the Pickle format.

  """
  magic = f.read(128)
  try:
    return not re.match('^\s*\{', magic)
  finally:
    f.seek(-len(magic), 1)
# }}}


def ReadJsonFile(f):
  """Reads a JSON file.

  """
  return simplejson.load(f)


def ReadConfig(path):
  """Reads configuration file.

  """
  f = open(path, 'r')
  try:
    if IsPickleFile(f):
      return ReadPickleFile(f)
    else:
      return ReadJsonFile(f)
  finally:
    f.close()


def WriteConfig(path, data):
  """Writes the configuration file.

  """
  if not options.dry_run:
    utils.CreateBackup(path)

  (fd, name) = tempfile.mkstemp(dir=os.path.dirname(path))
  f = os.fdopen(fd, 'w')
  try:
    try:
      simplejson.dump(data, f)
      f.flush()
      if options.dry_run:
        os.unlink(name)
      else:
        os.rename(name, path)
    except:
      os.unlink(name)
      raise
  finally:
    f.close()


def UpdateFromVersion2To3(cfg):
  """Updates the configuration from version 2 to 3.

  """
  if cfg['cluster']['config_version'] != 2:
    return

  # Add port pool
  if 'tcpudp_port_pool' not in cfg['cluster']:
    cfg['cluster']['tcpudp_port_pool'] = []

  # Add bridge settings
  if 'default_bridge' not in cfg['cluster']:
    cfg['cluster']['default_bridge'] = 'xen-br0'
  for inst in cfg['instances'].values():
    for nic in inst['nics']:
      if 'bridge' not in nic:
        nic['bridge'] = None

  cfg['cluster']['config_version'] = 3


# Main program
if __name__ == "__main__":
  program = os.path.basename(sys.argv[0])

  # Option parsing
  parser = optparse.OptionParser(usage="%prog [options] <config-file>")
  parser.add_option('--dry-run', dest='dry_run',
                    action="store_true",
                    help="Try to do the conversion, but don't write"
                         " output file")
  parser.add_option(cli.FORCE_OPT)
  parser.add_option('--verbose', dest='verbose',
                    action="store_true",
                    help="Verbose output")
  (options, args) = parser.parse_args()

  # Option checking
  if args:
    cfg_file = args[0]
  else:
    raise Error("Configuration file not specified")

  if not options.force:
    usertext = ("%s MUST run on the master node. Is this the master"
                " node?" % program)
    if not cli.AskUser(usertext):
      sys.exit(1)

  config = ReadConfig(cfg_file)

  if options.verbose:
    import pprint
    print "Before upgrade:"
    pprint.pprint(config)
    print

  UpdateFromVersion2To3(config)

  if options.verbose:
    print "After upgrade:"
    pprint.pprint(config)
    print

  WriteConfig(cfg_file, config)

  print "The configuration file has been updated successfully. Please run"
  print "  gnt-cluster copyfile %s" % cfg_file
  print "now."

# vim: set foldmethod=marker :