#
#

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


"""Inter-node RPC library.

"""

# pylint: disable=C0103,R0201,R0904
# C0103: Invalid name, since call_ are not valid
# R0201: Method could be a function, we keep all rpcs instance methods
# as not to change them back and forth between static/instance methods
# if they need to start using instance attributes
# R0904: Too many public methods

import base64
import copy
import logging
import os
import threading
import zlib

import pycurl

from ganeti import utils
from ganeti import objects
from ganeti import http
from ganeti import serializer
from ganeti import constants
from ganeti import errors
from ganeti import netutils
from ganeti import ssconf
from ganeti import runtime
from ganeti import compat
from ganeti import rpc_defs
from ganeti import pathutils
from ganeti import vcluster

# Special module generated at build time
from ganeti import _generated_rpc

# pylint has a bug here, doesn't see this import
import ganeti.http.client  # pylint: disable=W0611


_RPC_CLIENT_HEADERS = [
  "Content-type: %s" % http.HTTP_APP_JSON,
  "Expect:",
  ]

#: Special value to describe an offline host
_OFFLINE = object()


def Init():
  """Initializes the module-global HTTP client manager.

  Must be called before using any RPC function and while exactly one thread is
  running.

  """
  # curl_global_init(3) and curl_global_cleanup(3) must be called with only
  # one thread running. This check is just a safety measure -- it doesn't
  # cover all cases.
  assert threading.activeCount() == 1, \
         "Found more than one active thread when initializing pycURL"

  logging.info("Using PycURL %s", pycurl.version)

  pycurl.global_init(pycurl.GLOBAL_ALL)


def Shutdown():
  """Stops the module-global HTTP client manager.

  Must be called before quitting the program and while exactly one thread is
  running.

  """
  pycurl.global_cleanup()


def _ConfigRpcCurl(curl):
  noded_cert = pathutils.NODED_CERT_FILE
  noded_client_cert = pathutils.NODED_CLIENT_CERT_FILE

  # This fallback is required for backwards compatibility with 2.10. Ganeti
  # 2.11 introduced per-node client certificates, but when we restart after
  # an upgrade from 2.10, the client certs are not in place yet, and we need
  # to fall back to using the cluster-wide server cert.
  if not os.path.exists(noded_client_cert):
    logging.warn("Using server certificate as client certificate for RPC"
                 "call.")
    noded_client_cert = noded_cert

  curl.setopt(pycurl.FOLLOWLOCATION, False)
  curl.setopt(pycurl.CAINFO, noded_cert)
  curl.setopt(pycurl.SSL_VERIFYHOST, 0)
  curl.setopt(pycurl.SSL_VERIFYPEER, True)
  curl.setopt(pycurl.SSLCERTTYPE, "PEM")
  curl.setopt(pycurl.SSLCERT, noded_client_cert)
  curl.setopt(pycurl.SSLKEYTYPE, "PEM")
  curl.setopt(pycurl.SSLKEY, noded_client_cert)
  curl.setopt(pycurl.CONNECTTIMEOUT, constants.RPC_CONNECT_TIMEOUT)


def RunWithRPC(fn):
  """RPC-wrapper decorator.

  When applied to a function, it runs it with the RPC system
  initialized, and it shutsdown the system afterwards. This means the
  function must be called without RPC being initialized.

  """
  def wrapper(*args, **kwargs):
    Init()
    try:
      return fn(*args, **kwargs)
    finally:
      Shutdown()
  return wrapper


def _Compress(_, data):
  """Compresses a string for transport over RPC.

  Small amounts of data are not compressed.

  @type data: str
  @param data: Data
  @rtype: tuple
  @return: Encoded data to send

  """
  # Small amounts of data are not compressed
  if len(data) < 512:
    return (constants.RPC_ENCODING_NONE, data)

  # Compress with zlib and encode in base64
  return (constants.RPC_ENCODING_ZLIB_BASE64,
          base64.b64encode(zlib.compress(data, 3)))


