#
#

# Copyright (C) 2006, 2007, 2010, 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.


"""DRBD block device related functionality"""

import errno
import logging
import time

from ganeti import constants
from ganeti import utils
from ganeti import errors
from ganeti import netutils
from ganeti import objects
from ganeti.storage import base
from ganeti.storage.drbd_info import DRBD8Info
from ganeti.storage import drbd_info
from ganeti.stora
ge import drbd_cmdgen


# Size of reads in _CanReadDevice

_DEVICE_READ_SIZE = 128 * 1024


class DRBD8(object):
  """Various methods to deals with the DRBD system as a whole.

  This class provides a set of methods to deal with the DRBD installation on
  the node or with uninitialized devices as opposed to a DRBD device.

  """
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"

  _MAX_MINORS = 255

  @staticmethod
  def GetUsermodeHelpe
r(filename=_USERMODE_HELPER_FILE):
    """Returns DRBD usermode_helper currently set.

    @type filename: string
    @param filename: the filename to read the usermode helper from
    @rtype: string
    @return: the currently configured DRBD usermode helper

    """
    try:
      helper = utils.ReadFile(filename).splitlines()[0]
    except EnvironmentError, err:
      if err.errno == errno.ENOENT:
        base.ThrowError("The file %s cannot be opened, check if the module"
                        " is loaded (%s)", filename, str(err))
      else:
        base.ThrowError("Can't read DRBD helper file %s: %s",
                        filename, str(err))
    if not helper:
      base.ThrowError("Can't read any data from %s", filename)
    return helper

  @staticmethod
  def GetProcInfo():
    """Reads and parses information from /proc/drbd.

    @rtype: DRBD8Info
    @return: a L{DRBD8Info} instance containing the current /proc/drbd info

    """
    return DRBD8Info.CreateFromFile()

  @staticmethod
  def GetUsedDevs():
    """Compute the list of used DRBD minors.

    @rtype: list of ints

    """
    info = DRBD8.GetProcInfo()
    return filter(lambda m: not info.GetMinorStatus(m).is_unconfigured,
                  info.GetMinors())

  @staticmethod
  def FindUnusedMinor():
    """Find an unused DRBD device.

    This is specific to 8.x as the minors are allocated dynamically,
    so non-existing numbers up to a max minor count are actually free.

    @rtype: int

    """
    highest = None
    info = DRBD8.GetProcInfo()
    for minor in info.GetMinors():
      status = info.GetMinorStatus(minor)
      if not status.is_in_use:
        return minor
      highest = max(highest, minor)

    if highest is None: # there are no minors in use at all
      return 0
    if highest >= DRBD8._MAX_MINORS:
      logging.error("Error: no free drbd minors!")
      raise errors.BlockDeviceError("Can't find a free DRBD minor")

    return highest + 1

  @staticmethod
  def GetCmdGenerator(info):
    """Creates a suitable L{BaseDRBDCmdGenerator} based on the given info.

    @type info: DRBD8Info
    @rtype: BaseDRBDCmdGenerator

    """
    version = info.GetVersion()
    if version["k_minor"] <= 3:
      return drbd_cmdgen.DRBD83CmdGenerator(version)
    else:
      return drbd_cmdgen.DRBD84CmdGenerator(version)

  @staticmethod
  def ShutdownAll(minor):
    """Deactivate the device.

    This will, of course, fail if the device is in use.

    @type minor: int
    @param minor: the minor to shut down

    """
    info = DRBD8.GetProcInfo()
    cmd_gen = DRBD8.GetCmdGenerator(info)

    cmd = cmd_gen.GenDownCmd(minor)
    result = utils.RunCmd(cmd)
    if result.failed:
      base.ThrowError("drbd%d: can't shutdown drbd device: %s",
                      minor, result.output)


class DRBD8Dev(base.BlockDev):
  """DRBD v8.x block device.

  This implements the local host part of the DRBD device, i.e. it
  doesn't do anything to the supposed peer. If you need a fully
  connected DRBD pair, you need to use this class on both hosts.

  The unique_id for the drbd device is a (pnode_uuid, snode_uuid,
  port, pnode_minor, lnode_minor, secret) tuple, and it must have
  two children: the data device and the meta_device. The meta
  device is checked for valid size and is zeroed on create.

  """
  _DRBD_MAJOR = 147

  # timeout constants
  _NET_RECONFIG_TIMEOUT = 60

  def __init__(self, unique_id, children, size, params, dyn_params, *args):
    if children and children.count(None) > 0:
      children = []
    if len(children) not in (0, 2):
      raise ValueError("Invalid configuration data %s" % str(children))
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
      raise ValueError("Invalid configuration data %s" % str(unique_id))
    if constants.DDP_LOCAL_IP not in dyn_params or \
       constants.DDP_REMOTE_IP not in dyn_params or \
       constants.DDP_LOCAL_MINOR not in dyn_params or \
       constants.DDP_REMOTE_MINOR not in dyn_params:
      raise ValueError("Invalid dynamic parameters %s" % str(dyn_params))

    self._lhost = dyn_params[constants.DDP_LOCAL_IP]
    self._lport = unique_id[2]
    self._rhost = dyn_params[constants.DDP_REMOTE_IP]
    self._rport = unique_id[2]
    self._aminor = dyn_params[constants.DDP_LOCAL_MINOR]
    self._secret = unique_id[5]

    if children:
      if not _CanReadDevice(children[1].dev_path):
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
        children = []
    super(DRBD8Dev, self).__init__(unique_id, children, size, params,
                                   dyn_params, *args)
    self.major = self._DRBD_MAJOR

    info = DRBD8.GetProcInfo()
    version = info.GetVersion()
    if version["k_major"] != 8:
      base.ThrowError("Mismatch in DRBD kernel version and requested ganeti"
                      " usage: kernel is %s.%s, ganeti wants 8.x",
                      version["k_major"], version["k_minor"])

    if version["k_minor"] <= 3:
      self._show_info_cls = drbd_info.DRBD83ShowInfo
    else:
      self._show_info_cls = drbd_info.DRBD84ShowInfo

    self._cmd_gen = DRBD8.GetCmdGenerator(info)

    if (self._lhost is not None and self._lhost == self._rhost and
            self._lport == self._rport):
      raise ValueError("Invalid configuration data, same local/remote %s, %s" %
                       (unique_id, dyn_params))
    self.Attach()

  @staticmethod
  def _DevPath(minor):
    """Return the path to a drbd device for a given minor.

    @type minor: int
    @rtype: string

    """
    return "/dev/drbd%d" % minor

  def _SetFromMinor(self, minor):
    """Set our parameters based on the given minor.

    This sets our minor variable and our dev_path.

    @type minor: int

    """
    if minor is None:
      self.minor = self.dev_path = None
      self.attached = False
    else:
      self.minor = minor
      self.dev_path = self._DevPath(minor)
      self.attached = True

  @staticmethod
  def _CheckMetaSize(meta_device):
    """Check if the given meta device looks like a valid one.

    This currently only checks the size, which must be around
    128MiB.

    @type meta_device: string
    @param meta_device: the path to the device to check

    """
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
    if result.failed:
      base.ThrowError("Failed to get device size: %s - %s",
                      result.fail_reason, result.output)
    try:
      sectors = int(result.stdout)
    except (TypeError, ValueError):
      base.ThrowError("Invalid out