#
#

# Copyright (C) 2010 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. 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.
#
# 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.

"""Module implementing configuration details at runtime.

"""


import grp
import pwd
import threading
import platform

from ganeti import constants
from ganeti import errors
from ganeti import luxi
from ganeti.rpc.errors import NoMasterError
from ganeti import pathutils
from ganeti import ssconf
from ganeti import utils


_priv = None
_priv_lock = threading.Lock()

#: Architecture information
_arch = None


def GetUid(user, _getpwnam):
  """Retrieve the uid from the database.

  @type user: string
  @param user: The username to retrieve
  @return: The resolved uid

  """
  try:
    return _getpwnam(user).pw_uid
  except KeyError as err:
    raise errors.ConfigurationError("User '%s' not found (%s)" % (user, err))


def GetGid(group, _getgrnam):
  """Retrieve the gid from the database.

  @type group: string
  @param group: The group name to retrieve
  @return: The resolved gid

  """
  try:
    return _getgrnam(group).gr_gid
  except KeyError as err:
    raise errors.ConfigurationError("Group '%s' not found (%s)" % (group, err))


class GetentResolver(object):
  """Resolves Ganeti uids and gids by name.

  @ivar masterd_uid: The resolved uid of the masterd user
  @ivar masterd_gid: The resolved gid of the masterd group
  @ivar confd_uid: The resolved uid of the confd user
  @ivar confd_gid: The resolved gid of the confd group
  @ivar wconfd_uid: The resolved uid of the wconfd user
  @ivar wconfd_gid: The resolved gid of the wconfd group
  @ivar luxid_uid: The resolved uid of the luxid user
  @ivar luxid_gid: The resolved gid of the luxid group
  @ivar rapi_uid: The resolved uid of the rapi user
  @ivar rapi_gid: The resolved gid of the rapi group
  @ivar noded_uid: The resolved uid of the noded user
  @ivar daemons_gid: The resolved gid of the daemons group
  @ivar admin_gid: The resolved gid of the admin group

  """
  def __init__(self, _getpwnam=pwd.getpwnam, _getgrnam=grp.getgrnam):
    """Initialize the resolver.

    """
    # Daemon pairs
    self.masterd_uid = GetUid(constants.MASTERD_USER, _getpwnam)
    self.masterd_gid = GetGid(constants.MASTERD_GROUP, _getgrnam)

    self.confd_uid = GetUid(constants.CONFD_USER, _getpwnam)
    self.confd_gid = GetGid(constants.CONFD_GROUP, _getgrnam)

    self.wconfd_uid = GetUid(constants.WCONFD_USER, _getpwnam)
    self.wconfd_gid = GetGid(constants.WCONFD_GROUP, _getgrnam)

    self.luxid_uid = GetUid(constants.LUXID_USER, _getpwnam)
    self.luxid_gid = GetGid(constants.LUXID_GROUP, _getgrnam)

    self.rapi_uid = GetUid(constants.RAPI_USER, _getpwnam)
    self.rapi_gid = GetGid(constants.RAPI_GROUP, _getgrnam)

    self.noded_uid = GetUid(constants.NODED_USER, _getpwnam)
    self.noded_gid = GetGid(constants.NODED_GROUP, _getgrnam)

    self.mond_uid = GetUid(constants.MOND_USER, _getpwnam)
    self.mond_gid = GetGid(constants.MOND_GROUP, _getgrnam)

    # Misc Ganeti groups
    self.daemons_gid = GetGid(constants.DAEMONS_GROUP, _getgrnam)
    self.admin_gid = GetGid(constants.ADMIN_GROUP, _getgrnam)

    self._uid2user = {
      self.masterd_uid: constants.MASTERD_USER,
      self.confd_uid: constants.CONFD_USER,
      self.wconfd_uid: constants.WCONFD_USER,
      self.luxid_uid: constants.LUXID_USER,
      self.rapi_uid: constants.RAPI_USER,
      self.noded_uid: constants.NODED_USER,
      self.mond_uid: constants.MOND_USER,
      }

    self._gid2group = {
      self.masterd_gid: constants.MASTERD_GROUP,
      self.confd_gid: constants.CONFD_GROUP,
      self.wconfd_gid: constants.WCONFD_GROUP,
      self.luxid_gid: constants.LUXID_GROUP,
      self.rapi_gid: constants.RAPI_GROUP,
      self.noded_gid: constants.NODED_GROUP,
      self.mond_gid: constants.MOND_GROUP,
      self.daemons_gid: constants.DAEMONS_GROUP,
      self.admin_gid: constants.ADMIN_GROUP,
      }

    self._user2uid = utils.InvertDict(self._uid2user)
    self._group2gid = utils.InvertDict(self._gid2group)

  def LookupUid(self, uid):
    """Looks which Ganeti user belongs to this uid.

    @param uid: The uid to lookup
    @returns The user name associated with that uid

    """
    try:
      return self._uid2user[uid]
    except KeyError:
      raise errors.ConfigurationError("Unknown Ganeti uid '%d'" % uid)

  def LookupGid(self, gid):
    """Looks which Ganeti group belongs to this gid.

    @param gid: The gid to lookup
    @returns The group name associated with that gid

    """
    try:
      return self._gid2group[gid]
    except KeyError:
      raise errors.ConfigurationError("Unknown Ganeti gid '%d'" % gid)

  def LookupUser(self, name):
    """Looks which uid belongs to this name.

    @param name: The name to lookup
    @returns The uid associated with that user name

    """
    try:
      return self._user2uid[name]
    except KeyError:
      raise errors.ConfigurationError("Unknown Ganeti user '%s'" % name)

  def LookupGroup(self, name):
    """Looks which gid belongs to this name.

    @param name: The name to lookup
    @returns The gid associated with that group name

    """
    try:
      return self._group2gid[name]
    except KeyError:
      raise errors.ConfigurationError("Unknown Ganeti group '%s'" % name)