class RpcResult(object):
  """RPC Result class.

  This class holds an RPC result. It is needed since in multi-node
  calls we can't raise an exception just because one out of many
  failed, and therefore we use this class to encapsulate the result.

  @ivar data: the data payload, for successful results, or None
  @ivar call: the name of the RPC call
  @ivar node: the name of the node to which we made the call
  @ivar offline: whether the operation failed because the node was
      offline, as opposed to actual failure; offline=True will always
      imply failed=True, in order to allow simpler checking if
      the user doesn't care about the exact failure mode
  @ivar fail_msg: the error message if the call failed

  """
  def __init__(self, data=None, failed=False, offline=False,
               call=None, node=None):
    self.offline = offline
    self.call = call
    self.node = node

    if offline:
      self.fail_msg = "Node is marked offline"
      self.data = self.payload = None
    elif failed:
      self.fail_msg = self._EnsureErr(data)
      self.data = self.payload = None
    else:
      self.data = data
      if not isinstance(self.data, (tuple, list)):
        self.fail_msg = ("RPC layer error: invalid result type (%s)" %
                         type(self.data))
        self.payload = None
      elif len(data) != 2:
        self.fail_msg = ("RPC layer error: invalid result length (%d), "
                         "expected 2" % len(self.data))
        self.payload = None
      elif not self.data[0]:
        self.fail_msg = self._EnsureErr(self.data[1])
        self.payload = None
      else:
        # finally success
        self.fail_msg = None
        self.payload = data[1]

    for attr_name in ["call", "data", "fail_msg",
                      "node", "offline", "payload"]:
      assert hasattr(self, attr_name), "Missing attribute %s" % attr_name

  def __repr__(self):
    return ("RpcResult(data=%s, call=%s, node=%s, offline=%s, fail_msg=%s)" %
            (self.offline, self.call, self.node, self.offline, self.fail_msg))

  @staticmethod
  def _EnsureErr(val):
    """Helper to ensure we return a 'True' value for error."""
    if val:
      return val
    else:
      return "No error information"

  def Raise(self, msg, prereq=False, ecode=None):
    """If the result has failed, raise an OpExecError.

    This is used so that LU code doesn't have to check for each
    result, but instead can call this function.

    """
    if not self.fail_msg:
      return

    if not msg: # one could pass None for default message
      msg = ("Call '%s' to node '%s' has failed: %s" %
             (self.call, self.node, self.fail_msg))
    else:
      msg = "%s: %s" % (msg, self.fail_msg)
    if prereq:
      ec = errors.OpPrereqError
    else:
      ec = errors.OpExecError
    if ecode is not None:
      args = (msg, ecode)
    else:
      args = (msg, )
    raise ec(*args)

  def Warn(self, msg, feedback_fn):
    """If the result has failed, call the feedback_fn.

    This is used to in cases were LU wants to warn the
    user about a failure, but continue anyway.

    """
    if not self.fail_msg:
      return

    msg = "%s: %s" % (msg, self.fail_msg)
    feedback_fn(msg)


def _SsconfResolver(ssconf_ips, node_list, _,
                    ssc=ssconf.SimpleStore,
                    nslookup_fn=netutils.Hostname.GetIP):
  """Return addresses for given node names.

  @type ssconf_ips: bool
  @param ssconf_ips: Use the ssconf IPs
  @type node_list: list
  @param node_list: List of node names
  @type ssc: class
  @param ssc: SimpleStore class that is used to obtain node->ip mappings
  @type nslookup_fn: callable
  @param nslookup_fn: function use to do NS lookup
  @rtype: list of tuple; (string, string)
  @return: List of tuples containing node name and IP address

  """
  ss = ssc()
  family = ss.GetPrimaryIPFamily()

  if ssconf_ips:
    iplist = ss.GetNodePrimaryIPList()
    ipmap = dict(entry.split() for entry in iplist)
  else:
    ipmap = {}

  result = []
  for node in node_list:
    ip = ipmap.get(node)
    if ip is None:
      ip = nslookup_fn(node, family=family)
    result.append((node, ip, node))

  return result


class _StaticResolver(object):
  def __init__(self, addresses):
    """Initializes this class.

    """
    self._addresses = addresses

  def __call__(self, hosts, _):
    """Returns static addresses for hosts.

    """
    assert len(hosts) == len(self._addresses)
    return list(zip(hosts, self._addresses, hosts))


