#
#

# Copyright (C) 2007, 2011, 2012, 2013 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.


"""QA configuration.

"""

import os

from ganeti import constants
from ganeti import utils
from ganeti import serializer
from ganeti import compat

import qa_error


_INSTANCE_CHECK_KEY = "instance-check"
_ENABLED_HV_KEY = "enabled-hypervisors"
# Key to store the cluster-wide run-time value of the exclusive storage flag
_EXCLUSIVE_STORAGE_KEY = "_exclusive_storage"


cfg = {}
options = None


def Load(path):
  """Loads the passed configuration file.

  """
  global cfg # pylint: disable=W0603

  cfg = serializer.LoadJson(utils.ReadFile(path))

  Validate()


def Validate():
  if len(cfg["nodes"]) < 1:
    raise qa_error.Error("Need at least one node")
  if len(cfg["instances"]) < 1:
    raise qa_error.Error("Need at least one instance")
  if len(cfg["disk"]) != len(cfg["disk-growth"]):
    raise qa_error.Error("Config options 'disk' and 'disk-growth' must have"
                         " the same number of items")

  check = GetInstanceCheckScript()
  if check:
    try:
      os.stat(check)
    except EnvironmentError, err:
      raise qa_error.Error("Can't find instance check script '%s': %s" %
                           (check, err))

  enabled_hv = frozenset(GetEnabledHypervisors())
  if not enabled_hv:
    raise qa_error.Error("No hypervisor is enabled")

  difference = enabled_hv - constants.HYPER_TYPES
  if difference:
    raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
                         utils.CommaJoin(difference))


def get(name, default=None):
  return cfg.get(name, default)


class Either:
  def __init__(self, tests):
    """Initializes this class.

    @type tests: list or string
    @param tests: List of test names
    @see: L{TestEnabled} for details

    """
    self.tests = tests


def _MakeSequence(value):
  """Make sequence of single argument.

  If the single argument is not already a list or tuple, a list with the
  argument as a single item is returned.

  """
  if isinstance(value, (list, tuple)):
    return value
  else:
    return [value]


def _TestEnabledInner(check_fn, names, fn):
  """Evaluate test conditions.

  @type check_fn: callable
  @param check_fn: Callback to check whether a test is enabled
  @type names: sequence or string
  @param names: Test name(s)
  @type fn: callable
  @param fn: Aggregation function
  @rtype: bool
  @return: Whether test is enabled

  """
  names = _MakeSequence(names)

  result = []

  for name in names:
    if isinstance(name, Either):
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
    elif isinstance(name, (list, tuple)):
      value = _TestEnabledInner(check_fn, name, compat.all)
    else:
      value = check_fn(name)

    result.append(value)

  return fn(result)


def TestEnabled(tests, _cfg=None):
  """Returns True if the given tests are enabled.

  @param tests: A single test as a string, or a list of tests to check; can
    contain L{Either} for OR conditions, AND is default

  """
  if _cfg is None:
    _cfg = cfg

  # Get settings for all tests
  cfg_tests = _cfg.get("tests", {})

  # Get default setting
  default = cfg_tests.get("default", True)

  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
                           tests, compat.all)


def GetInstanceCheckScript():
  """Returns path to instance check script or C{None}.

  """
  return cfg.get(_INSTANCE_CHECK_KEY, None)


def GetEnabledHypervisors():
  """Returns list of enabled hypervisors.

  @rtype: list

  """
  try:
    value = cfg[_ENABLED_HV_KEY]
  except KeyError:
    return [constants.DEFAULT_ENABLED_HYPERVISOR]
  else:
    if isinstance(value, basestring):
      # The configuration key ("enabled-hypervisors") implies there can be
      # multiple values. Multiple hypervisors are comma-separated on the
      # command line option to "gnt-cluster init", so we need to handle them
      # equally here.
      return value.split(",")
    else:
      return value


def GetDefaultHypervisor():
  """Returns the default hypervisor to be used.

  """
  return GetEnabledHypervisors()[0]


def GetInstanceNicMac(inst, default=None):
  """Returns MAC address for instance's network interface.

  """
  return inst.get("nic.mac/0", default)


def GetMasterNode():
  return cfg["nodes"][0]


def AcquireInstance():
  """Returns an instance which isn't in use.

  """
  # Filter out unwanted instances
  tmp_flt = lambda inst: not inst.get("_used", False)
  instances = filter(tmp_flt, cfg["instances"])
  del tmp_flt

  if len(instances) == 0:
    raise qa_error.OutOfInstancesError("No instances left")

  inst = instances[0]
  inst["_used"] = True
  inst["_template"] = None
  return inst


def ReleaseInstance(inst):
  inst["_used"] = False


def GetInstanceTemplate(inst):
  """Return the disk template of an instance.

  """
  templ = inst["_template"]
  assert templ is not None
  return templ


def SetInstanceTemplate(inst, template):
  """Set the disk template for an instance.

  """
  inst["_template"] = template


def SetExclusiveStorage(value):
  """Set the expected value of the exclusive_storage flag for the cluster.

  """
  cfg[_EXCLUSIVE_STORAGE_KEY] = bool(value)


def GetExclusiveStorage():
  """Get the expected value of the exclusive_storage flag for the cluster.

  """
  val = cfg.get(_EXCLUSIVE_STORAGE_KEY)
  assert val is not None
  return val


def IsTemplateSupported(templ):
  """Is the given templated supported by the current configuration?

  """
  if GetExclusiveStorage():
    return templ in constants.DTS_EXCL_STORAGE
  else:
    return True


def AcquireNode(exclude=None):
  """Returns the least used node.

  """
  master = GetMasterNode()

  # Filter out unwanted nodes
  # TODO: Maybe combine filters
  if exclude is None:
    nodes = cfg["nodes"][:]
  elif isinstance(exclude, (list, tuple)):
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
  else:
    nodes = filter(lambda node: node != exclude, cfg["nodes"])

  tmp_flt = lambda node: node.get("_added", False) or node == master
  nodes = filter(tmp_flt, nodes)
  del tmp_flt

  if len(nodes) == 0:
    raise qa_error.OutOfNodesError("No nodes left")

  # Get node with least number of uses
  def compare(a, b):
    result = cmp(a.get("_count", 0), b.get("_count", 0))
    if result == 0:
      result = cmp(a["primary"], b["primary"])
    return result

  nodes.sort(cmp=compare)

  node = nodes[0]
  node["_count"] = node.get("_count", 0) + 1
  return node


def AcquireManyNodes(num, exclude=None):
  """Return the least used nodes.

  @type num: int
  @param num: Number of nodes; can be 0.
  @type exclude: list of nodes or C{None}
  @param exclude: nodes to be excluded from the choice
  @rtype: list of nodes
  @return: C{num} different nodes

  """
  nodes = []
  if exclude is None:
    exclude = []
  elif isinstance(exclude, (list, tuple)):
    # Don't modify the incoming argument
    exclude = list(exclude)
  else:
    exclude = [exclude]

  try:
    for _ in range(0, num):
      n = AcquireNode(exclude=exclude)
      nodes.append(n)
      exclude.append(n)
  except qa_error.OutOfNodesError:
    ReleaseManyNodes(nodes)
    raise
  return nodes


def ReleaseNode(node):
  node["_count"] = node.get("_count", 0) - 1


def ReleaseManyNodes(nodes):
  for n in nodes:
    ReleaseNode(n)