#
#

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

"""Logical unit for creating a single instance."""

import logging
import os

import OpenSSL

from ganeti import compat
from ganeti import constants
from ganeti import errors
from ganeti import hypervisor
from ganeti import locking
from ganeti.masterd import iallocator
from ganeti import masterd
from ganeti import netutils
from ganeti import objects
from ganeti import pathutils
from ganeti import utils
from ganeti import serializer

from ganeti.cmdlib.base import LogicalUnit

from ganeti.cmdlib.common import \
  CheckNodeOnline, \
  CheckParamsNotGlobal, \
  IsExclusiveStorageEnabledNode, CheckHVParams, CheckOSParams, \
  ExpandNodeUuidAndName, \
  IsValidDiskAccessModeCombination, \
  CheckDiskTemplateEnabled, CheckIAllocatorOrNode, CheckOSImage
from ganeti.cmdlib.instance_helpervm import RunWithHelperVM
from ganeti.cmdlib.instance_storage import CalculateFileStorageDir, \
  CheckNodesFreeDiskPerVG, CheckRADOSFreeSpace, CheckSpindlesExclusiveStorage, \
  ComputeDiskSizePerVG, CreateDisks, \
  GenerateDiskTemplate, CommitDisks, \
  WaitForSync, ComputeDisks, \
  ImageDisks, WipeDisks
from ganeti.cmdlib.instance_utils import \
  CheckNodeNotDrained, CopyLockList, \
  ReleaseLocks, CheckNodeVmCapable, \
  RemoveDisks, CheckNodeFreeMemory, \
  UpdateMetadata, CheckForConflictingIp, \
  ComputeInstanceCommunicationNIC, \
  ComputeIPolicyInstanceSpecViolation, \
  CheckHostnameSane, CheckOpportunisticLocking, \
  ComputeFullBeParams, ComputeNics, GetClusterDomainSecret, \
  CheckInstanceExistence, CreateInstanceAllocRequest, BuildInstanceHookEnv, \
  NICListToTuple, CheckNicsBridgesExist, CheckCompressionTool
import ganeti.masterd.instance


def _ValidateTrunkVLAN(vlan):
  if not compat.all(vl.isdigit() for vl in vlan[1:].split(':')):
    raise errors.OpPrereqError("Specified VLAN parameter is invalid"
                               " : %s" % vlan, errors.ECODE_INVAL)