def _CheckConfigNode(node_uuid_or_name, node, accept_offline_node):
  """Checks if a node is online.

  @type node_uuid_or_name: string
  @param node_uuid_or_name: Node UUID
  @type node: L{objects.Node} or None
  @param node: Node object

  """
  if node is None:
    # Assume that the passed parameter was actually a node name, so depend on
    # DNS for name resolution
    return (node_uuid_or_name, node_uuid_or_name, node_uuid_or_name)
  else:
    if node.offline and not accept_offline_node:
      ip = _OFFLINE
    else:
      ip = node.primary_ip
    return (node.name, ip, node_uuid_or_name)


def _NodeConfigResolver(single_node_fn, all_nodes_fn, node_uuids, opts):
  """Calculate node addresses using configuration.

  Note that strings in node_uuids are treated as node names if the UUID is not
  found in the configuration.

  """
  accept_offline_node = (opts is rpc_defs.ACCEPT_OFFLINE_NODE)

  assert accept_offline_node or opts is None, "Unknown option"

  # Special case for single-host lookups
  if len(node_uuids) == 1:
    (uuid, ) = node_uuids
    return [_CheckConfigNode(uuid, single_node_fn(uuid), accept_offline_node)]
  else:
    all_nodes = all_nodes_fn()
    return [_CheckConfigNode(uuid, all_nodes.get(uuid, None),
                             accept_offline_node)
            for uuid in node_uuids]


class _RpcProcessor(object):
  def __init__(self, resolver, port, lock_monitor_cb=None):
    """Initializes this class.

    @param resolver: callable accepting a list of node UUIDs or hostnames,
      returning a list of tuples containing name, IP address and original name
      of the resolved node. IP address can be the name or the special value
      L{_OFFLINE} to mark offline machines.
    @type port: int
    @param port: TCP port
    @param lock_monitor_cb: Callable for registering with lock monitor

    """
    self._resolver = resolver
    self._port = port
    self._lock_monitor_cb = lock_monitor_cb

  @staticmethod
  def _PrepareRequests(hosts, port, procedure, body, read_timeout):
    """Prepares requests by sorting offline hosts into separate list.

    @type body: dict
    @param body: a dictionary with per-host body data

    """
    results = {}
    requests = {}

    assert isinstance(body, dict)
    assert len(body) == len(hosts)
    assert compat.all(isinstance(v, (str, bytes)) for v in body.values())
    assert frozenset(h[2] for h in hosts) == frozenset(body), \
        "%s != %s" % (hosts, list(body))

    for (name, ip, original_name) in hosts:
      if ip is _OFFLINE:
        # Node is marked as offline
        results[original_name] = RpcResult(node=name,
                                           offline=True,
                                           call=procedure)
      else:
        requests[original_name] = \
          http.client.HttpClientRequest(str(ip), port,
                                        http.HTTP_POST, str("/%s" % procedure),
                                        headers=_RPC_CLIENT_HEADERS,
                                        post_data=body[original_name],
                                        read_timeout=read_timeout,
                                        nicename="%s/%s" % (name, procedure),
                                        curl_config_fn=_ConfigRpcCurl)

    return (results, requests)

  @staticmethod
  def _CombineResults(results, requests, procedure):
    """Combines pre-computed results for offline hosts with actual call results.

    """
    for name, req in requests.items():
      if req.success and req.resp_status_code == http.HTTP_OK:
        host_result = RpcResult(data=serializer.LoadJson(req.resp_body),
                                node=name, call=procedure)
      else:
        # TODO: Better error reporting
        if req.error:
          msg = req.error
        else:
          msg = req.resp_body

        logging.error("RPC error in %s on node %s: %s", procedure, name, msg)
        host_result = RpcResult(data=msg, failed=True, node=name,
                                call=procedure)

      results[name] = host_result

    return results

  def __call__(self, nodes, procedure, body, read_timeout, resolver_opts,
               _req_process_fn=None):
    """Makes an RPC request to a number of nodes.

    @type nodes: sequence
    @param nodes: node UUIDs or Hostnames
    @type procedure: string
    @param procedure: Request path
    @type body: dictionary
    @param body: dictionary with request bodies per host
    @type read_timeout: int or None
    @param read_timeout: Read timeout for request
    @rtype: dictionary
    @return: a dictionary mapping host names to rpc.RpcResult objects

    """
    assert read_timeout is not None, \
      "Missing RPC read timeout for procedure '%s'" % procedure

    if _req_process_fn is None:
      _req_process_fn = http.client.ProcessRequests

    (results, requests) = \
      self._PrepareRequests(self._resolver(nodes, resolver_opts), self._port,
                            procedure, body, read_timeout)

    _req_process_fn(list(requests.values()),
                    lock_monitor_cb=self._lock_monitor_cb)

    assert not frozenset(results).intersection(requests)

    return self._CombineResults(results, requests, procedure)