def GetEnts(resolver=GetentResolver):
  """Singleton wrapper around resolver instance.

  As this method is accessed by multiple threads at the same time
  we need to take thread-safety carefully.

  """
  # We need to use the global keyword here
  global _priv # pylint: disable=W0603

  if not _priv:
    _priv_lock.acquire()
    try:
      if not _priv:
        # W0621: Redefine '_priv' from outer scope (used for singleton)
        _priv = resolver() # pylint: disable=W0621
    finally:
      _priv_lock.release()

  return _priv


def InitArchInfo():
  """Initialize architecture information.

  We can assume this information never changes during the lifetime of a
  process, therefore the information can easily be cached.

  @note: This function uses C{platform.architecture} to retrieve the Python
    binary architecture and does so by forking to run C{file} (see Python
    documentation for more information). Therefore it must not be used in a
    multi-threaded environment.

  """
  global _arch # pylint: disable=W0603

  if _arch is not None:
    raise errors.ProgrammerError("Architecture information can only be"
                                 " initialized once")

  _arch = (platform.architecture()[0], platform.machine())


def GetArchInfo():
  """Returns previsouly initialized architecture information.

  """
  if _arch is None:
    raise errors.ProgrammerError("Architecture information hasn't been"
                                 " initialized")

  return _arch


def GetClient():
  """Connects to the a luxi socket and returns a client.

  """
  try:
    client = luxi.Client(address=pathutils.QUERY_SOCKET)
  except NoMasterError:
    ss = ssconf.SimpleStore()

    # Try to read ssconf file
    try:
      ss.GetMasterNode()
    except errors.ConfigurationError:
      raise errors.OpPrereqError("Cluster not initialized or this machine is"
                                 " not part of a cluster",
                                 errors.ECODE_INVAL)

    master, myself = ssconf.GetMasterAndMyself(ss=ss)
    if master != myself:
      raise errors.OpPrereqError("This is not the master node, please connect"
                                 " to node '%s' and rerun the command" %
                                 master, errors.ECODE_INVAL)
    raise
  return client