class LUInstanceCreate(LogicalUnit):
  """Create an instance.

  """
  HPATH = "instance-add"
  HTYPE = constants.HTYPE_INSTANCE
  REQ_BGL = False

  def _CheckDiskTemplateValid(self):
    """Checks validity of disk template.

    """
    cluster = self.cfg.GetClusterInfo()
    if self.op.disk_template is None:
      # FIXME: It would be better to take the default disk template from the
      # ipolicy, but for the ipolicy we need the primary node, which we get from
      # the iallocator, which wants the disk template as input. To solve this
      # chicken-and-egg problem, it should be possible to specify just a node
      # group from the iallocator and take the ipolicy from that.
      self.op.disk_template = cluster.enabled_disk_templates[0]
    CheckDiskTemplateEnabled(cluster, self.op.disk_template)

  def _CheckDiskArguments(self):
    """Checks validity of disk-related arguments.

    """
    # check that disk's names are unique and valid
    utils.ValidateDeviceNames("disk", self.op.disks)

    self._CheckDiskTemplateValid()

    # check disks. parameter names and consistent adopt/no-adopt strategy
    has_adopt = has_no_adopt = False
    for disk in self.op.disks:
      if self.op.disk_template != constants.DT_EXT:
        utils.ForceDictType(disk, constants.IDISK_PARAMS_TYPES)
      if constants.IDISK_ADOPT in disk: # pylint: disable=R0102
        has_adopt = True
      else:
        has_no_adopt = True
    if has_adopt and has_no_adopt:
      raise errors.OpPrereqError("Either all disks are adopted or none is",
                                 errors.ECODE_INVAL)
    if has_adopt:
      if self.op.disk_template not in constants.DTS_MAY_ADOPT:
        raise errors.OpPrereqError("Disk adoption is not supported for the"
                                   " '%s' disk template" %
                                   self.op.disk_template,
                                   errors.ECODE_INVAL)
      if self.op.iallocator is not None:
        raise errors.OpPrereqError("Disk adoption not allowed with an"
                                   " iallocator script", errors.ECODE_INVAL)
      if self.op.mode == constants.INSTANCE_IMPORT:
        raise errors.OpPrereqError("Disk adoption not allowed for"
                                   " instance import", errors.ECODE_INVAL)
    else:
      if self.op.disk_template in constants.DTS_MUST_ADOPT:
        raise errors.OpPrereqError("Disk template %s requires disk adoption,"
                                   " but no 'adopt' parameter given" %
                                   self.op.disk_template,
                                   errors.ECODE_INVAL)

    self.adopt_disks = has_adopt

  def _CheckVLANArguments(self):
    """ Check validity of VLANs if given

    """
    for nic in self.op.nics:
      vlan = nic.get(constants.INIC_VLAN, None)
      if vlan:
        if vlan[0] == ".":
          # vlan starting with dot means single untagged vlan,
          # might be followed by trunk (:)
          if not vlan[1:].isdigit():
            _ValidateTrunkVLAN(vlan)
        elif vlan[0] == ":":
          # Trunk - tagged only
          _ValidateTrunkVLAN(vlan)
        elif vlan.isdigit():
          # This is the simplest case. No dots, only single digit
          # -> Create untagged access port, dot needs to be added
          nic[constants.INIC_VLAN] = "." + vlan
        else:
          raise errors.OpPrereqError("Specified VLAN parameter is invalid"
                                       " : %s" % vlan, errors.ECODE_INVAL)

  def CheckArguments(self):
    """Check arguments.

    """
    if self.op.forthcoming and self.op.commit:
      raise errors.OpPrereqError("Forthcoming generation and commiting are"
                                 " mutually exclusive", errors.ECODE_INVAL)

    # do not require name_check to ease forward/backward compatibility
    # for tools
    if self.op.no_install and self.op.start:
      self.LogInfo("No-installation mode selected, disabling startup")
      self.op.start = False
    # validate/normalize the instance name
    self.op.instance_name = \
      netutils.Hostname.GetNormalizedName(self.op.instance_name)

    if self.op.ip_check and not self.op.name_check:
      # TODO: make the ip check more flexible and not depend on the name check
      raise errors.OpPrereqError("Cannot do IP address check without a name"
                                 " check", errors.ECODE_INVAL)

    # instance name verification
    if self.op.name_check:
      self.hostname = CheckHostnameSane(self, self.op.instance_name)
      self.op.instance_name = self.hostname.name
      # used in CheckPrereq for ip ping check
      self.check_ip = self.hostname.ip
    else:
      self.check_ip = None

    # add NIC for instance communication
    if self.op.instance_communication:
      nic_name = ComputeInstanceCommunicationNIC(self.op.instance_name)

      for nic in self.op.nics:
        if nic.get(constants.INIC_NAME, None) == nic_name:
          break
      else:
        self.op.nics.append({constants.INIC_NAME: nic_name,
                             constants.INIC_MAC: constants.VALUE_GENERATE,
                             constants.INIC_IP: constants.NIC_IP_POOL,
                             constants.INIC_NETWORK:
                               self.cfg.GetInstanceCommunicationNetwork()})

    # timeouts for unsafe OS installs
    if self.op.helper_startup_timeout is None:
      self.op.helper_startup_timeout = constants.HELPER_VM_STARTUP

    if self.op.helper_shutdown_timeout is None:
      self.op.helper_shutdown_timeout = constants.HELPER_VM_SHUTDOWN

    # check nics' parameter names
    for nic in self.op.nics:
      utils.ForceDictType(nic, constants.INIC_PARAMS_TYPES)
    # check that NIC's parameters names are unique and valid
    utils.ValidateDeviceNames("NIC", self.op.nics)

    self._CheckVLANArguments()

    self._CheckDiskArguments()
    assert self.op.disk_template is not None

    # file storage checks
    if (self.op.file_driver and
        not self.op.file_driver in constants.FILE_DRIVER):
      raise errors.OpPrereqError("Invalid file driver name '%s'" %
                                 self.op.file_driver, errors.ECODE_INVAL)

    # set default file_driver if unset and required
    if (not self.op.file_driver and
        self.op.disk_template in constants.DTS_FILEBASED):
      self.op.file_driver = constants.FD_DEFAULT

    ### Node/iallocator related checks
    CheckIAllocatorOrNode(self, "iallocator", "pnode")

    if self.op.pnode is not None:
      if self.op.disk_template in constants.DTS_INT_MIRROR:
        if self.op.snode is None:
          raise errors.OpPrereqError("The networked disk templates need"
                                     " a mirror node", errors.ECODE_INVAL)
      elif self.op.snode:
        self.LogWarning("Secondary node will be ignored on non-mirrored disk"
                        " template")
        self.op.snode = None

    CheckOpportunisticLocking(self.op)

    if self.op.mode == constants.INSTANCE_IMPORT:
      # On import force_variant must be True, because if we forced it at
      # initial install, our only chance when importing it back is that it
      # works again!
      self.op.force_variant = True

      if self.op.no_install:
        self.LogInfo("No-installation mode has no effect during import")

      if objects.GetOSImage(self.op.osparams):
        self.LogInfo("OS image has no effect during import")
    elif self.op.mode == constants.INSTANCE_CREATE:
      os_image = CheckOSImage(self.op)

      if self.op.os_type is None and os_image is None:
        raise errors.OpPrereqError("No guest OS or OS image specified",
                                   errors.ECODE_INVAL)

      if self.op.os_type is not None \
            and self.op.os_type in self.cfg.GetClusterInfo().blacklisted_os:
        raise errors.OpPrereqError("Guest OS '%s' is not allowed for"
                                   " installation" % self.op.os_type,
                                   errors.ECODE_STATE)
    elif self.op.mode == constants.INSTANCE_REMOTE_IMPORT:
      if objects.GetOSImage(self.op.osparams):
        self.LogInfo("OS image has no effect during import")

      self._cds = GetClusterDomainSecret()

      # Check handshake to ensure both clusters have the same domain secret
      src_handshake = self.op.source_handshake
      if not src_handshake:
        raise errors.OpPrereqError("Missing source handshake",
                                   errors.ECODE_INVAL)

      errmsg = masterd.instance.CheckRemoteExportHandshake(self._cds,
                                                           src_handshake)
      if errmsg:
        raise errors.OpPrereqError("Invalid handshake: %s" % errmsg,
                                   errors.ECODE_INVAL)

      # Load and check source CA
      self.source_x509_ca_pem = self.op.source_x509_ca
      if not self.source_x509_ca_pem:
        raise errors.OpPrereqError("Missing source X509 CA",
                                   errors.ECODE_INVAL)

      try:
        (cert, _) = utils.LoadSignedX509Certificate(self.source_x509_ca_pem,
                                                    self._cds)
      except OpenSSL.crypto.Error as err:
        raise errors.OpPrereqError("Unable to load source X509 CA (%s)" %
                                   (err, ), errors.ECODE_INVAL)

      (errcode, msg) = utils.VerifyX509Certificate(cert, None, None)
      if errcode is not None:
        raise errors.OpPrereqError("Invalid source X509 CA (%s)" % (msg, ),
                                   errors.ECODE_INVAL)

      self.source_x509_ca = cert

      src_instance_name = self.op.source_instance_name
      if not src_instance_name:
        raise errors.OpPrereqError("Missing source instance name",
                                   errors.ECODE_INVAL)

      self.source_instance_name = \
        netutils.GetHostname(name=src_instance_name).name

    else:
      raise errors.OpPrereqError("Invalid instance creation mode %r" %
                                 self.op.mode, errors.ECODE_INVAL)

  def ExpandNames(self):
    """ExpandNames for CreateInstance.

    Figure out the right locks for instance creation.

    """
    self.needed_locks = {}

    if self.op.commit:
      (uuid, name) = self.cfg.ExpandInstanceName(self.op.instance_name)
      if name is None:
        raise errors.OpPrereqError("Instance %s unknown" %
                                   self.op.instance_name,
                                   errors.ECODE_INVAL)
      self.op.instance_name = name
      if not self.cfg.GetInstanceInfo(uuid).forthcoming:
        raise errors.OpPrereqError("Instance %s (with uuid %s) not forthcoming"
                                   " but --commit was passed." % (name, uuid),
                                   errors.ECODE_STATE)
      logging.debug("Verified that instance %s with uuid %s is forthcoming",
                    name, uuid)
    else:
      # this is just a preventive check, but someone might still add this
      # instance in the meantime; we check again in CheckPrereq
      CheckInstanceExistence(self, self.op.instance_name)

    self.add_locks[locking.LEVEL_INSTANCE] = self.op.instance_name

    if self.op.commit:
      (uuid, _) = self.cfg.ExpandInstanceName(self.op.instance_name)
      self.needed_locks[locking.LEVEL_NODE] = self.cfg.GetInstanceNodes(uuid)
      logging.debug("Forthcoming instance %s resides on %s", uuid,
                    self.needed_locks[locking.LEVEL_NODE])
    elif self.op.iallocator:
      # TODO: Find a solution to not lock all nodes in the cluster, e.g. by
      # specifying a group on instance creation and then selecting nodes from
      # that group
      self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET

      if self.op.opportunistic_locking:
        self.opportunistic_locks[locking.LEVEL_NODE] = True
        self.opportunistic_locks[locking.LEVEL_NODE_RES] = True
        if self.op.disk_template == constants.DT_DRBD8:
          self.opportunistic_locks_count[locking.LEVEL_NODE] = 2
          self.opportunistic_locks_count[locking.LEVEL_NODE_RES] = 2
    else:
      (self.op.pnode_uuid, self.op.pnode) = \
        ExpandNodeUuidAndName(self.cfg, self.op.pnode_uuid, self.op.pnode)
      nodelist = [self.op.pnode_uuid]
      if self.op.snode is not None:
        (self.op.snode_uuid, self.op.snode) = \
          ExpandNodeUuidAndName(self.cfg, self.op.snode_uuid, self.op.snode)
        nodelist.append(self.op.snode_uuid)
      self.needed_locks[locking.LEVEL_NODE] = nodelist

    # in case of import lock the source node too
    if self.op.mode == constants.INSTANCE_IMPORT:
      src_node = self.op.src_node
      src_path = self.op.src_path

      if src_path is None:
        self.op.src_path = src_path = self.op.instance_name

      if src_node is None:
        self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET
        self.op.src_node = None
        if os.path.isabs(src_path):
          raise errors.OpPrereqError("Importing an instance from a path"
                                     " requires a source node option",
                                     errors.ECODE_INVAL)
      else:
        (self.op.src_node_uuid, self.op.src_node) = (_, src_node) = \
          ExpandNodeUuidAndName(self.cfg, self.op.src_node_uuid, src_node)
        if self.needed_locks[locking.LEVEL_NODE] is not locking.ALL_SET:
          self.needed_locks[locking.LEVEL_NODE].append(self.op.src_node_uuid)
        if not os.path.isabs(src_path):
          self.op.src_path = \
            utils.PathJoin(pathutils.EXPORT_DIR, src_path)

    self.needed_locks[locking.LEVEL_NODE_RES] = \
      CopyLockList(self.needed_locks[locking.LEVEL_NODE])

    # Optimistically acquire shared group locks (we're reading the
    # configuration).  We can't just call GetInstanceNodeGroups, because the
    # instance doesn't exist yet. Therefore we lock all node groups of all
    # nodes we have.
    if self.needed_locks[locking.LEVEL_NODE] == locking.ALL_SET:
      # In the case we lock all nodes for opportunistic allocation, we have no
      # choice than to lock all groups, because they're allocated before nodes.
      # This is sad, but true. At least we release all those we don't need in
      # CheckPrereq later.
      self.needed_locks[locking.LEVEL_NODEGROUP] = locking.ALL_SET
    else:
      self.needed_locks[locking.LEVEL_NODEGROUP] = \
        list(self.cfg.GetNodeGroupsFromNodes(
          self.needed_locks[locking.LEVEL_NODE]))
    self.share_locks[locking.LEVEL_NODEGROUP] = 1

  def DeclareLocks(self, level):
    if level == locking.LEVEL_NODE_RES:
      if self.op.opportunistic_locking:
        self.needed_locks[locking.LEVEL_NODE_RES] = \
          CopyLockList(list(self.owned_locks(locking.LEVEL_NODE)))

  def _RunAllocator(self):
    """Run the allocator based on input opcode.

    """
    if self.op.opportunistic_locking:
      # Only consider nodes for which a lock is held
      node_name_whitelist = self.cfg.GetNodeNames(
        set(self.owned_locks(locking.LEVEL_NODE)) &
        set(self.owned_locks(locking.LEVEL_NODE_RES)))
      logging.debug("Trying to allocate on nodes %s", node_name_whitelist)
    else:
      node_name_whitelist = None

    req = CreateInstanceAllocRequest(self.op, self.disks,
                                     self.nics, self.be_full,
                                     node_name_whitelist)
    ial = iallocator.IAllocator(self.cfg, self.rpc, req)

    ial.Run(self.op.iallocator)

    if not ial.success:
      # When opportunistic locks are used only a temporary failure is generated
      if self.op.opportunistic_locking:
        ecode = errors.ECODE_TEMP_NORES
        self.LogInfo("IAllocator '%s' failed on opportunistically acquired"
                     " nodes: %s", self.op.iallocator, ial.info)
      else:
        ecode = errors.ECODE_NORES

      raise errors.OpPrereqError("Can't compute nodes using"
                                 " iallocator '%s': %s" %
                                 (self.op.iallocator, ial.info),
                                 ecode)

    (self.op.pnode_uuid, self.op.pnode) = ExpandNodeUuidAndName(
        self.cfg, None, ial.result[0]) # pylint: disable=E1136
    self.LogInfo("Selected nodes for instance %s via iallocator %s: %s",
                 self.op.instance_name, self.op.iallocator,
                 utils.CommaJoin(ial.result))

    assert req.RequiredNodes() in (1, 2), "Wrong node count from iallocator"

    if req.RequiredNodes() == 2:
      (self.op.snode_uuid, self.op.snode) = ExpandNodeUuidAndName(
          self.cfg, None, ial.result[1]) # pylint: disable=E1136

  def BuildHooksEnv(self):
    """Build hooks env.

    This runs on master, primary and secondary nodes of the instance.

    """
    env = {
      "ADD_MODE": self.op.mode,
      }
    if self.op.mode == constants.INSTANCE_IMPORT:
      env["SRC_NODE"] = self.op.src_node
      env["SRC_PATH"] = self.op.src_path
      env["SRC_IMAGES"] = self.src_images

    env.update(BuildInstanceHookEnv(
      name=self.op.instance_name,
      primary_node_name=self.op.pnode,
      secondary_node_names=self.cfg.GetNodeNames(self.secondaries),
      status=self.op.start,
      os_type=self.op.os_type,
      minmem=self.be_full[constants.BE_MINMEM],
      maxmem=self.be_full[constants.BE_MAXMEM],
      vcpus=self.be_full[constants.BE_VCPUS],
      nics=NICListToTuple(self, self.nics),
      disk_template=self.op.disk_template,
      # Note that self.disks here is not a list with objects.Disk
      # but with dicts as returned by ComputeDisks.
      disks=self.disks,
      bep=self.be_full,
      hvp=self.hv_full,
      hypervisor_name=self.op.hypervisor,
      tags=self.op.tags,
      ))

    return env

  def BuildHooksNodes(self):
    """Build hooks nodes.

    """
    nl = [self.cfg.GetMasterNode(), self.op.pnode_uuid] + self.secondaries
    return nl, nl

  def _ReadExportInfo(self):
    """Reads the export information from disk.

    It will override the opcode source node and path with the actual
    information, if these two were not specified before.

    @return: the export information

    """
    assert self.op.mode == constants.INSTANCE_IMPORT

    if self.op.src_node_uuid is None:
      locked_nodes = self.owned_locks(locking.LEVEL_NODE)
      exp_list = self.rpc.call_export_list(locked_nodes)
      found = False
      for node_uuid in exp_list:
        if exp_list[node_uuid].fail_msg:
          continue
        if self.op.src_path in exp_list[node_uuid].payload:
          found = True
          self.op.src_node = self.cfg.GetNodeInfo(node_uuid).name
          self.op.src_node_uuid = node_uuid
          self.op.src_path = utils.PathJoin(pathutils.EXPORT_DIR,
                                            self.op.src_path)
          break
      if not found:
        raise errors.OpPrereqError("No export found for relative path %s" %
                                   self.op.src_path, errors.ECODE_INVAL)

    CheckNodeOnline(self, self.op.src_node_uuid)
    result = self.rpc.call_export_info(self.op.src_node_uuid, self.op.src_path)
    result.Raise("No export or invalid export found in dir %s" %
                 self.op.src_path)

    export_info = objects.SerializableConfigParser.Loads(str(result.payload))
    if not export_info.has_section(constants.INISECT_EXP):
      raise errors.ProgrammerError("Corrupted export config",
                                   errors.ECODE_ENVIRON)

    ei_version = export_info.get(constants.INISECT_EXP, "version")
    if int(ei_version) != constants.EXPORT_VERSION:
      raise errors.OpPrereqError("Wrong export version %s (wanted %d)" %
                                 (ei_version, constants.EXPORT_VERSION),
                                 errors.ECODE_ENVIRON)
    return export_info

  def _ReadExportParams(self, einfo):
    """Use export parameters as defaults.

    In case the opcode doesn't specify (as in override) some instance
    parameters, then try to use them from the export information, if
    that declares them.

    """
    self.op.os_type = einfo.get(constants.INISECT_EXP, "os")

    if not self.op.disks:
      disks = []
      # TODO: import the disk iv_name too
      for idx in range(constants.MAX_DISKS):
        if einfo.has_option(constants.INISECT_INS, "disk%d_size" % idx):
          disk_sz = einfo.getint(constants.INISECT_INS, "disk%d_size" % idx)
          disk_name = einfo.get(constants.INISECT_INS, "disk%d_name" % idx)
          disk = {
            constants.IDISK_SIZE: disk_sz,
            constants.IDISK_NAME: disk_name
            }
          disks.append(disk)
      self.op.disks = disks
      if not disks and self.op.disk_template != constants.DT_DISKLESS:
        raise errors.OpPrereqError("No disk info specified and the export"
                                   " is missing the disk information",
                                   errors.ECODE_INVAL)

    if not self.op.nics:
      nics = []
      for idx in range(constants.MAX_NICS):
        if einfo.has_option(constants.INISECT_INS, "nic%d_mac" % idx):
          ndict = {}
          for name in [constants.INIC_IP,
                       constants.INIC_MAC, constants.INIC_NAME]:
            nic_param_name = "nic%d_%s" % (idx, name)
            if einfo.has_option(constants.INISECT_INS, nic_param_name):
              v = einfo.get(constants.INISECT_INS, "nic%d_%s" % (idx, name))
              ndict[name] = v
          network = einfo.get(constants.INISECT_INS,
                              "nic%d_%s" % (idx, constants.INIC_NETWORK))
          # in case network is given link and mode are inherited
          # from nodegroup's netparams and thus should not be passed here
          if network:
            ndict[constants.INIC_NETWORK] = network
          else:
            for name in list(constants.NICS_PARAMETERS):
              v = einfo.get(constants.INISECT_INS, "nic%d_%s" % (idx, name))
              ndict[name] = v
          nics.append(ndict)
        else:
          break
      self.op.nics = nics

    if not self.op.tags and einfo.has_option(constants.INISECT_INS, "tags"):
      self.op.tags = einfo.get(constants.INISECT_INS, "tags").split()

    if (self.op.hypervisor is None and
        einfo.has_option(constants.INISECT_INS, "hypervisor")):
      self.op.hypervisor = einfo.get(constants.INISECT_INS, "hypervisor")

    if einfo.has_section(constants.INISECT_HYP):
      # use the export parameters but do not override the ones
      # specified by the user
      for name, value in einfo.items(constants.INISECT_HYP):
        if name not in self.op.hvparams:
          self.op.hvparams[name] = value

    if einfo.has_section(constants.INISECT_BEP):
      # use the parameters, without overriding
      for name, value in einfo.items(constants.INISECT_BEP):
        if name not in self.op.beparams:
          self.op.beparams[name] = value
        # Compatibility for the old "memory" be param
        if name == constants.BE_MEMORY:
          if constants.BE_MAXMEM not in self.op.beparams:
            self.op.beparams[constants.BE_MAXMEM] = value
          if constants.BE_MINMEM not in self.op.beparams:
            self.op.beparams[constants.BE_MINMEM] = value
    else:
      # try to read the parameters old style, from the main section
      for name in constants.BES_PARAMETERS:
        if (name not in self.op.beparams and
            einfo.has_option(constants.INISECT_INS, name)):
          self.op.beparams[name] = einfo.get(constants.INISECT_INS, name)

    if einfo.has_section(constants.INISECT_OSP):
      # use the parameters, without overriding
      for name, value in einfo.items(constants.INISECT_OSP):
        if name not in self.op.osparams:
          self.op.osparams[name] = value

    if einfo.has_section(constants.INISECT_OSP_PRIVATE):
      # use the parameters, without overriding
      for name, value in einfo.items(constants.INISECT_OSP_PRIVATE):
        if name not in self.op.osparams_private:
          self.op.osparams_private[name] = serializer.Private(value, descr=name)

  def _RevertToDefaults(self, cluster):
    """Revert the instance parameters to the default values.

    """
    # hvparams
    hv_defs = cluster.SimpleFillHV(self.op.hypervisor, self.op.os_type, {})
    for name in list(self.op.hvparams):
      if name in hv_defs and hv_defs[name] == self.op.hvparams[name]:
        del self.op.hvparams[name]
    # beparams
    be_defs = cluster.SimpleFillBE({})
    for name in list(self.op.beparams):
      if name in be_defs and be_defs[name] == self.op.beparams[name]:
        del self.op.beparams[name]
    # nic params
    nic_defs = cluster.SimpleFillNIC({})
    for nic in self.op.nics:
      for name in constants.NICS_PARAMETERS:
        if name in nic and name in nic_defs and nic[name] == nic_defs[name]:
          del nic[name]
    # osparams
    os_defs = cluster.SimpleFillOS(self.op.os_type, {})
    for name in list(self.op.osparams):
      if name in os_defs and os_defs[name] == self.op.osparams[name]:
        del self.op.osparams[name]

    os_defs_ = cluster.SimpleFillOS(self.op.os_type, {},
                                    os_params_private={})
    for name in list(self.op.osparams_private):
      if name in os_defs_ and os_defs_[name] == self.op.osparams_private[name]:
        del self.op.osparams_private[name]

  def _GetNodesFromForthcomingInstance(self):
    """Set nodes as in the forthcoming instance

    """
    (uuid, name) = self.cfg.ExpandInstanceName(self.op.instance_name)
    inst = self.cfg.GetInstanceInfo(uuid)
    self.op.pnode_uuid = inst.primary_node
    self.op.pnode = self.cfg.GetNodeName(inst.primary_node)
    sec_nodes = self.cfg.GetInstanceSecondaryNodes(uuid)
    node_names = [self.op.pnode]
    if sec_nodes:
      self.op.snode_uuid = sec_nodes[0]
      self.op.snode = self.cfg.GetNodeName(sec_nodes[0])
      node_names.append(self.op.snode)
    self.LogInfo("Nodes of instance %s: %s", name, node_names)

  def CheckPrereq(self): # pylint: disable=R0914
    """Check prerequisites.

    """
    owned_nodes = frozenset(self.owned_locks(locking.LEVEL_NODE))

    if self.op.commit:
      # Check that the instance is still on the cluster, forthcoming, and
      # still resides on the nodes we acquired.
      (uuid, name) = self.cfg.ExpandInstanceName(self.op.instance_name)
      if uuid is None:
        raise errors.OpPrereqError("Instance %s disappeared from the cluster"
                                   " while waiting for locks"
                                   % (self.op.instance_name,),
                                   errors.ECODE_STATE)
      if not self.cfg.GetInstanceInfo(uuid).forthcoming:
        raise errors.OpPrereqError("Instance %s (with uuid %s) is no longer"
                                   " forthcoming" % (name, uuid),
                                   errors.ECODE_STATE)
      required_nodes = self.cfg.GetInstanceNodes(uuid)
      if not owned_nodes.issuperset(required_nodes):
        raise errors.OpPrereqError("Forthcoming instance %s nodes changed"
                                   " since locks were acquired; retry the"
                                   " operation" % self.op.instance_name,
                                   errors.ECODE_STATE)
    else:
      CheckInstanceExistence(self, self.op.instance_name)

    # Check that the optimistically acquired groups are correct wrt the
    # acquired nodes
    owned_groups = frozenset(self.owned_locks(locking.LEVEL_NODEGROUP))
    cur_groups = list(self.cfg.GetNodeGroupsFromNodes(owned_nodes))
    if not owned_groups.issuperset(cur_groups):
      raise errors.OpPrereqError("New instance %s's node groups changed since"
                                 " locks were acquired, current groups are"
                                 " are '%s', owning groups '%s'; retry the"
                                 " operation" %
                                 (self.op.instance_name,
                                  utils.CommaJoin(cur_groups),
                                  utils.CommaJoin(owned_groups)),
                                 errors.ECODE_STATE)

    self.instance_file_storage_dir = CalculateFileStorageDir(
        self.op.disk_template, self.cfg, self.op.instance_name,
        self.op.file_storage_dir)

    if self.op.mode == constants.INSTANCE_IMPORT:
      export_info = self._ReadExportInfo()
      self._ReadExportParams(export_info)
      self._old_instance_name = export_info.get(constants.INISECT_INS, "name")
    else:
      self._old_instance_name = None

    if (not self.cfg.GetVGName() and
        self.op.disk_template not in constants.DTS_NOT_LVM):
      raise errors.OpPrereqError("Cluster does not support lvm-based"
                                 " instances", errors.ECODE_STATE)

    if (self.op.hypervisor is None or
        self.op.hypervisor == constants.VALUE_AUTO):
      self.op.hypervisor = self.cfg.GetHypervisorType()

    cluster = self.cfg.GetClusterInfo()
    enabled_hvs = cluster.enabled_hypervisors
    if self.op.hypervisor not in enabled_hvs:
      raise errors.OpPrereqError("Selected hypervisor (%s) not enabled in the"
                                 " cluster (%s)" %
                                 (self.op.hypervisor, ",".join(enabled_hvs)),
                                 errors.ECODE_STATE)

    # Check tag validity
    for tag in self.op.tags:
      objects.TaggableObject.ValidateTag(tag)

    # check hypervisor parameter syntax (locally)
    utils.ForceDictType(self.op.hvparams, constants.HVS_PARAMETER_TYPES)
    filled_hvp = cluster.SimpleFillHV(self.op.hypervisor, self.op.os_type,
                                      self.op.hvparams)
    hv_type = hypervisor.GetHypervisorClass(self.op.hypervisor)
    hv_type.CheckParameterSyntax(filled_hvp)
    self.hv_full = filled_hvp
    # check that we don't specify global parameters on an instance
    CheckParamsNotGlobal(self.op.hvparams, constants.HVC_GLOBALS, "hypervisor",
                         "instance", "cluster")

    # fill and remember the beparams dict
    self.be_full = ComputeFullBeParams(self.op, cluster)

    # build os parameters
    if self.op.osparams_private is None:
      self.op.osparams_private = serializer.PrivateDict()
    if self.op.osparams_secret is None:
      self.op.osparams_secret = serializer.PrivateDict()

    self.os_full = cluster.SimpleFillOS(
      self.op.os_type,
      self.op.osparams,
      os_params_private=self.op.osparams_private,
      os_params_secret=self.op.osparams_secret
    )

    # now that hvp/bep are in final format, let's reset to defaults,
    # if told to do so
    if self.op.identify_defaults:
      self._RevertToDefaults(cluster)

    # NIC buildup
    self.nics = ComputeNics(self.op, cluster, self.check_ip, self.cfg,
                            self.proc.GetECId())

    # disk checks/pre-build
    default_vg = self.cfg.GetVGName()
    self.disks = ComputeDisks(self.op.disks, self.op.disk_template, default_vg)

    if self.op.mode == constants.INSTANCE_IMPORT:
      disk_images = []
      for idx in range(len(self.disks)):
        option = "disk%d_dump" % idx
        if export_info.has_option(constants.INISECT_INS, option):
          # FIXME: are the old os-es, disk sizes, etc. useful?
          export_name = export_info.get(constants.INISECT_INS, option)
          image = utils.PathJoin(self.op.src_path, export_name)
          disk_images.append(image)
        else:
          disk_images.append(False)

      self.src_images = disk_images

      if self.op.instance_name == self._old_instance_name:
        for idx, nic in enumerate(self.nics):
          if nic.mac == constants.VALUE_AUTO:
            nic_mac_ini = "nic%d_mac" % idx
            nic.mac = export_info.get(constants.INISECT_INS, nic_mac_ini)

    # ENDIF: self.op.mode == constants.INSTANCE_IMPORT

    # ip ping checks (we use the same ip that was resolved in ExpandNames)
    if self.op.ip_check:
      if netutils.TcpPing(self.check_ip, constants.DEFAULT_NODED_PORT):
        raise errors.OpPrereqError("IP %s of instance %s already in use" %
                                   (self.check_ip, self.op.instance_name),
                                   errors.ECODE_NOTUNIQUE)

    #### mac address generation
    # By generating here the mac address both the allocator and the hooks get
    # the real final mac address rather than the 'auto' or 'generate' value.
    # There is a race condition between the generation and the instance object
    # creation, which means that we know the mac is valid now, but we're not
    # sure it will be when we actually add the instance. If things go bad
    # adding the instance will abort because of a duplicate mac, and the
    # creation job will fail.
    for nic in self.nics:
      if nic.mac in (constants.VALUE_AUTO, constants.VALUE_GENERATE):
        nic.mac = self.cfg.GenerateMAC(nic.network, self.proc.GetECId())

    #### allocator run

    if self.op.iallocator is not None:
      if self.op.commit:
        self._GetNodesFromForthcomingInstance()
      else:
        self._RunAllocator()

    # Release all unneeded node locks
    keep_locks = [uuid for uuid in [self.op.pnode_uuid, self.op.snode_uuid,
                                    self.op.src_node_uuid] if uuid]
    ReleaseLocks(self, locking.LEVEL_NODE, keep=keep_locks)
    ReleaseLocks(self, locking.LEVEL_NODE_RES, keep=keep_locks)
    # Release all unneeded group locks
    ReleaseLocks(self, locking.LEVEL_NODEGROUP,
                 keep=self.cfg.GetNodeGroupsFromNodes(keep_locks))

    assert (self.owned_locks(locking.LEVEL_NODE) ==
            self.owned_locks(locking.LEVEL_NODE_RES)), \
      ("Node locks differ from node resource locks (%s vs %s)"
       % (self.owned_locks(locking.LEVEL_NODE),
          self.owned_locks(locking.LEVEL_NODE_RES)))

    #### node related checks

    # check primary node
    self.pnode = pnode = self.cfg.GetNodeInfo(self.op.pnode_uuid)
    assert self.pnode is not None, \
      "Cannot retrieve locked node %s" % self.op.pnode_uuid
    if pnode.offline:
      raise errors.OpPrereqError("Cannot use offline primary node '%s'" %
                                 pnode.name, errors.ECODE_STATE)
    if pnode.drained:
      raise errors.OpPrereqError("Cannot use drained primary node '%s'" %
                                 pnode.name, errors.ECODE_STATE)
    if not pnode.vm_capable:
      raise errors.OpPrereqError("Cannot use non-vm_capable primary node"
                                 " '%s'" % pnode.name, errors.ECODE_STATE)

    self.secondaries = []

    # Fill in any IPs from IP pools. This must happen here, because we need to
    # know the nic's primary node, as specified by the iallocator
    for idx, nic in enumerate(self.nics):
      net_uuid = nic.network
      if net_uuid is not None:
        nobj = self.cfg.GetNetwork(net_uuid)
        netparams = self.cfg.GetGroupNetParams(net_uuid, self.pnode.uuid)
        if netparams is None:
          raise errors.OpPrereqError("No netparams found for network"
                                     " %s. Probably not connected to"
                                     " node's %s nodegroup" %
                                     (nobj.name, self.pnode.name),
                                     errors.ECODE_INVAL)
        self.LogInfo("NIC/%d inherits netparams %s" %
                     (idx, list(netparams.values())))
        nic.nicparams = dict(netparams)
        if nic.ip is not None:
          if nic.ip.lower() == constants.NIC_IP_POOL:
            try:
              nic.ip = self.cfg.GenerateIp(net_uuid, self.proc.GetECId())
            except errors.ReservationError:
              raise errors.OpPrereqError("Unable to get a free IP for NIC %d"
                                         " from the address pool" % idx,
                                         errors.ECODE_STATE)
            self.LogInfo("Chose IP %s from network %s", nic.ip, nobj.name)
          else:
            try:
              self.cfg.ReserveIp(net_uuid, nic.ip, self.proc.GetECId(),
                                 check=self.op.conflicts_check)
            except errors.ReservationError:
              raise errors.OpPrereqError("IP address %s already in use"
                                         " or does not belong to network %s" %
                                         (nic.ip, nobj.name),
                                         errors.ECODE_NOTUNIQUE)

      # net is None, ip None or given
      elif self.op.conflicts_check:
        CheckForConflictingIp(self, nic.ip, self.pnode.uuid)

    # mirror node verification
    if self.op.disk_template in constants.DTS_INT_MIRROR:
      if self.op.snode_uuid == pnode.uuid:
        raise errors.OpPrereqError("The secondary node cannot be the"
                                   " primary node", errors.ECODE_INVAL)
      CheckNodeOnline(self, self.op.snode_uuid)
      CheckNodeNotDrained(self, self.op.snode_uuid)
      CheckNodeVmCapable(self, self.op.snode_uuid)
      self.secondaries.append(self.op.snode_uuid)

      snode = self.cfg.GetNodeInfo(self.op.snode_uuid)
      if pnode.group != snode.group:
        self.LogWarning("The primary and secondary nodes are in two"
                        " different node groups; the disk parameters"
                        " from the first disk's node group will be"
                        " used")

    nodes = [pnode]
    if self.op.disk_template in constants.DTS_INT_MIRROR:
      nodes.append(snode)
    has_es = lambda n: IsExclusiveStorageEnabledNode(self.cfg, n)
    excl_stor = compat.any(map(has_es, nodes))
    if excl_stor and not self.op.disk_template in constants.DTS_EXCL_STORAGE:
      raise errors.OpPrereqError("Disk template %s not supported with"
                                 " exclusive storage" % self.op.disk_template,
                                 errors.ECODE_STATE)
    for disk in self.disks:
      CheckSpindlesExclusiveStorage(disk, excl_stor, True)

    node_uuids = [pnode.uuid] + self.secondaries

    if not self.adopt_disks:
      if self.op.disk_template == constants.DT_RBD:
        # _CheckRADOSFreeSpace() is just a placeholder.
        # Any function that checks prerequisites can be placed here.
        # Check if there is enough space on the RADOS cluster.
        CheckRADOSFreeSpace()
      elif self.op.disk_template == constants.DT_EXT:
        # FIXME: Function that checks prereqs if needed
        pass
      elif self.op.disk_template in constants.DTS_LVM:
        # Check lv size requirements, if not adopting
        req_sizes = ComputeDiskSizePerVG(self.op.disk_template, self.disks)
        CheckNodesFreeDiskPerVG(self, node_uuids, req_sizes)
      else:
        # FIXME: add checks for other, non-adopting, non-lvm disk templates
        pass

    elif self.op.disk_template == constants.DT_PLAIN: # Check the adoption data
      all_lvs = set(["%s/%s" % (disk[constants.IDISK_VG],
                                disk[constants.IDISK_ADOPT])
                     for disk in self.disks])
      if len(all_lvs) != len(self.disks):
        raise errors.OpPrereqError("Duplicate volume names given for adoption",
                                   errors.ECODE_INVAL)
      for lv_name in all_lvs:
        try:
          # FIXME: lv_name here is "vg/lv" need to ensure that other calls
          # to ReserveLV uses the same syntax
          self.cfg.ReserveLV(lv_name, self.proc.GetECId())
        except errors.ReservationError:
          raise errors.OpPrereqError("LV named %s used by another instance" %
                                     lv_name, errors.ECODE_NOTUNIQUE)

      vg_names = self.rpc.call_vg_list([pnode.uuid])[pnode.uuid]
      vg_names.Raise("Cannot get VG information from node %s" % pnode.name,
                     prereq=True)

      node_lvs = self.rpc.call_lv_list([pnode.uuid],
                                       list(vg_names.payload))[pnode.uuid]
      node_lvs.Raise("Cannot get LV information from node %s" % pnode.name,
                     prereq=True)
      node_lvs = node_lvs.payload

      delta = all_lvs.difference(node_lvs)
      if delta:
        raise errors.OpPrereqError("Missing logical volume(s): %s" %
                                   utils.CommaJoin(delta),
                                   errors.ECODE_INVAL)
      online_lvs = [lv for lv in all_lvs if node_lvs[lv][2]]
      if online_lvs:
        raise errors.OpPrereqError("Online logical volumes found, cannot"
                                   " adopt: %s" % utils.CommaJoin(online_lvs),
                                   errors.ECODE_STATE)
      # update the size of disk based on what is found
      for dsk in self.disks:
        dsk[constants.IDISK_SIZE] = \
          int(float(node_lvs["%s/%s" % (dsk[constants.IDISK_VG],
                                        dsk[constants.IDISK_ADOPT])][0]))

    elif self.op.disk_template == constants.DT_BLOCK:
      # Normalize and de-duplicate device paths
      all_disks = set([os.path.abspath(disk[constants.IDISK_ADOPT])
                       for disk in self.disks])
      if len(all_disks) != len(self.disks):
        raise errors.OpPrereqError("Duplicate disk names given for adoption",
                                   errors.ECODE_INVAL)
      baddisks = [d for d in all_disks
                  if not d.startswith(constants.ADOPTABLE_BLOCKDEV_ROOT)]
      if baddisks:
        raise errors.OpPrereqError("Device node(s) %s lie outside %s and"
                                   " cannot be adopted" %
                                   (utils.CommaJoin(baddisks),
                                    constants.ADOPTABLE_BLOCKDEV_ROOT),
                                   errors.ECODE_INVAL)

      node_disks = self.rpc.call_bdev_sizes([pnode.uuid],
                                            list(all_disks))[pnode.uuid]
      node_disks.Raise("Cannot get block device information from node %s" %
                       pnode.name, prereq=True)
      node_disks = node_disks.payload
      delta = all_disks.difference(node_disks)
      if delta:
        raise errors.OpPrereqError("Missing block device(s): %s" %
                                   utils.CommaJoin(delta),
                                   errors.ECODE_INVAL)
      for dsk in self.disks:
        dsk[constants.IDISK_SIZE] = \
          int(float(node_disks[dsk[constants.IDISK_ADOPT]]))

    # Check disk access param to be compatible with specified hypervisor
    node_info = self.cfg.GetNodeInfo(self.op.pnode_uuid)
    node_group = self.cfg.GetNodeGroup(node_info.group)
    group_disk_params = self.cfg.GetGroupDiskParams(node_group)
    group_access_type = group_disk_params[self.op.disk_template].get(
      constants.RBD_ACCESS, constants.DISK_KERNELSPACE
    )
    for dsk in self.disks:
      access_type = dsk.get(constants.IDISK_ACCESS, group_access_type)
      if not IsValidDiskAccessModeCombination(self.op.hypervisor,
                                              self.op.disk_template,
                                              access_type):
        raise errors.OpPrereqError("Selected hypervisor (%s) cannot be"
                                   " used with %s disk access param" %
                                   (self.op.hypervisor, access_type),
                                    errors.ECODE_STATE)

    # Verify instance specs
    spindle_use = self.be_full.get(constants.BE_SPINDLE_USE, None)
    ispec = {
      constants.ISPEC_MEM_SIZE: self.be_full.get(constants.BE_MAXMEM, None),
      constants.ISPEC_CPU_COUNT: self.be_full.get(constants.BE_VCPUS, None),
      constants.ISPEC_DISK_COUNT: len(self.disks),
      constants.ISPEC_DISK_SIZE: [disk[constants.IDISK_SIZE]
                                  for disk in self.disks],
      constants.ISPEC_NIC_COUNT: len(self.nics),
      constants.ISPEC_SPINDLE_USE: spindle_use,
      }

    group_info = self.cfg.GetNodeGroup(pnode.group)
    ipolicy = ganeti.masterd.instance.CalculateGroupIPolicy(cluster, group_info)
    disk_types = [self.op.disk_template] * len(self.disks)
    res = ComputeIPolicyInstanceSpecViolation(ipolicy, ispec, disk_types)
    if not self.op.ignore_ipolicy and res:
      msg = ("Instance allocation to group %s (%s) violates policy: %s" %
             (pnode.group, group_info.name, utils.CommaJoin(res)))
      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)

    CheckHVParams(self, node_uuids, self.op.hypervisor, self.op.hvparams)

    CheckOSParams(self, True, node_uuids, self.op.os_type, self.os_full,
                  self.op.force_variant)

    CheckNicsBridgesExist(self, self.nics, self.pnode.uuid)

    CheckCompressionTool(self, self.op.compress)

    #TODO: _CheckExtParams (remotely)
    # Check parameters for extstorage

    # memory check on primary node
    #TODO(dynmem): use MINMEM for checking
    if self.op.start:
      hvfull = objects.FillDict(cluster.hvparams.get(self.op.hypervisor, {}),
                                self.op.hvparams)
      CheckNodeFreeMemory(self, self.pnode.uuid,
                          "creating instance %s" % self.op.instance_name,
                          self.be_full[constants.BE_MAXMEM],
                          self.op.hypervisor, hvfull)

    self.dry_run_result = list(node_uuids)

  def _RemoveDegradedDisks(self, feedback_fn, disk_abort, instance):
    """Removes degraded disks and instance.

    It optionally checks whether disks are degraded.  If the disks are
    degraded, they are removed and the instance is also removed from
    the configuration.

    If L{disk_abort} is True, then the disks are considered degraded
    and removed, and the instance is removed from the configuration.

    If L{disk_abort} is False, then it first checks whether disks are
    degraded and, if so, it removes the disks and the instance is
    removed from the configuration.

    @type feedback_fn: callable
    @param feedback_fn: function used send feedback back to the caller

    @type disk_abort: boolean
    @param disk_abort:
      True if disks are degraded, False to first check if disks are
      degraded
    @type instance: L{objects.Instance}
    @param instance: instance containing the disks to check

    @rtype: NoneType
    @return: None
    @raise errors.OpPrereqError: if disks are degraded

    """
    disk_info = self.cfg.GetInstanceDisks(instance.uuid)
    if disk_abort:
      pass
    elif self.op.wait_for_sync:
      disk_abort = not WaitForSync(self, instance)
    elif utils.AnyDiskOfType(disk_info, constants.DTS_INT_MIRROR):
      # make sure the disks are not degraded (still sync-ing is ok)
      feedback_fn("* checking mirrors status")
      disk_abort = not WaitForSync(self, instance, oneshot=True)
    else:
      disk_abort = False

    if disk_abort:
      RemoveDisks(self, instance)
      for disk_uuid in instance.disks:
        self.cfg.RemoveInstanceDisk(instance.uuid, disk_uuid)
      self.cfg.RemoveInstance(instance.uuid)
      raise errors.OpExecError("There are some degraded disks for"
                               " this instance")

  def RunOsScripts(self, feedback_fn, iobj):
    """Run OS scripts

    If necessary, disks are paused.  It handles instance create,
    import, and remote import.

    @type feedback_fn: callable
    @param feedback_fn: function used send feedback back to the caller

    @type iobj: L{objects.Instance}
    @param iobj: instance object

    """
    if not iobj.disks:
      return

    if self.adopt_disks:
      return

    disks = self.cfg.GetInstanceDisks(iobj.uuid)
    if self.op.mode == constants.INSTANCE_CREATE:
      os_image = objects.GetOSImage(self.op.osparams)

      if os_image is None and not self.op.no_install:
        pause_sync = (not self.op.wait_for_sync and
                      utils.AnyDiskOfType(disks, constants.DTS_INT_MIRROR))
        if pause_sync:
          feedback_fn("* pausing disk sync to install instance OS")
          result = self.rpc.call_blockdev_pause_resume_sync(self.pnode.uuid,
                                                            (disks, iobj),
                                                            True)
          for idx, success in enumerate(result.payload):
            if not success:
              logging.warn("pause-sync of instance %s for disk %d failed",
                           self.op.instance_name, idx)

        feedback_fn("* running the instance OS create scripts...")
        # FIXME: pass debug option from opcode to backend
        os_add_result = \
          self.rpc.call_instance_os_add(self.pnode.uuid,
                                        (iobj, self.op.osparams_secret),
                                        False,
                                        self.op.debug_level)
        if pause_sync:
          feedback_fn("* resuming disk sync")
          result = self.rpc.call_blockdev_pause_resume_sync(self.pnode.uuid,
                                                            (disks, iobj),
                                                            False)
          for idx, success in enumerate(result.payload):
            if not success:
              logging.warn("resume-sync of instance %s for disk %d failed",
                           self.op.instance_name, idx)

        os_add_result.Raise("Could not add os for instance %s"
                            " on node %s" % (self.op.instance_name,
                                             self.pnode.name))

    else:
      if self.op.mode == constants.INSTANCE_IMPORT:
        feedback_fn("* running the instance OS import scripts...")

        transfers = []

        for idx, image in enumerate(self.src_images):
          if not image:
            continue

          if iobj.os:
            dst_io = constants.IEIO_SCRIPT
            dst_ioargs = ((disks[idx], iobj), idx)
          else:
            dst_io = constants.IEIO_RAW_DISK
            dst_ioargs = (disks[idx], iobj)

          # FIXME: pass debug option from opcode to backend
          dt = masterd.instance.DiskTransfer("disk/%s" % idx,
                                             constants.IEIO_FILE, (image, ),
                                             dst_io, dst_ioargs,
                                             None)
          transfers.append(dt)

        import_result = \
          masterd.instance.TransferInstanceData(self, feedback_fn,
                                                self.op.src_node_uuid,
                                                self.pnode.uuid,
                                                self.pnode.secondary_ip,
                                                self.op.compress,
                                                iobj, transfers)
        if not compat.all(import_result):
          self.LogWarning("Some disks for instance %s on node %s were not"
                          " imported successfully" % (self.op.instance_name,
                                                      self.pnode.name))

        rename_from = self._old_instance_name

      elif self.op.mode == constants.INSTANCE_REMOTE_IMPORT:
        feedback_fn("* preparing remote import...")
        # The source cluster will stop the instance before attempting to make
        # a connection. In some cases stopping an instance can take a long
        # time, hence the shutdown timeout is added to the connection
        # timeout.
        connect_timeout = (constants.RIE_CONNECT_TIMEOUT +
                           self.op.source_shutdown_timeout)
        timeouts = masterd.instance.ImportExportTimeouts(connect_timeout)

        assert iobj.primary_node == self.pnode.uuid
        disk_results = \
          masterd.instance.RemoteImport(self, feedback_fn, iobj, self.pnode,
                                        self.source_x509_ca,
                                        self._cds, self.op.compress, timeouts)
        if not compat.all(disk_results):
          # TODO: Should the instance still be started, even if some disks
          # failed to import (valid for local imports, too)?
          self.LogWarning("Some disks for instance %s on node %s were not"
                          " imported successfully" % (self.op.instance_name,
                                                      self.pnode.name))

        rename_from = self.source_instance_name

      else:
        # also checked in the prereq part
        raise errors.ProgrammerError("Unknown OS initialization mode '%s'"
                                     % self.op.mode)

      assert iobj.name == self.op.instance_name

      # Run rename script on newly imported instance
      if iobj.os:
        feedback_fn("Running rename script for %s" % self.op.instance_name)
        result = self.rpc.call_instance_run_rename(self.pnode.uuid, iobj,
                                                   rename_from,
                                                   self.op.debug_level)
        result.Warn("Failed to run rename script for %s on node %s" %
                    (self.op.instance_name, self.pnode.name), self.LogWarning)

  def GetOsInstallPackageEnvironment(self, instance, script):
    """Returns the OS scripts environment for the helper VM

    @type instance: L{objects.Instance}
    @param instance: instance for which the OS scripts are run

    @type script: string
    @param script: script to run (e.g.,
                   constants.OS_SCRIPT_CREATE_UNTRUSTED)

    @rtype: dict of string to string
    @return: OS scripts environment for the helper VM

    """
    env = {"OS_SCRIPT": script}

    # We pass only the instance's disks, not the helper VM's disks.
    if instance.hypervisor == constants.HT_KVM:
      prefix = "/dev/vd"
    elif instance.hypervisor in [constants.HT_XEN_PVM, constants.HT_XEN_HVM]:
      prefix = "/dev/xvd"
    else:
      raise errors.OpExecError("Cannot run OS scripts in a virtualized"
                               " environment for hypervisor '%s'"
                               % instance.hypervisor)

    num_disks = len(self.cfg.GetInstanceDisks(instance.uuid))

    for idx, disk_label in enumerate(utils.GetDiskLabels(prefix, num_disks + 1,
                                                         start=1)):
      env["DISK_%d_PATH" % idx] = disk_label

    return env

  def UpdateInstanceOsInstallPackage(self, feedback_fn, instance, override_env):
    """Updates the OS parameter 'os-install-package' for an instance.

    The OS install package is an archive containing an OS definition
    and a file containing the environment variables needed to run the
    OS scripts.

    The OS install package is served by the metadata daemon to the
    instances, so the OS scripts can run inside the virtualized
    environment.

    @type feedback_fn: callable
    @param feedback_fn: function used send feedback back to the caller

    @type instance: L{objects.Instance}
    @param instance: instance for which the OS parameter
                     'os-install-package' is updated

    @type override_env: dict of string to string
    @param override_env: if supplied, it overrides the environment of
                         the export OS scripts archive

    """
    if "os-install-package" in instance.osparams:
      feedback_fn("Using OS install package '%s'" %
                  instance.osparams["os-install-package"])
    else:
      result = self.rpc.call_os_export(instance.primary_node, instance,
                                       override_env)
      result.Raise("Could not export OS '%s'" % instance.os)
      instance.osparams["os-install-package"] = result.payload

      feedback_fn("Created OS install package '%s'" % result.payload)

  def RunOsScriptsVirtualized(self, feedback_fn, instance):
    """Runs the OS scripts inside a safe virtualized environment.

    The virtualized environment reuses the instance and temporarily
    creates a disk onto which the image of the helper VM is dumped.
    The temporary disk is used to boot the helper VM.  The OS scripts
    are passed to the helper VM through the metadata daemon and the OS
    install package.

    @type feedback_fn: callable
    @param feedback_fn: function used send feedback back to the caller

    @type instance: L{objects.Instance}
    @param instance: instance for which the OS scripts must be run
                     inside the virtualized environment

    """
    install_image = self.cfg.GetInstallImage()

    if not install_image:
      raise errors.OpExecError("Cannot create install instance because an"
                               " install image has not been specified")

    env = self.GetOsInstallPackageEnvironment(
      instance,
      constants.OS_SCRIPT_CREATE_UNTRUSTED)
    self.UpdateInstanceOsInstallPackage(feedback_fn, instance, env)
    UpdateMetadata(feedback_fn, self.rpc, instance,
                   osparams_private=self.op.osparams_private,
                   osparams_secret=self.op.osparams_secret)

    RunWithHelperVM(self, instance, install_image,
                    self.op.helper_startup_timeout,
                    self.op.helper_shutdown_timeout,
                    log_prefix="Running OS create script",
                    feedback_fn=feedback_fn)

  def Exec(self, feedback_fn):
    """Create and add the instance to the cluster.

    """
    assert not (self.owned_locks(locking.LEVEL_NODE_RES) -
                self.owned_locks(locking.LEVEL_NODE)), \
      "Node locks differ from node resource locks"

    ht_kind = self.op.hypervisor
    if ht_kind in constants.HTS_REQ_PORT:
      network_port = self.cfg.AllocatePort()
    else:
      network_port = None

    if self.op.commit:
      (instance_uuid, _) = self.cfg.ExpandInstanceName(self.op.instance_name)
    else:
      instance_uuid = self.cfg.GenerateUniqueID(self.proc.GetECId())

    # This is ugly but we got a chicken-egg problem here
    # We can only take the group disk parameters, as the instance
    # has no disks yet (we are generating them right here).
    nodegroup = self.cfg.GetNodeGroup(self.pnode.group)

    if self.op.commit:
      disks = self.cfg.GetInstanceDisks(instance_uuid)
      CommitDisks(disks)
    else:
      disks = GenerateDiskTemplate(self,
                                   self.op.disk_template,
                                   instance_uuid, self.pnode.uuid,
                                   self.secondaries,
                                   self.disks,
                                   self.instance_file_storage_dir,
                                   self.op.file_driver,
                                   0,
                                   feedback_fn,
                                   self.cfg.GetGroupDiskParams(nodegroup),
                                   forthcoming=self.op.forthcoming)

    if self.op.os_type is None:
      os_type = ""
    else:
      os_type = self.op.os_type

    iobj = objects.Instance(name=self.op.instance_name,
                            uuid=instance_uuid,
                            os=os_type,
                            primary_node=self.pnode.uuid,
                            nics=self.nics, disks=[],
                            disk_template=self.op.disk_template,
                            disks_active=False,
                            admin_state=constants.ADMINST_DOWN,
                            admin_state_source=constants.ADMIN_SOURCE,
                            network_port=network_port,
                            beparams=self.op.beparams,
                            hvparams=self.op.hvparams,
                            hypervisor=self.op.hypervisor,
                            osparams=self.op.osparams,
                            osparams_private=self.op.osparams_private,
                            forthcoming=self.op.forthcoming,
                            )

    if self.op.tags:
      for tag in self.op.tags:
        iobj.AddTag(tag)

    if self.adopt_disks:
      if self.op.disk_template == constants.DT_PLAIN:
        # rename LVs to the newly-generated names; we need to construct
        # 'fake' LV disks with the old data, plus the new unique_id
        tmp_disks = [objects.Disk.FromDict(v.ToDict()) for v in disks]
        rename_to = []
        for t_dsk, a_dsk in zip(tmp_disks, self.disks):
          rename_to.append(t_dsk.logical_id)
          t_dsk.logical_id = (t_dsk.logical_id[0], a_dsk[constants.IDISK_ADOPT])
        result = self.rpc.call_blockdev_rename(self.pnode.uuid,
                                               list(zip(tmp_disks, rename_to)))
        result.Raise("Failed to rename adoped LVs")
    elif self.op.forthcoming:
      feedback_fn("Instance is forthcoming, not creating disks")
    else:
      feedback_fn("* creating instance disks...")
      try:
        CreateDisks(self, iobj, disks=disks)
      except errors.OpExecError:
        self.LogWarning("Device creation failed")
        for disk in disks:
          self.cfg.ReleaseDRBDMinors(disk.uuid)
        raise

    feedback_fn("adding instance %s to cluster config" % self.op.instance_name)
    self.cfg.AddInstance(iobj, self.proc.GetECId(), replace=self.op.commit)

    feedback_fn("adding disks to cluster config")
    for disk in disks:
      self.cfg.AddInstanceDisk(iobj.uuid, disk, replace=self.op.commit)

    if self.op.forthcoming:
      feedback_fn("Instance is forthcoming; not creating the actual instance")
      return self.cfg.GetNodeNames(list(self.cfg.GetInstanceNodes(iobj.uuid)))

    # re-read the instance from the configuration
    iobj = self.cfg.GetInstanceInfo(iobj.uuid)

    if self.op.mode == constants.INSTANCE_IMPORT:
      # Release unused nodes
      ReleaseLocks(self, locking.LEVEL_NODE, keep=[self.op.src_node_uuid])
    else:
      # Release all nodes
      ReleaseLocks(self, locking.LEVEL_NODE)

    # Wipe disks
    disk_abort = False
    if not self.adopt_disks and self.cfg.GetClusterInfo().prealloc_wipe_disks:
      feedback_fn("* wiping instance disks...")
      try:
        WipeDisks(self, iobj)
      except errors.OpExecError as err:
        logging.exception("Wiping disks failed")
        self.LogWarning("Wiping instance disks failed (%s)", err)
        disk_abort = True

    self._RemoveDegradedDisks(feedback_fn, disk_abort, iobj)

    # Image disks
    os_image = objects.GetOSImage(iobj.osparams)
    disk_abort = False

    if not self.adopt_disks and os_image is not None:
      feedback_fn("* imaging instance disks...")
      try:
        ImageDisks(self, iobj, os_image)
      except errors.OpExecError as err:
        logging.exception("Imaging disks failed")
        self.LogWarning("Imaging instance disks failed (%s)", err)
        disk_abort = True

    self._RemoveDegradedDisks(feedback_fn, disk_abort, iobj)

    # instance disks are now active
    iobj.disks_active = True

    # Release all node resource locks
    ReleaseLocks(self, locking.LEVEL_NODE_RES)

    if iobj.os:
      result = self.rpc.call_os_diagnose([iobj.primary_node])[iobj.primary_node]
      result.Raise("Failed to get OS '%s'" % iobj.os)

      trusted = None

      for (name, _, _, _, _, _, _, os_trusted) in result.payload:
        if name == objects.OS.GetName(iobj.os):
          trusted = os_trusted
          break

      if trusted is None:
        raise errors.OpPrereqError("OS '%s' is not available in node '%s'" %
                                   (iobj.os, iobj.primary_node))
      elif trusted:
        self.RunOsScripts(feedback_fn, iobj)
      else:
        self.RunOsScriptsVirtualized(feedback_fn, iobj)
        # Instance is modified by 'RunOsScriptsVirtualized',
        # therefore, it must be retrieved once again from the
        # configuration, otherwise there will be a config object
        # version mismatch.
        iobj = self.cfg.GetInstanceInfo(iobj.uuid)

    # Update instance metadata so that it can be reached from the
    # metadata service.
    UpdateMetadata(feedback_fn, self.rpc, iobj,
                   osparams_private=self.op.osparams_private,
                   osparams_secret=self.op.osparams_secret)

    assert not self.owned_locks(locking.LEVEL_NODE_RES)

    if self.op.start:
      iobj.admin_state = constants.ADMINST_UP
      self.cfg.Update(iobj, feedback_fn)
      logging.info("Starting instance %s on node %s", self.op.instance_name,
                   self.pnode.name)
      feedback_fn("* starting instance...")
      result = self.rpc.call_instance_start(self.pnode.uuid, (iobj, None, None),
                                            False, self.op.reason)
      result.Raise("Could not start instance")

    return self.cfg.GetNodeNames(list(self.cfg.GetInstanceNodes(iobj.uuid)))

  def PrepareRetry(self, feedback_fn):
    # A temporary lack of resources can only happen if opportunistic locking
    # is used.
    assert self.op.opportunistic_locking

    logging.info("Opportunistic locking did not suceed, falling back to"
                 " full lock allocation")
    feedback_fn("* falling back to full lock allocation")
    self.op.opportunistic_locking = False