class _RpcClientBase(object):
  def __init__(self, resolver, encoder_fn, lock_monitor_cb=None,
               _req_process_fn=None):
    """Initializes this class.

    """
    proc = _RpcProcessor(resolver,
                         netutils.GetDaemonPort(constants.NODED),
                         lock_monitor_cb=lock_monitor_cb)
    self._proc = compat.partial(proc, _req_process_fn=_req_process_fn)
    self._encoder = compat.partial(self._EncodeArg, encoder_fn)

  @staticmethod
  def _EncodeArg(encoder_fn, node, arg):
    """Encode argument.

    """
    (argkind, value) = arg
    if argkind is None:
      return value
    else:
      return encoder_fn(argkind)(node, value)

  def _Call(self, cdef, node_list, args):
    """Entry point for automatically generated RPC wrappers.

    """
    (procedure, _, resolver_opts, timeout, argdefs,
     prep_fn, postproc_fn, _) = cdef

    if callable(timeout):
      read_timeout = timeout(args)
    else:
      read_timeout = timeout

    if callable(resolver_opts):
      req_resolver_opts = resolver_opts(args)
    else:
      req_resolver_opts = resolver_opts

    if len(args) != len(argdefs):
      raise errors.ProgrammerError("Number of passed arguments doesn't match")

    if prep_fn is None:
      prep_fn = lambda _, args: args
    assert callable(prep_fn)

    # encode the arguments for each node individually, pass them and the node
    # name to the prep_fn, and serialise its return value
    encode_args_fn = lambda node: [self._encoder(node, (argdef[1], val)) for
                                      (argdef, val) in zip(argdefs, args)]
    pnbody = dict(
      (n,
       serializer.DumpJson(prep_fn(n, encode_args_fn(n)),
                           private_encoder=serializer.EncodeWithPrivateFields))
      for n in node_list
    )

    result = self._proc(node_list, procedure, pnbody, read_timeout,
                        req_resolver_opts)

    if postproc_fn:
      return dict((k, postproc_fn(v)) for (k, v) in result.items())
    else:
      return result


def _ObjectToDict(_, value):
  """Converts an object to a dictionary.

  @note: See L{objects}.

  """
  return value.ToDict()


def _ObjectListToDict(node, value):
  """Converts a list of L{objects} to dictionaries.

  """
  return [_ObjectToDict(node, v) for v in value]


def _PrepareFileUpload(getents_fn, node, filename):
  """Loads a file and prepares it for an upload to nodes.

  """
  statcb = utils.FileStatHelper()
  data = _Compress(node, utils.ReadBinaryFile(filename, preread=statcb))
  st = statcb.st

  if getents_fn is None:
    getents_fn = runtime.GetEnts

  getents = getents_fn()

  virt_filename = vcluster.MakeVirtualPath(filename)

  return [virt_filename, data, st.st_mode, getents.LookupUid(st.st_uid),
          getents.LookupGid(st.st_gid), st.st_atime, st.st_mtime]


def _PrepareFinalizeExportDisks(_, snap_disks):
  """Encodes disks for finalizing export.

  """
  flat_disks = []

  for disk in snap_disks:
    if isinstance(disk, bool):
      flat_disks.append(disk)
    else:
      flat_disks.append(disk.ToDict())

  return flat_disks


def _EncodeBlockdevRename(_, value):
  """Encodes information for renaming block devices.

  """
  return [(d.ToDict(), uid) for d, uid in value]


