#
#

# Copyright (C) 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.


"""Support for mocking the cluster configuration"""


import random
import time
import uuid as uuid_module

from ganeti import config
from ganeti import constants
from ganeti import errors
from ganeti.network import AddressPool
from ganeti import objects
from ganeti import utils

import mocks


RESERVE_ACTION = "reserve"
RELEASE_ACTION = "release"


def _StubGetEntResolver():
  return mocks.FakeGetentResolver()


def _UpdateIvNames(base_idx, disks):
  """Update the C{iv_name} attribute of disks.

  @type disks: list of L{objects.Disk}

  """
  for (idx, disk) in enumerate(disks):
    disk.iv_name = "disk/%s" % (base_idx + idx)


# pylint: disable=R0904
class ConfigMock(config.ConfigWriter):
  """A mocked cluster configuration with added methods for easy customization.

  """

  def __init__(self, cfg_file="/dev/null"):
    self._cur_group_id = 1
    self._cur_node_id = 1
    self._cur_inst_id = 1
    self._cur_disk_id = 1
    self._cur_os_id = 1
    self._cur_nic_id = 1
    self._cur_net_id = 1
    self._default_os = None
    self._mocked_config_store = None

    self._temporary_macs = config.TemporaryReservationManager()
    self._temporary_secrets = config.TemporaryReservationManager()
    self._temporary_lvs = config.TemporaryReservationManager()
    self._temporary_ips = config.TemporaryReservationManager()

    super(ConfigMock, self).__init__(cfg_file=cfg_file,
                                     _getents=_StubGetEntResolver(),
                                     offline=True)

    with self.GetConfigManager():
      self._CreateConfig()

  def _GetUuid(self):
    return str(uuid_module.uuid4())

  def _GetObjUuid(self, obj):
    if obj is None:
      return None
    elif isinstance(obj, objects.ConfigObject):
      return obj.uuid
    else:
      return obj

  def AddNewNodeGroup(self,
                      uuid=None,
                      name=None,
                      ndparams=None,
                      diskparams=None,
                      ipolicy=None,
                      hv_state_static=None,
                      disk_state_static=None,
                      alloc_policy=None,
                      networks=None):
    """Add a new L{objects.NodeGroup} to the cluster configuration

    See L{objects.NodeGroup} for parameter documentation.

    @rtype: L{objects.NodeGroup}
    @return: the newly added node group

    """
    group_id = self._cur_group_id
    self._cur_group_id += 1

    if uuid is None:
      uuid = self._GetUuid()
    if name is None:
      name = "mock_group_%d" % group_id
    if networks is None:
      networks = {}

    group = objects.NodeGroup(uuid=uuid,
                              name=name,
                              ndparams=ndparams,
                              diskparams=diskparams,
                              ipolicy=ipolicy,
                              hv_state_static=hv_state_static,
                              disk_state_static=disk_state_static,
                              alloc_policy=alloc_policy,
                              networks=networks,
                              members=[])

    self._UnlockedAddNodeGroup(group, None, True)
    return group

  # pylint: disable=R0913
  def AddNewNode(self,
                 uuid=None,
                 name=None,
                 primary_ip=None,
                 secondary_ip=None,
                 master_candidate=True,
                 offline=False,
                 drained=False,
                 group=None,
                 master_capable=True,
                 vm_capable=True,
                 ndparams=None,
                 powered=True,
                 hv_state=None,
                 hv_state_static=None,
                 disk_state=None,
                 disk_state_static=None):
    """Add a new L{objects.Node} to the cluster configuration

    See L{objects.Node} for parameter documentation.

    @rtype: L{objects.Node}
    @return: the newly added node

    """
    node_id = self._cur_node_id
    self._cur_node_id += 1

    if uuid is None:
      uuid = self._GetUuid()
    if name is None:
      name = "mock_node_%d.example.com" % node_id
    if primary_ip is None:
      primary_ip = "192.0.2.%d" % node_id
    if secondary_ip is None:
      secondary_ip = "203.0.113.%d" % node_id
    if group is None:
      group = self._default_group.uuid
    group = self._GetObjUuid(group)
    if ndparams is None:
      ndparams = {}

    node = objects.Node(uuid=uuid,
                        name=name,
                        primary_ip=primary_ip,
                        secondary_ip=secondary_ip,
                        master_candidate=master_candidate,
                        offline=offline,
                        drained=drained,
                        group=group,
                        master_capable=master_capable,
                        vm_capable=vm_capable,
                        ndparams=ndparams,
                        powered=powered,
                        hv_state=hv_state,
                        hv_state_static=hv_state_static,
                        disk_state=disk_state,
                        disk_state_static=disk_state_static)

    self._UnlockedAddNode(node, None)
    return node

  def AddNewInstance(self,
                     uuid=None,
                     name=None,
                     primary_node=None,
                     os=None,
                     hypervisor=None,
                     hvparams=None,
                     beparams=None,
                     osparams=None,
                     osparams_private=None,
                     admin_state=None,
                     admin_state_source=None,
                     nics=None,
                     disks=None,
                     disk_template=None,
                     disks_active=None,
                     network_port=None,
                     secondary_node=None):
    """Add a new L{objects.Instance} to the cluster configuration

    See L{objects.Instance} for parameter documentation.

    @rtype: L{objects.Instance}
    @return: the newly added instance

    """
    inst_id = self._cur_inst_id
    self._cur_inst_id += 1

    if uuid is None:
      uuid = self._GetUuid()
    if name is None:
      name = "mock_inst_%d.example.com" % inst_id
    if primary_node is None:
      primary_node = self._master_node.uuid
    primary_node = self._GetObjUuid(primary_node)
    if os is None:
      os = self.GetDefaultOs().name + objects.OS.VARIANT_DELIM +\
           self.GetDefaultOs().supported_variants[0]
    if hypervisor is None:
      hypervisor = self.GetClusterInfo().enabled_hypervisors[0]
    if hvparams is None:
      hvparams = {}
    if beparams is None:
      beparams = {}
    if osparams is None:
      osparams = {}
    if osparams_private is None:
      osparams_private = {}
    if admin_state is None:
      admin_state = constants.ADMINST_DOWN
    if admin_state_source is None:
      admin_state_source = constants.ADMIN_SOURCE
    if nics is None:
      nics = [self.CreateNic()]
    if disk_template is None:
      if disks is None:
        # user chose nothing, so create a plain disk for him
        disk_template = constants.DT_PLAIN
      elif len(disks) == 0:
        disk_template = constants.DT_DISKLESS
      else:
        disk_template = disks[0].dev_type
    if disks is None:
      if disk_template == constants.DT_DISKLESS:
        disks = []
      elif disk_template == constants.DT_EXT:
        provider = "mock_provider"
        disks = [self.CreateDisk(dev_type=disk_template,
                                 primary_node=primary_node,
                                 secondary_node=secondary_node,
                                 params={constants.IDISK_PROVIDER: provider})]
      else:
        disks = [self.CreateDisk(dev_type=disk_template,
                                 primary_node=primary_node,
                                 secondary_node=secondary_node)]
    if disks_active is None:
      disks_active = admin_state == constants.ADMINST_UP

    inst = objects.Instance(uuid=uuid,
                            name=name,
                            primary_node=primary_node,
                            os=os,
                            hypervisor=hypervisor,
                            hvparams=hvparams,
                            beparams=beparams,
                            osparams=osparams,
                            osparams_private=osparams_private,
                            admin_state=admin_state,
                            admin_state_source=admin_state_source,
                            nics=nics,
                            disks=[],
                            disks_active=disks_active,
                            network_port=network_port)
    self.AddInstance(inst, None)
    for disk in disks:
      self.AddInstanceDisk(inst.uuid, disk)
    return inst

  def AddNewNetwork(self,
                    uuid=None,
                    name=None,
                    mac_prefix=None,
                    network=None,
                    network6=None,
                    gateway=None,
                    gateway6=None,
                    reservations=None,
                    ext_reservations=None):
    """Add a new L{objects.Network} to the cluster configuration

    See L{objects.Network} for parameter documentation.

    @rtype: L{objects.Network}
    @return: the newly added network

    """
    net_id = self._cur_net_id
    self._cur_net_id += 1

    if uuid is None:
      uuid = self._GetUuid()
    if name is None:
      name = "mock_net_%d" % net_id
    if network is None:
      network = "198.51.100.0/24"
    if gateway is None:
      if network[-3:] == "/24":
        gateway = network[:-4] + "1"
      else:
        gateway = "198.51.100.1"
    if network[-3:] == "/24" and gateway == network[:-4] + "1":
      if reservations is None:
        reservations = "0" * 256
      if ext_reservations:
        ext_reservations = "11" + ("0" * 253) + "1"
    elif reservations is None or ext_reservations is None:
      raise AssertionError("You have to specify 'reservations' and"
                           " 'ext_reservations'!")

    net = objects.Network(uuid=uuid,
                          name=name,
                          mac_prefix=mac_prefix,
                          network=network,
                          network6=network6,
                          gateway=gateway,
                          gateway6=gateway6,
                          reservations=reservations,
                          ext_reservations=ext_reservations)
    self.AddNetwork(net, None)
    return net

  def AddOrphanDisk(self, **params):
    disk = self.CreateDisk(**params)
    self._UnlockedAddDisk(disk)

  def ConnectNetworkToGroup(self, net, group, netparams=None):
    """Connect the given network to the group.

    @type net: string or L{objects.Network}
    @param net: network object or UUID
    @type group: string of L{objects.NodeGroup}
    @param group: node group object of UUID
    @type netparams: dict
    @param netparams: network parameters for this connection

    """
    net_obj = None
    if isinstance(net, objects.Network):
      net_obj = net
    else:
      net_obj = self.GetNetwork(net)

    group_obj = None
    if isinstance(group, objects.NodeGroup):
      group_obj = group
    else:
      group_obj = self.GetNodeGroup(group)

    if net_obj is None or group_obj is None:
      raise AssertionError("Failed to get network or node group")

    if netparams is None:
      netparams = {
        constants.NIC_MODE: constants.NIC_MODE_BRIDGED,
        constants.NIC_LINK: "br_mock"
      }

    group_obj.networks[net_obj.uuid] = netparams

  def CreateDisk(self,
                 uuid=None,
                 name=None,
                 dev_type=constants.DT_PLAIN,
                 logical_id=None,
                 children=None,
                 nodes=None,
                 iv_name=None,
                 size=1024,
                 mode=constants.DISK_RDWR,
                 params=None,
                 spindles=None,
                 primary_node=None,
                 secondary_node=None,
                 create_nodes=False,
                 instance_disk_index=0):
    """Create a new L{objecs.Disk} object

    @rtype: L{objects.Disk}
    @return: the newly create disk object

    """
    disk_id = self._cur_disk_id
    self._cur_disk_id += 1

    if uuid is None:
      uuid = self._GetUuid()
    if name is None:
      name = "mock_disk_%d" % disk_id
    if params is None:
      params = {}

    if dev_type == constants.DT_DRBD8:
      pnode_uuid = self._GetObjUuid(primary_node)
      snode_uuid = self._GetObjUuid(secondary_node)
      if logical_id is not None:
        pnode_uuid = logical_id[0]
        snode_uuid = logical_id[1]

      if pnode_uuid is None and create_nodes:
        pnode_uuid = self.AddNewNode().uuid
      if snode_uuid is None and create_nodes:
        snode_uuid = self.AddNewNode().uuid

      if pnode_uuid is None or snode_uuid is None:
        raise AssertionError("Trying to create DRBD disk without nodes!")

      if logical_id is None:
        logical_id = (pnode_uuid, snode_uuid,
                      constants.FIRST_DRBD_PORT + disk_id,
                      disk_id, disk_id, "mock_secret")
      if children is None:
        data_child = self.CreateDisk(dev_type=constants.DT_PLAIN,
                                     size=size)
        meta_child = self.CreateDisk(dev_type=constants.DT_PLAIN,
                                     size=constants.DRBD_META_SIZE)
        children = [data_child, meta_child]

      if nodes is None:
        nodes = [pnode_uuid, snode_uuid]
    elif dev_type == constants.DT_PLAIN:
      if logical_id is None:
        logical_id = ("mockvg", "mock_disk_%d" % disk_id)
      if nodes is None and primary_node is not None:
        nodes = [primary_node]
    elif dev_type in constants.DTS_FILEBASED:
      if logical_id is None:
        logical_id = (constants.FD_LOOP, "/file/storage/disk%d" % disk_id)
      if (nodes is None and primary_node is not None and
          dev_type == constants.DT_FILE):
        nodes = [primary_node]
    elif dev_type == constants.DT_BLOCK:
      if logical_id is None:
        logical_id = (constants.BLOCKDEV_DRIVER_MANUAL,
                      "/dev/disk/disk%d" % disk_id)
    elif dev_type == constants.DT_EXT:
      if logical_id is None:
        provider = params.get(constants.IDISK_PROVIDER, None)
        if provider is None:
          raise AssertionError("You must specify a 'provider' for 'ext' disks")
        logical_id = (provider, "mock_disk_%d" % disk_id)
    elif logical_id is None:
      raise NotImplementedError
    if children is None:
      children = []
    if nodes is None:
      nodes = []
    if iv_name is None:
      iv_name = "disk/%d" % instance_disk_index

    return objects.Disk(uuid=uuid,
                        name=name,
                        dev_type=dev_type,
                        logical_id=logical_id,
                        children=children,
                        nodes=nodes,
                        iv_name=iv_name,
                        size=size,
                        mode=mode,
                        params=params,
                        spindles=spindles)

  def GetDefaultOs(self):
    if self._default_os is None:
      self._default_os = self.CreateOs(name="mocked_os")
    return self._default_os

  def CreateOs(self,
               name=None,
               path=None,
               api_versions=None,
               create_script=None,
               export_script=None,
               import_script=None,
               rename_script=None,
               verify_script=None,
               supported_variants=None,
               supported_parameters=None):
    """Create a new L{objects.OS} object

    @rtype: L{object.OS}
    @return: the newly create OS objects

    """
    os_id = self._cur_os_id
    self._cur_os_id += 1

    if name is None:
      name = "mock_os_%d" % os_id
    if path is None:
      path = "/mocked/path/%d" % os_id
    if api_versions is None:
      api_versions = [constants.OS_API_V20]
    if create_script is None:
      create_script = "mock_create.sh"
    if export_script is None:
      export_script = "mock_export.sh"
    if import_script is None:
      import_script = "mock_import.sh"
    if rename_script is None:
      rename_script = "mock_rename.sh"
    if verify_script is None:
      verify_script = "mock_verify.sh"
    if supported_variants is None:
      supported_variants = ["default"]
    if supported_parameters is None:
      supported_parameters = ["mock_param"]

    return objects.OS(name=name,
                      path=path,
                      api_versions=api_versions,
                      create_script=create_script,
                      export_script=export_script,
                      import_script=import_script,
                      rename_script=rename_script,
                      verify_script=verify_script,
                      supported_variants=supported_variants,
                      supported_parameters=supported_parameters)

  def CreateNic(self,
                uuid=None,
                name=None,
                mac=None,
                ip=None,
                network=None,
                nicparams=None,
                netinfo=None):
    """Create a new L{objecs.NIC} object

    @rtype: L{objects.NIC}
    @return: the newly create NIC object

    """
    nic_id = self._cur_nic_id
    self._cur_nic_id += 1

    if uuid is None:
      uuid = self._GetUuid()
    if name is None:
      name = "mock_nic_%d" % nic_id
    if mac is None:
      mac = "aa:00:00:aa:%02x:%02x" % (nic_id // 0xff, nic_id % 0xff)
    if isinstance(network, objects.Network):
      if ip:
        pool = AddressPool(network)
        pool.Reserve(ip)
      network = network.uuid
    if nicparams is None:
      nicparams = {}

    return objects.NIC(uuid=uuid,
                       name=name,
                       mac=mac,
                       ip=ip,
                       network=network,
                       nicparams=nicparams,
                       netinfo=netinfo)

  def SetEnabledDiskTemplates(self, enabled_disk_templates):
    """Set the enabled disk templates in the cluster.

    This also takes care of required IPolicy updates.

    @type enabled_disk_templates: list of string
    @param enabled_disk_templates: list of disk templates to enable

    """
    cluster = self.GetClusterInfo()
    cluster.enabled_disk_templates = list(enabled_disk_templates)
    cluster.ipolicy[constants.IPOLICY_DTS] = list(enabled_disk_templates)

  def ComputeDRBDMap(self):
    return dict((node_uuid, {}) for node_uuid in self._ConfigData().nodes)

  def AllocateDRBDMinor(self, node_uuids, disk_uuid):
    return [0] * len(node_uuids)

  def ReleaseDRBDMinors(self, disk_uuid):
    pass

  def SetIPolicyField(self, category, field, value):
    """Set a value of a desired ipolicy field.

    @type category: one of L{constants.ISPECS_MAX}, L{constants.ISPECS_MIN},
      L{constants.ISPECS_STD}
    @param category: Whether to change the default value, or the upper or lower
      bound.
    @type field: string
    @param field: The field to change.
    @type value: any
    @param value: The value to assign.

    """
    if category not in [constants.ISPECS_MAX, constants.ISPECS_MIN,
                        constants.ISPECS_STD]:
      raise ValueError("Invalid ipolicy category %s" % category)

    ipolicy_dict = self.GetClusterInfo().ipolicy[constants.ISPECS_MINMAX][0]
    ipolicy_dict[category][field] = value

  def _CreateConfig(self):
    self._config_data = objects.ConfigData(
      version=constants.CONFIG_VERSION,
      cluster=None,
      nodegroups={},
      nodes={},
      instances={},
      networks={},
      disks={})

    master_node_uuid = self._GetUuid()

    self._cluster = objects.Cluster(
      serial_no=1,
      rsahostkeypub="",
      highest_used_port=(constants.FIRST_DRBD_PORT - 1),
      tcpudp_port_pool=set(),
      mac_prefix="aa:00:00",
      volume_group_name="xenvg",
      reserved_lvs=None,
      drbd_usermode_helper="/bin/true",
      master_node=master_node_uuid,
      master_ip="192.0.2.254",
      master_netdev=constants.DEFAULT_BRIDGE,
      master_netmask=None,
      use_external_mip_script=None,
      cluster_name="cluster.example.com",
      file_storage_dir="/tmp",
      shared_file_storage_dir=None,
      enabled_hypervisors=[constants.HT_XEN_HVM, constants.HT_XEN_PVM,
                           constants.HT_KVM],
      hvparams=constants.HVC_DEFAULTS.copy(),
      ipolicy=None,
      os_hvp={self.GetDefaultOs().name: constants.HVC_DEFAULTS.copy()},
      beparams=None,
      osparams=None,
      osparams_private_cluster=None,
      nicparams={constants.PP_DEFAULT: constants.NICC_DEFAULTS},
      ndparams=None,
      diskparams=None,
      candidate_pool_size=3,
      modify_etc_hosts=False,
      modify_ssh_setup=False,
      maintain_node_health=False,
      uid_pool=None,
      default_iallocator="mock_iallocator",
      hidden_os=None,
      blacklisted_os=None,
      primary_ip_family=None,
      prealloc_wipe_disks=None,
      enabled_disk_templates=list(constants.DISK_TEMPLATE_PREFERENCE),
      )
    self._cluster.ctime = self._cluster.mtime = time.time()
    self._cluster.UpgradeConfig()
    self._ConfigData().cluster = self._cluster

    self._default_group = self.AddNewNodeGroup(name="default")
    self._master_node = self.AddNewNode(uuid=master_node_uuid)

  def _OpenConfig(self, _accept_foreign, force=False):
    self._config_data = self._mocked_config_store

  def _WriteConfig(self, destination=None, releaselock=False):
    self._mocked_config_store = self._ConfigData()

  def _GetRpc(self, _address_list):
    raise AssertionError("This should not be used during tests!")

  def _UnlockedGetNetworkMACPrefix(self, net_uuid):
    """Return the network mac prefix if it exists or the cluster level default.

    """
    prefix = None
    if net_uuid:
      nobj = self._UnlockedGetNetwork(net_uuid)
      if nobj.mac_prefix:
        prefix = nobj.mac_prefix

    return prefix

  def _GenerateOneMAC(self, prefix=None):
    """Return a function that randomly generates a MAC suffic
       and appends it to the given prefix. If prefix is not given get
       the cluster level default.

    """
    if not prefix:
      prefix = self._ConfigData().cluster.mac_prefix

    def GenMac():
      byte1 = random.randrange(0, 256)
      byte2 = random.randrange(0, 256)
      byte3 = random.randrange(0, 256)
      mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
      return mac

    return GenMac

  def GenerateMAC(self, net_uuid, ec_id):
    """Generate a MAC for an instance.

    This should check the current instances for duplicates.

    """
    existing = self._AllMACs()
    prefix = self._UnlockedGetNetworkMACPrefix(net_uuid)
    gen_mac = self._GenerateOneMAC(prefix)
    return self._temporary_macs.Generate(existing, gen_mac, ec_id)

  def ReserveMAC(self, mac, ec_id):
    """Reserve a MAC for an instance.

    This only checks instances managed by this cluster, it does not
    check for potential collisions elsewhere.

    """
    all_macs = self._AllMACs()
    if mac in all_macs:
      raise errors.ReservationError("mac already in use")
    else:
      self._temporary_macs.Reserve(ec_id, mac)

  def GenerateDRBDSecret(self, ec_id):
    """Generate a DRBD secret.

    This checks the current disks for duplicates.

    """
    return self._temporary_secrets.Generate(self._AllDRBDSecrets(),
                                            utils.GenerateSecret,
                                            ec_id)

  def ReserveLV(self, lv_name, ec_id):
    """Reserve an VG/LV pair for an instance.

    @type lv_name: string
    @param lv_name: the logical volume name to reserve

    """
    all_lvs = self._AllLVs()
    if lv_name in all_lvs:
      raise errors.ReservationError("LV already in use")
    else:
      self._temporary_lvs.Reserve(ec_id, lv_name)

  def _UnlockedCommitTemporaryIps(self, ec_id):
    """Commit all reserved IP address to their respective pools

    """
    for action, address, net_uuid in self._temporary_ips.GetECReserved(ec_id):
      self._UnlockedCommitIp(action, net_uuid, address)

  def _UnlockedCommitIp(self, action, net_uuid, address):
    """Commit a reserved IP address to an IP pool.

    The IP address is taken from the network's IP pool and marked as reserved.

    """
    nobj = self._UnlockedGetNetwork(net_uuid)
    pool = AddressPool(nobj)
    if action == RESERVE_ACTION:
      pool.Reserve(address)
    elif action == RELEASE_ACTION:
      pool.Release(address)

  def _UnlockedReleaseIp(self, net_uuid, address, ec_id):
    """Give a specific IP address back to an IP pool.

    The IP address is returned to the IP pool designated by pool_id and marked
    as reserved.

    """
    self._temporary_ips.Reserve(ec_id,
                                (RELEASE_ACTION, address, net_uuid))

  def ReleaseIp(self, net_uuid, address, ec_id):
    """Give a specified IP address back to an IP pool.

    This is just a wrapper around _UnlockedReleaseIp.

    """
    if net_uuid:
      self._UnlockedReleaseIp(net_uuid, address, ec_id)

  def GenerateIp(self, net_uuid, ec_id):
    """Find a free IPv4 address for an instance.

    """
    nobj = self._UnlockedGetNetwork(net_uuid)
    pool = AddressPool(nobj)

    def gen_one():
      try:
        ip = pool.GenerateFree()
      except errors.AddressPoolError:
        raise errors.ReservationError("Cannot generate IP. Network is full")
      return (RESERVE_ACTION, ip, net_uuid)

    _, address, _ = self._temporary_ips.Generate([], gen_one, ec_id)
    return address

  def _UnlockedReserveIp(self, net_uuid, address, ec_id, check=True):
    """Reserve a given IPv4 address for use by an instance.

    """
    nobj = self._UnlockedGetNetwork(net_uuid)
    pool = AddressPool(nobj)
    try:
      isreserved = pool.IsReserved(address)
      isextreserved = pool.IsReserved(address, external=True)
    except errors.AddressPoolError:
      raise errors.ReservationError("IP address not in network")
    if isreserved:
      raise errors.ReservationError("IP address already in use")
    if check and isextreserved:
      raise errors.ReservationError("IP is externally reserved")
    return self._temporary_ips.Reserve(ec_id,
                                       (RESERVE_ACTION,
                                        address, net_uuid))

  def ReserveIp(self, net_uuid, address, ec_id, check=True):
    """Reserve a given IPv4 address for use by an instance.

    """
    if net_uuid:
      return self._UnlockedReserveIp(net_uuid, address, ec_id, check)

  def AddInstance(self, instance, ec_id, replace=False):
    """Add an instance to the config.

    """
    instance.serial_no = 1
    instance.ctime = instance.mtime = time.time()
    self._ConfigData().instances[instance.uuid] = instance
    self._ConfigData().cluster.serial_no += 1 # pylint: disable=E1103
    self.ReleaseDRBDMinors(instance.uuid)
    self._UnlockedCommitTemporaryIps(ec_id)

  def _UnlockedAddDisk(self, disk):
    disk.UpgradeConfig()
    self._ConfigData().disks[disk.uuid] = disk
    self._ConfigData().cluster.serial_no += 1 # pylint: disable=E1103
    self.ReleaseDRBDMinors(disk.uuid)

  def _UnlockedAttachInstanceDisk(self, inst_uuid, disk_uuid, idx=None):
    instance = self._UnlockedGetInstanceInfo(inst_uuid)
    if idx is None:
      idx = len(instance.disks)
    instance.disks.insert(idx, disk_uuid)
    instance_disks = self._UnlockedGetInstanceDisks(inst_uuid)
    for (disk_idx, disk) in enumerate(instance_disks[idx:]):
      disk.iv_name = "disk/%s" % (idx + disk_idx)
    instance.serial_no += 1
    instance.mtime = time.time()

  def AddInstanceDisk(self, inst_uuid, disk, idx=None, replace=False):
    self._UnlockedAddDisk(disk)
    self._UnlockedAttachInstanceDisk(inst_uuid, disk.uuid, idx)

  def AttachInstanceDisk(self, inst_uuid, disk_uuid, idx=None):
    self._UnlockedAttachInstanceDisk(inst_uuid, disk_uuid, idx)

  def GetDisk(self, disk_uuid):
    """Retrieves a disk object if present.

    """
    return self._ConfigData().disks[disk_uuid]

  def AllocatePort(self):
    return 1

  def Update(self, target, feedback_fn, ec_id=None):
    def replace_in(target, tdict):
      tdict[target.uuid] = target

    update_serial = False
    if isinstance(target, objects.Cluster):
      self._ConfigData().cluster = target
    elif isinstance(target, objects.Node):
      replace_in(target, self._ConfigData().nodes)
      update_serial = True
    elif isinstance(target, objects.Instance):
      replace_in(target, self._ConfigData().instances)
    elif isinstance(target, objects.NodeGroup):
      replace_in(target, self._ConfigData().nodegroups)
    elif isinstance(target, objects.Network):
      replace_in(target, self._ConfigData().networks)
    elif isinstance(target, objects.Disk):
      replace_in(target, self._ConfigData().disks)

    target.serial_no += 1
    target.mtime = now = time.time()

    if update_serial:
      self._ConfigData().cluster.serial_no += 1 # pylint: disable=E1103
      self._ConfigData().cluster.mtime = now

  def SetInstancePrimaryNode(self, inst_uuid, target_node_uuid):
    self._UnlockedGetInstanceInfo(inst_uuid).primary_node = target_node_uuid

  def _SetInstanceStatus(self, inst_uuid, status,
                         disks_active, admin_state_source):
    if inst_uuid not in self._ConfigData().instances:
      raise errors.ConfigurationError("Unknown instance '%s'" %
                                      inst_uuid)
    instance = self._ConfigData().instances[inst_uuid]

    if status is None:
      status = instance.admin_state
    if disks_active is None:
      disks_active = instance.disks_active
    if admin_state_source is None:
      admin_state_source = instance.admin_state_source

    assert status in constants.ADMINST_ALL, \
           "Invalid status '%s' passed to SetInstanceStatus" % (status,)

    if instance.admin_state != status or \
       instance.disks_active != disks_active or \
       instance.admin_state_source != admin_state_source:
      instance.admin_state = status
      instance.disks_active = disks_active
      instance.admin_state_source = admin_state_source
      instance.serial_no += 1
      instance.mtime = time.time()
    return instance

  def _UnlockedDetachInstanceDisk(self, inst_uuid, disk_uuid):
    """Detach a disk from an instance.

    @type inst_uuid: string
    @param inst_uuid: The UUID of the instance object
    @type disk_uuid: string
    @param disk_uuid: The UUID of the disk object

    """
    instance = self._UnlockedGetInstanceInfo(inst_uuid)
    if instance is None:
      raise errors.ConfigurationError("Instance %s doesn't exist"
                                      % inst_uuid)
    if disk_uuid not in self._ConfigData().disks:
      raise errors.ConfigurationError("Disk %s doesn't exist" % disk_uuid)

    # Check if disk is attached to the instance
    if disk_uuid not in instance.disks:
      raise errors.ProgrammerError("Disk %s is not attached to an instance"
                                   % disk_uuid)

    idx = instance.disks.index(disk_uuid)
    instance.disks.remove(disk_uuid)
    instance_disks = self._UnlockedGetInstanceDisks(inst_uuid)
    _UpdateIvNames(idx, instance_disks[idx:])
    instance.serial_no += 1
    instance.mtime = time.time()

  def DetachInstanceDisk(self, inst_uuid, disk_uuid):
    self._UnlockedDetachInstanceDisk(inst_uuid, disk_uuid)

  def RemoveInstanceDisk(self, inst_uuid, disk_uuid):
    self._UnlockedDetachInstanceDisk(inst_uuid, disk_uuid)
    self._UnlockedRemoveDisk(disk_uuid)

  def RemoveInstance(self, inst_uuid):
    del self._ConfigData().instances[inst_uuid]

  def AddTcpUdpPort(self, port):
    self._ConfigData().cluster.tcpudp_port_pool.add(port)


class ConfigObjectMatcher(object):
  """Wraps a ConfigObject to provide __eq__

  """
  def __init__(self, obj):
    if not isinstance(obj, objects.ConfigObject):
      raise TypeError

    self.obj = obj

  def __eq__(self, other):
    if not isinstance(other, objects.ConfigObject):
      return False

    return self.obj.ToDict() == other.ToDict()