def _AddSpindlesToLegacyNodeInfo(result, space_info):
  """Extracts the spindle information from the space info and adds
  it to the result dictionary.

  @type result: dict of strings
  @param result: dictionary holding the result of the legacy node info
  @type space_info: list of dicts of strings
  @param space_info: list, each row holding space information of one storage
    unit
  @rtype: None
  @return: does not return anything, manipulates the C{result} variable

  """
  lvm_pv_info = utils.storage.LookupSpaceInfoByStorageType(
      space_info, constants.ST_LVM_PV)
  if lvm_pv_info:
    result["spindles_free"] = lvm_pv_info["storage_free"]
    result["spindles_total"] = lvm_pv_info["storage_size"]
  else:
    result["spindles_free"] = 0
    result["spindles_total"] = 0


def _AddStorageInfoToLegacyNodeInfoByTemplate(
    result, space_info, disk_template):
  """Extracts the storage space information of the disk template from
  the space info and adds it to the result dictionary.

  @see: C{_AddSpindlesToLegacyNodeInfo} for parameter information.

  """
  if utils.storage.DiskTemplateSupportsSpaceReporting(disk_template):
    disk_info = utils.storage.LookupSpaceInfoByDiskTemplate(
        space_info, disk_template)
    result["name"] = disk_info["name"]
    result["storage_free"] = disk_info["storage_free"]
    result["storage_size"] = disk_info["storage_size"]
  else:
    # FIXME: consider displaying '-' in this case
    result["storage_free"] = 0
    result["storage_size"] = 0


def MakeLegacyNodeInfo(data, disk_template):
  """Formats the data returned by call_node_info.

  Converts the data into a single dictionary. This is fine for most use cases,
  but some require information from more than one volume group or hypervisor.

  """
  (bootid, space_info, (hv_info, )) = data

  ret = utils.JoinDisjointDicts(hv_info, {"bootid": bootid})

  _AddSpindlesToLegacyNodeInfo(ret, space_info)
  _AddStorageInfoToLegacyNodeInfoByTemplate(ret, space_info, disk_template)

  return ret


def _AnnotateDParamsDRBD(disk, params):
  """Annotates just DRBD disks layouts.

  """
  (drbd_params, data_params, meta_params) = params
  assert disk.dev_type == constants.DT_DRBD8

  disk.params = objects.FillDict(drbd_params, disk.params)
  (dev_data, dev_meta) = disk.children
  dev_data.params = objects.FillDict(data_params, dev_data.params)
  dev_meta.params = objects.FillDict(meta_params, dev_meta.params)

  return disk


def _AnnotateDParamsGeneric(disk, params):
  """Generic disk parameter annotation routine.

  """
  assert disk.dev_type != constants.DT_DRBD8

  disk.params = objects.FillDict(params[0], disk.params)

  return disk


def AnnotateDiskParams(disks, disk_params):
  """Annotates the disk objects with the disk parameters.

  @param disks: The list of disks objects to annotate
  @param disk_params: The disk parameters for annotation
  @returns: A list of disk objects annotated

  """
  def AnnotateDisk(disk):
    if disk.dev_type == constants.DT_DISKLESS:
      return disk

    ld_params = objects.Disk.ComputeLDParams(disk.dev_type, disk_params)

    if disk.dev_type == constants.DT_DRBD8:
      return _AnnotateDParamsDRBD(disk, ld_params)
    else:
      return _AnnotateDParamsGeneric(disk, ld_params)

  return [AnnotateDisk(disk.Copy()) for disk in disks]


def _GetExclusiveStorageFlag(cfg, node_uuid):
  ni = cfg.GetNodeInfo(node_uuid)
  if ni is None:
    raise errors.OpPrereqError("Invalid node name %s" % node_uuid,
                               errors.ECODE_NOENT)
  return cfg.GetNdParams(ni)[constants.ND_EXCLUSIVE_STORAGE]


def _AddExclusiveStorageFlagToLvmStorageUnits(storage_units, es_flag):
  """Adds the exclusive storage flag to lvm units.

  This function creates a copy of the storage_units lists, with the
  es_flag being added to all lvm storage units.

  @type storage_units: list of pairs (string, string)
  @param storage_units: list of 'raw' storage units, consisting only of
    (storage_type, storage_key)
  @type es_flag: boolean
  @param es_flag: exclusive storage flag
  @rtype: list of tuples (string, string, list)
  @return: list of storage units (storage_type, storage_key, params) with
    the params containing the es_flag for lvm-vg storage units

  """
  result = []
  for (storage_type, storage_key) in storage_units:
    if storage_type in [constants.ST_LVM_VG]:
      result.append((storage_type, storage_key, [es_flag]))
      if es_flag:
        result.append((constants.ST_LVM_PV, storage_key, [es_flag]))
    else:
      result.append((storage_type, storage_key, []))
  return result


def GetExclusiveStorageForNodes(cfg, node_uuids):
  """Return the exclusive storage flag for all the given nodes.

  @type cfg: L{config.ConfigWriter}
  @param cfg: cluster configuration
  @type node_uuids: list or tuple
  @param node_uuids: node UUIDs for which to read the flag
  @rtype: dict
  @return: mapping from node uuids to exclusive storage flags
  @raise errors.OpPrereqError: if any given node name has no corresponding
  node

  """
  getflag = lambda n: _GetExclusiveStorageFlag(cfg, n)
  flags = map(getflag, node_uuids)
  return dict(zip(node_uuids, flags))


def PrepareStorageUnitsForNodes(cfg, storage_units, node_uuids):
  """Return the lvm storage unit for all the given nodes.

  Main purpose of this function is to map the exclusive storage flag, which
  can be different for each node, to the default LVM storage unit.

  @type cfg: L{config.ConfigWriter}
  @param cfg: cluster configuration
  @type storage_units: list of pairs (string, string)
  @param storage_units: list of 'raw' storage units, e.g. pairs of
    (storage_type, storage_key)
  @type node_uuids: list or tuple
  @param node_uuids: node UUIDs for which to read the flag
  @rtype: dict
  @return: mapping from node uuids to a list of storage units which include
    the exclusive storage flag for lvm storage
  @raise errors.OpPrereqError: if any given node name has no corresponding
  node

  """
  getunit = lambda n: _AddExclusiveStorageFlagToLvmStorageUnits(
      storage_units, _GetExclusiveStorageFlag(cfg, n))
  flags = map(getunit, node_uuids)
  return dict(zip(node_uuids, flags))


#: Generic encoders
_ENCODERS = {
  rpc_defs.ED_OBJECT_DICT: _ObjectToDict,
  rpc_defs.ED_OBJECT_DICT_LIST: _ObjectListToDict,
  rpc_defs.ED_COMPRESS: _Compress,
  rpc_defs.ED_FINALIZE_EXPORT_DISKS: _PrepareFinalizeExportDisks,
  rpc_defs.ED_BLOCKDEV_RENAME: _EncodeBlockdevRename,
  }


class RpcRunner(_RpcClientBase,
                _generated_rpc.RpcClientDefault,
                _generated_rpc.RpcClientBootstrap,
                _generated_rpc.RpcClientDnsOnly,
                _generated_rpc.RpcClientConfig):
  """RPC runner class.

  """
  def __init__(self, cfg, lock_monitor_cb, _req_process_fn=None, _getents=None):
    """Initialized the RPC runner.

    @type cfg: L{config.ConfigWriter}
    @param cfg: Configuration
    @type lock_monitor_cb: callable
    @param lock_monitor_cb: Lock monitor callback

    """
    self._cfg = cfg

    encoders = _ENCODERS.copy()

    encoders.update({
      # Encoders requiring configuration object
      rpc_defs.ED_INST_DICT: self._InstDict,
      rpc_defs.ED_INST_DICT_HVP_BEP_DP: self._InstDictHvpBepDp,
      rpc_defs.ED_INST_DICT_OSP_DP: self._InstDictOspDp,
      rpc_defs.ED_NIC_DICT: self._NicDict,
      rpc_defs.ED_DEVICE_DICT: self._DeviceDict,

      # Encoders annotating disk parameters
      rpc_defs.ED_DISKS_DICT_DP: self._DisksDictDP,
      rpc_defs.ED_MULTI_DISKS_DICT_DP: self._MultiDiskDictDP,
      rpc_defs.ED_SINGLE_DISK_DICT_DP: self._SingleDiskDictDP,
      rpc_defs.ED_NODE_TO_DISK_DICT_DP: self._EncodeNodeToDiskDictDP,

      # Encoders with special requirements
      rpc_defs.ED_FILE_DETAILS: compat.partial(_PrepareFileUpload, _getents),

      rpc_defs.ED_IMPEXP_IO: self._EncodeImportExportIO,
      })

    # Resolver using configuration
    resolver = compat.partial(_NodeConfigResolver, cfg.GetNodeInfo,
                              cfg.GetAllNodesInfo)

    # Pylint doesn't recognize multiple inheritance properly, see
    # <http://www.logilab.org/ticket/36586> and
    # <http://www.logilab.org/ticket/35642>
    # pylint: disable=W0233
    _RpcClientBase.__init__(self, resolver, encoders.get,
                            lock_monitor_cb=lock_monitor_cb,
                            _req_process_fn=_req_process_fn)
    _generated_rpc.RpcClientConfig.__init__(self)
    _generated_rpc.RpcClientBootstrap.__init__(self)
    _generated_rpc.RpcClientDnsOnly.__init__(self)
    _generated_rpc.RpcClientDefault.__init__(self)

  def _NicDict(self, _, nic):
    """Convert the given nic to a dict and encapsulate netinfo

    """
    n = copy.deepcopy(nic)
    if n.network:
      net_uuid = self._cfg.LookupNetwork(n.network)
      if net_uuid:
        nobj = self._cfg.GetNetwork(net_uuid)
        n.netinfo = objects.Network.ToDict(nobj)
    return n.ToDict()

  def _DeviceDict(self, _, devinstance):
    (device, instance) = devinstance
    if isinstance(device, objects.NIC):
      return self._NicDict(None, device)
    elif isinstance(device, objects.Disk):
      return self._SingleDiskDictDP(None, (device, instance))

  def _InstDict(self, node, instance, hvp=None, bep=None, osp=None):
    """Convert the given instance to a dict.

    This is done via the instance's ToDict() method and additionally
    we fill the hvparams with the cluster defaults.

    @type instance: L{objects.Instance}
    @param instance: an Instance object
    @type hvp: dict or None
    @param hvp: a dictionary with overridden hypervisor parameters
    @type bep: dict or None
    @param bep: a dictionary with overridden backend parameters
    @type osp: dict or None
    @param osp: a dictionary with overridden os parameters
    @rtype: dict
    @return: the instance dict, with the hvparams filled with the
        cluster defaults

    """
    idict = instance.ToDict()
    cluster = self._cfg.GetClusterInfo()
    idict["hvparams"] = cluster.FillHV(instance)
    idict["secondary_nodes"] = \
      self._cfg.GetInstanceSecondaryNodes(instance.uuid)
    if hvp is not None:
      idict["hvparams"].update(hvp)
    idict["beparams"] = cluster.FillBE(instance)
    if bep is not None:
      idict["beparams"].update(bep)
    idict["osparams"] = cluster.SimpleFillOS(instance.os, instance.osparams)
    if osp is not None:
      idict["osparams"].update(osp)
    disks = self._cfg.GetInstanceDisks(instance.uuid)
    idict["disks_info"] = self._DisksDictDP(node, (disks, instance))
    for nic in idict["nics"]:
      nic["nicparams"] = objects.FillDict(
        cluster.nicparams[constants.PP_DEFAULT],
        nic["nicparams"])
      network = nic.get("network", None)
      if network:
        net_uuid = self._cfg.LookupNetwork(network)
        if net_uuid:
          nobj = self._cfg.GetNetwork(net_uuid)
          nic["netinfo"] = objects.Network.ToDict(nobj)
    return idict

  def _InstDictHvpBepDp(self, node, instance_params):
    """Wrapper for L{_InstDict}.

    """
    (instance, hvp, bep) = instance_params
    return self._InstDict(node, instance, hvp=hvp, bep=bep)

  def _InstDictOspDp(self, node, instance_osparams):
    """Wrapper for L{_InstDict}.

    """
    (instance, osparams) = instance_osparams
    return self._InstDict(node, instance, osp=osparams)

  def _DisksDictDP(self, node, instance_disks):
    """Wrapper for L{AnnotateDiskParams}.

    """
    (disks, instance) = instance_disks
    diskparams = self._cfg.GetInstanceDiskParams(instance)
    ret = []
    for disk in AnnotateDiskParams(disks, diskparams):
      disk_node_uuids = disk.GetNodes(instance.primary_node)
      node_ips = dict((uuid, node.secondary_ip) for (uuid, node)
                      in self._cfg.GetMultiNodeInfo(disk_node_uuids))

      disk.UpdateDynamicDiskParams(node, node_ips)

      ret.append(disk.ToDict(include_dynamic_params=True))

    return ret

  def _MultiDiskDictDP(self, node, disks_insts):
    """Wrapper for L{AnnotateDiskParams}.

    Supports a list of (disk, instance) tuples.
    """
    return [disk for disk_inst in disks_insts
            for disk in self._DisksDictDP(node, disk_inst)]

  def _SingleDiskDictDP(self, node, instance_disk):
    """Wrapper for L{AnnotateDiskParams}.

    """
    (disk, instance) = instance_disk
    anno_disk = self._DisksDictDP(node, ([disk], instance))[0]
    return anno_disk

  def _EncodeNodeToDiskDictDP(self, node, value):
    """Encode dict of node name -> list of (disk, instance) tuples as values.

    """
    return dict((name, [self._SingleDiskDictDP(node, disk) for disk in disks])
                for name, disks in value.items())

  def _EncodeImportExportIO(self, node, ieinfo):
    """Encodes import/export I/O information.

    """
    (ieio, ieioargs) = ieinfo
    if ieio == constants.IEIO_RAW_DISK:
      assert len(ieioargs) == 2
      return (ieio, (self._SingleDiskDictDP(node, ieioargs), ))

    if ieio == constants.IEIO_SCRIPT:
      assert len(ieioargs) == 2
      return (ieio, (self._SingleDiskDictDP(node, ieioargs[0]), ieioargs[1]))

    return (ieio, ieioargs)


class JobQueueRunner(_RpcClientBase, _generated_rpc.RpcClientJobQueue):
  """RPC wrappers for job queue.

  """
  def __init__(self, _context, address_list):
    """Initializes this class.

    """
    if address_list is None:
      resolver = compat.partial(_SsconfResolver, True)
    else:
      # Caller provided an address list
      resolver = _StaticResolver(address_list)

    _RpcClientBase.__init__(self, resolver, _ENCODERS.get,
                            lock_monitor_cb=lambda _: None)
    _generated_rpc.RpcClientJobQueue.__init__(self)


class BootstrapRunner(_RpcClientBase,
                      _generated_rpc.RpcClientBootstrap,
                      _generated_rpc.RpcClientDnsOnly):
  """RPC wrappers for bootstrapping.

  """
  def __init__(self):
    """Initializes this class.

    """
    # Pylint doesn't recognize multiple inheritance properly, see
    # <http://www.logilab.org/ticket/36586> and
    # <http://www.logilab.org/ticket/35642>
    # pylint: disable=W0233
    _RpcClientBase.__init__(self, compat.partial(_SsconfResolver, True),
                            _ENCODERS.get)
    _generated_rpc.RpcClientBootstrap.__init__(self)
    _generated_rpc.RpcClientDnsOnly.__init__(self)


class DnsOnlyRunner(_RpcClientBase, _generated_rpc.RpcClientDnsOnly):
  """RPC wrappers for calls using only DNS.

  """
  def __init__(self):
    """Initialize this class.

    """
    _RpcClientBase.__init__(self, compat.partial(_SsconfResolver, False),
                            _ENCODERS.get)
    _generated_rpc.RpcClientDnsOnly.__init__(self)


class ConfigRunner(_RpcClientBase, _generated_rpc.RpcClientConfig):
  """RPC wrappers for L{config}.

  """
  def __init__(self, _context, address_list, _req_process_fn=None,
               _getents=None):
    """Initializes this class.

    """
    lock_monitor_cb = None

    if address_list is None:
      resolver = compat.partial(_SsconfResolver, True)
    else:
      # Caller provided an address list
      resolver = _StaticResolver(address_list)

    encoders = _ENCODERS.copy()

    encoders.update({
      rpc_defs.ED_FILE_DETAILS: compat.partial(_PrepareFileUpload, _getents),
      })

    _RpcClientBase.__init__(self, resolver, encoders.get,
                            lock_monitor_cb=lock_monitor_cb,
                            _req_process_fn=_req_process_fn)
    _generated_rpc.RpcClientConfig.__init__(self)
