import os
import tempfile
import random
-import re
+import logging
from ganeti import errors
-from ganeti import logger
+from ganeti import locking
from ganeti import utils
from ganeti import constants
from ganeti import rpc
from ganeti import objects
+from ganeti import serializer
+from ganeti import ssconf
+
+
+_config_lock = locking.SharedLock()
+
+
+def ValidateConfig():
+ sstore = ssconf.SimpleStore()
+
+ if sstore.GetConfigVersion() != constants.CONFIG_VERSION:
+ raise errors.ConfigurationError("Cluster configuration version"
+ " mismatch, got %s instead of %s" %
+ (sstore.GetConfigVersion(),
+ constants.CONFIG_VERSION))
class ConfigWriter:
"""
def __init__(self, cfg_file=None, offline=False):
self.write_count = 0
+ self._lock = _config_lock
self._config_data = None
self._config_time = None
self._config_size = None
else:
self._cfg_file = cfg_file
self._temporary_ids = set()
+ self._temporary_drbds = {}
# Note: in order to prevent errors when resolving our name in
# _DistributeConfig, we compute it here once and reuse it; it's
# better to raise an error before starting to modify the config
"""
return os.path.exists(constants.CLUSTER_CONF_FILE)
+ @locking.ssynchronized(_config_lock, shared=1)
def GenerateMAC(self):
"""Generate a MAC for an instance.
"""
self._OpenConfig()
- self._ReleaseLock()
prefix = self._config_data.cluster.mac_prefix
all_macs = self._AllMACs()
retries = 64
raise errors.ConfigurationError("Can't generate unique MAC")
return mac
+ @locking.ssynchronized(_config_lock, shared=1)
def IsMacInUse(self, mac):
"""Predicate: check if the specified MAC is in use in the Ganeti cluster.
"""
self._OpenConfig()
- self._ReleaseLock()
all_macs = self._AllMACs()
return mac in all_macs
"""
self._OpenConfig()
- self._ReleaseLock()
lvnames = set()
for instance in self._config_data.instances.values():
node_data = instance.MapLVsByNode()
lvnames.update(lv_list)
return lvnames
+ @locking.ssynchronized(_config_lock, shared=1)
def GenerateUniqueID(self, exceptions=None):
"""Generate an unique disk name.
"""
self._OpenConfig()
- self._ReleaseLock()
result = []
for instance in self._config_data.instances.values():
return result
+ @locking.ssynchronized(_config_lock, shared=1)
def VerifyConfig(self):
"""Stub verify function.
"""
self._OpenConfig()
- self._ReleaseLock()
result = []
seen_macs = []
seen_macs.append(nic.mac)
return result
- def SetDiskID(self, disk, node_name):
+ def _UnlockedSetDiskID(self, disk, node_name):
"""Convert the unique ID to the ID needed on the target nodes.
This is used only for drbd, which needs ip/port configuration.
this helps when the only the top device is passed to the remote
node.
+ This function is for internal use, when the config lock is already held.
+
"""
if disk.children:
for child in disk.children:
- self.SetDiskID(child, node_name)
+ self._UnlockedSetDiskID(child, node_name)
if disk.logical_id is None and disk.physical_id is not None:
return
- if disk.dev_type in constants.LDS_DRBD:
- pnode, snode, port = disk.logical_id
+ if disk.dev_type == constants.LD_DRBD8:
+ pnode, snode, port, pminor, sminor = disk.logical_id
if node_name not in (pnode, snode):
raise errors.ConfigurationError("DRBD device not knowing node %s" %
node_name)
- pnode_info = self.GetNodeInfo(pnode)
- snode_info = self.GetNodeInfo(snode)
+ pnode_info = self._UnlockedGetNodeInfo(pnode)
+ snode_info = self._UnlockedGetNodeInfo(snode)
if pnode_info is None or snode_info is None:
raise errors.ConfigurationError("Can't find primary or secondary node"
" for %s" % str(disk))
+ p_data = (pnode_info.secondary_ip, port)
+ s_data = (snode_info.secondary_ip, port)
if pnode == node_name:
- disk.physical_id = (pnode_info.secondary_ip, port,
- snode_info.secondary_ip, port)
+ disk.physical_id = p_data + s_data + (pminor,)
else: # it must be secondary, we tested above
- disk.physical_id = (snode_info.secondary_ip, port,
- pnode_info.secondary_ip, port)
+ disk.physical_id = s_data + p_data + (sminor,)
else:
disk.physical_id = disk.logical_id
return
+ @locking.ssynchronized(_config_lock)
+ def SetDiskID(self, disk, node_name):
+ """Convert the unique ID to the ID needed on the target nodes.
+
+ This is used only for drbd, which needs ip/port configuration.
+
+ The routine descends down and updates its children also, because
+ this helps when the only the top device is passed to the remote
+ node.
+
+ """
+ return self._UnlockedSetDiskID(disk, node_name)
+
+ @locking.ssynchronized(_config_lock)
def AddTcpUdpPort(self, port):
"""Adds a new port to the available port pool.
self._config_data.cluster.tcpudp_port_pool.add(port)
self._WriteConfig()
+ @locking.ssynchronized(_config_lock, shared=1)
def GetPortList(self):
"""Returns a copy of the current port list.
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data.cluster.tcpudp_port_pool.copy()
+ @locking.ssynchronized(_config_lock)
def AllocatePort(self):
"""Allocate a port.
self._WriteConfig()
return port
+ def _ComputeDRBDMap(self, instance):
+ """Compute the used DRBD minor/nodes.
+
+ Return: dictionary of node_name: dict of minor: instance_name. The
+ returned dict will have all the nodes in it (even if with an empty
+ list).
+
+ """
+ def _AppendUsedPorts(instance_name, disk, used):
+ if disk.dev_type == constants.LD_DRBD8 and len(disk.logical_id) == 5:
+ nodeA, nodeB, dummy, minorA, minorB = disk.logical_id
+ for node, port in ((nodeA, minorA), (nodeB, minorB)):
+ assert node in used, "Instance node not found in node list"
+ if port in used[node]:
+ raise errors.ProgrammerError("DRBD minor already used:"
+ " %s/%s, %s/%s" %
+ (node, port, instance_name,
+ used[node][port]))
+
+ used[node][port] = instance_name
+ if disk.children:
+ for child in disk.children:
+ _AppendUsedPorts(instance_name, child, used)
+
+ my_dict = dict((node, {}) for node in self._config_data.nodes)
+ for (node, minor), instance in self._temporary_drbds.iteritems():
+ my_dict[node][minor] = instance
+ for instance in self._config_data.instances.itervalues():
+ for disk in instance.disks:
+ _AppendUsedPorts(instance.name, disk, my_dict)
+ return my_dict
+
+ @locking.ssynchronized(_config_lock)
+ def AllocateDRBDMinor(self, nodes, instance):
+ """Allocate a drbd minor.
+
+ The free minor will be automatically computed from the existing
+ devices. A node can be given multiple times in order to allocate
+ multiple minors. The result is the list of minors, in the same
+ order as the passed nodes.
+
+ """
+ self._OpenConfig()
+
+ d_map = self._ComputeDRBDMap(instance)
+ result = []
+ for nname in nodes:
+ ndata = d_map[nname]
+ if not ndata:
+ # no minors used, we can start at 0
+ result.append(0)
+ ndata[0] = instance
+ continue
+ keys = ndata.keys()
+ keys.sort()
+ ffree = utils.FirstFree(keys)
+ if ffree is None:
+ # return the next minor
+ # TODO: implement high-limit check
+ minor = keys[-1] + 1
+ else:
+ minor = ffree
+ result.append(minor)
+ ndata[minor] = instance
+ assert (nname, minor) not in self._temporary_drbds, \
+ "Attempt to reuse reserved DRBD minor"
+ self._temporary_drbds[(nname, minor)] = instance
+ logging.debug("Request to allocate drbd minors, input: %s, returning %s",
+ nodes, result)
+ return result
+
+ @locking.ssynchronized(_config_lock)
+ def ReleaseDRBDMinors(self, instance):
+ """Release temporary drbd minors allocated for a given instance.
+
+ This should be called on both the error paths and on the success
+ paths (after the instance has been added or updated).
+
+ @type instance: string
+ @param instance: the instance for which temporary minors should be
+ released
+
+ """
+ for key, name in self._temporary_drbds.items():
+ if name == instance:
+ del self._temporary_drbds[key]
+
+ @locking.ssynchronized(_config_lock, shared=1)
def GetHostKey(self):
"""Return the rsa hostkey from the config.
Returns: rsa hostkey
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data.cluster.rsahostkeypub
+ @locking.ssynchronized(_config_lock)
def AddInstance(self, instance):
"""Add an instance to the config.
if instance.disk_template != constants.DT_DISKLESS:
all_lvs = instance.MapLVsByNode()
- logger.Info("Instance '%s' DISK_LAYOUT: %s" % (instance.name, all_lvs))
+ logging.info("Instance '%s' DISK_LAYOUT: %s", instance.name, all_lvs)
self._OpenConfig()
+ instance.serial_no = 1
self._config_data.instances[instance.name] = instance
+ self._config_data.cluster.serial_no += 1
self._WriteConfig()
- def MarkInstanceUp(self, instance_name):
- """Mark the instance status to up in the config.
+ def _SetInstanceStatus(self, instance_name, status):
+ """Set the instance's status to a given value.
"""
+ if status not in ("up", "down"):
+ raise errors.ProgrammerError("Invalid status '%s' passed to"
+ " ConfigWriter._SetInstanceStatus()" %
+ status)
self._OpenConfig()
if instance_name not in self._config_data.instances:
raise errors.ConfigurationError("Unknown instance '%s'" %
instance_name)
instance = self._config_data.instances[instance_name]
- instance.status = "up"
- self._WriteConfig()
+ if instance.status != status:
+ instance.status = status
+ instance.serial_no += 1
+ self._WriteConfig()
+
+ @locking.ssynchronized(_config_lock)
+ def MarkInstanceUp(self, instance_name):
+ """Mark the instance status to up in the config.
+ """
+ self._SetInstanceStatus(instance_name, "up")
+
+ @locking.ssynchronized(_config_lock)
def RemoveInstance(self, instance_name):
"""Remove the instance from the configuration.
if instance_name not in self._config_data.instances:
raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
del self._config_data.instances[instance_name]
+ self._config_data.cluster.serial_no += 1
self._WriteConfig()
+ @locking.ssynchronized(_config_lock)
def RenameInstance(self, old_name, new_name):
"""Rename an instance.
disk.iv_name))
self._config_data.instances[inst.name] = inst
+ self._config_data.cluster.serial_no += 1
self._WriteConfig()
+ @locking.ssynchronized(_config_lock)
def MarkInstanceDown(self, instance_name):
"""Mark the status of an instance to down in the configuration.
"""
- self._OpenConfig()
+ self._SetInstanceStatus(instance_name, "down")
- if instance_name not in self._config_data.instances:
- raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
- instance = self._config_data.instances[instance_name]
- instance.status = "down"
- self._WriteConfig()
+ def _UnlockedGetInstanceList(self):
+ """Get the list of instances.
+
+ This function is for internal use, when the config lock is already held.
+
+ """
+ self._OpenConfig()
+ return self._config_data.instances.keys()
+ @locking.ssynchronized(_config_lock, shared=1)
def GetInstanceList(self):
"""Get the list of instances.
these contains all the instances, also the ones in Admin_down state
"""
- self._OpenConfig()
- self._ReleaseLock()
-
- return self._config_data.instances.keys()
+ return self._UnlockedGetInstanceList()
+ @locking.ssynchronized(_config_lock, shared=1)
def ExpandInstanceName(self, short_name):
"""Attempt to expand an incomplete instance name.
"""
self._OpenConfig()
- self._ReleaseLock()
return utils.MatchNameComponent(short_name,
self._config_data.instances.keys())
+ def _UnlockedGetInstanceInfo(self, instance_name):
+ """Returns informations about an instance.
+
+ This function is for internal use, when the config lock is already held.
+
+ """
+ self._OpenConfig()
+
+ if instance_name not in self._config_data.instances:
+ return None
+
+ return self._config_data.instances[instance_name]
+
+ @locking.ssynchronized(_config_lock, shared=1)
def GetInstanceInfo(self, instance_name):
"""Returns informations about an instance.
the instance object
"""
- self._OpenConfig()
- self._ReleaseLock()
+ return self._UnlockedGetInstanceInfo(instance_name)
- if instance_name not in self._config_data.instances:
- return None
+ @locking.ssynchronized(_config_lock, shared=1)
+ def GetAllInstancesInfo(self):
+ """Get the configuration of all instances.
- return self._config_data.instances[instance_name]
+ @rtype: dict
+ @returns: dict of (instance, instance_info), where instance_info is what
+ would GetInstanceInfo return for the node
+
+ """
+ my_dict = dict([(instance, self._UnlockedGetInstanceInfo(instance))
+ for instance in self._UnlockedGetInstanceList()])
+ return my_dict
+ @locking.ssynchronized(_config_lock)
def AddNode(self, node):
"""Add a node to the configuration.
node: an object.Node instance
"""
+ logging.info("Adding node %s to configuration" % node.name)
+
self._OpenConfig()
+ node.serial_no = 1
self._config_data.nodes[node.name] = node
+ self._config_data.cluster.serial_no += 1
self._WriteConfig()
+ @locking.ssynchronized(_config_lock)
def RemoveNode(self, node_name):
"""Remove a node from the configuration.
"""
+ logging.info("Removing node %s from configuration" % node_name)
+
self._OpenConfig()
if node_name not in self._config_data.nodes:
raise errors.ConfigurationError("Unknown node '%s'" % node_name)
del self._config_data.nodes[node_name]
+ self._config_data.cluster.serial_no += 1
self._WriteConfig()
+ @locking.ssynchronized(_config_lock, shared=1)
def ExpandNodeName(self, short_name):
"""Attempt to expand an incomplete instance name.
"""
self._OpenConfig()
- self._ReleaseLock()
return utils.MatchNameComponent(short_name,
self._config_data.nodes.keys())
- def GetNodeInfo(self, node_name):
+ def _UnlockedGetNodeInfo(self, node_name):
"""Get the configuration of a node, as stored in the config.
+ This function is for internal use, when the config lock is already held.
+
Args: node: nodename (tuple) of the node
Returns: the node object
"""
self._OpenConfig()
- self._ReleaseLock()
if node_name not in self._config_data.nodes:
return None
return self._config_data.nodes[node_name]
- def GetNodeList(self):
+
+ @locking.ssynchronized(_config_lock, shared=1)
+ def GetNodeInfo(self, node_name):
+ """Get the configuration of a node, as stored in the config.
+
+ Args: node: nodename (tuple) of the node
+
+ Returns: the node object
+
+ """
+ return self._UnlockedGetNodeInfo(node_name)
+
+ def _UnlockedGetNodeList(self):
"""Return the list of nodes which are in the configuration.
+ This function is for internal use, when the config lock is already held.
+
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data.nodes.keys()
+
+ @locking.ssynchronized(_config_lock, shared=1)
+ def GetNodeList(self):
+ """Return the list of nodes which are in the configuration.
+
+ """
+ return self._UnlockedGetNodeList()
+
+ @locking.ssynchronized(_config_lock, shared=1)
+ def GetAllNodesInfo(self):
+ """Get the configuration of all nodes.
+
+ @rtype: dict
+ @returns: dict of (node, node_info), where node_info is what
+ would GetNodeInfo return for the node
+
+ """
+ my_dict = dict([(node, self._UnlockedGetNodeInfo(node))
+ for node in self._UnlockedGetNodeList()])
+ return my_dict
+
+ @locking.ssynchronized(_config_lock, shared=1)
def DumpConfig(self):
"""Return the entire configuration of the cluster.
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data
def _BumpSerialNo(self):
"""Bump up the serial number of the config.
"""
- self._config_data.cluster.serial_no += 1
+ self._config_data.serial_no += 1
def _OpenConfig(self):
"""Read the config data from disk.
self._config_inode == st.st_ino):
# data is current, so skip loading of config file
return
+
+ # Make sure the configuration has the right version
+ ValidateConfig()
+
f = open(self._cfg_file, 'r')
try:
try:
- data = objects.ConfigData.Load(f)
+ data = objects.ConfigData.FromDict(serializer.Load(f.read()))
except Exception, err:
raise errors.ConfigurationError(err)
finally:
f.close()
if (not hasattr(data, 'cluster') or
- not hasattr(data.cluster, 'config_version')):
+ not hasattr(data.cluster, 'rsahostkeypub')):
raise errors.ConfigurationError("Incomplete configuration"
- " (missing cluster.config_version)")
- if data.cluster.config_version != constants.CONFIG_VERSION:
- raise errors.ConfigurationError("Cluster configuration version"
- " mismatch, got %s instead of %s" %
- (data.cluster.config_version,
- constants.CONFIG_VERSION))
+ " (missing cluster.rsahostkeypub)")
self._config_data = data
self._config_time = st.st_mtime
self._config_size = st.st_size
self._config_inode = st.st_ino
- def _ReleaseLock(self):
- """xxxx
- """
-
def _DistributeConfig(self):
"""Distribute the configuration to the other nodes.
if self._offline:
return True
bad = False
- nodelist = self.GetNodeList()
+ nodelist = self._UnlockedGetNodeList()
myhostname = self._my_hostname
- tgt_list = []
- for node in nodelist:
- nodeinfo = self.GetNodeInfo(node)
- if nodeinfo.name == myhostname:
- continue
- tgt_list.append(node)
+ try:
+ nodelist.remove(myhostname)
+ except ValueError:
+ pass
- result = rpc.call_upload_file(tgt_list, self._cfg_file)
- for node in tgt_list:
+ result = rpc.call_upload_file(nodelist, self._cfg_file)
+ for node in nodelist:
if not result[node]:
- logger.Error("copy of file %s to node %s failed" %
- (self._cfg_file, node))
+ logging.error("copy of file %s to node %s failed",
+ self._cfg_file, node)
bad = True
return not bad
if destination is None:
destination = self._cfg_file
self._BumpSerialNo()
+ txt = serializer.Dump(self._config_data.ToDict())
dir_name, file_name = os.path.split(destination)
fd, name = tempfile.mkstemp('.newconfig', file_name, dir_name)
f = os.fdopen(fd, 'w')
try:
- self._config_data.Dump(f)
+ f.write(txt)
os.fsync(f.fileno())
finally:
f.close()
# and redistribute the config file
self._DistributeConfig()
+ @locking.ssynchronized(_config_lock)
def InitConfig(self, node, primary_ip, secondary_ip,
hostkeypub, mac_prefix, vg_name, def_bridge):
"""Create the initial cluster configuration.
"""
hu_port = constants.FIRST_DRBD_PORT - 1
- globalconfig = objects.Cluster(config_version=constants.CONFIG_VERSION,
- serial_no=1,
+ globalconfig = objects.Cluster(serial_no=1,
rsahostkeypub=hostkeypub,
highest_used_port=hu_port,
mac_prefix=mac_prefix,
if secondary_ip is None:
secondary_ip = primary_ip
nodeconfig = objects.Node(name=node, primary_ip=primary_ip,
- secondary_ip=secondary_ip)
+ secondary_ip=secondary_ip, serial_no=1)
self._config_data = objects.ConfigData(nodes={node: nodeconfig},
instances={},
- cluster=globalconfig)
+ cluster=globalconfig,
+ serial_no=1)
self._WriteConfig()
+ @locking.ssynchronized(_config_lock, shared=1)
def GetVGName(self):
"""Return the volume group name.
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data.cluster.volume_group_name
+ @locking.ssynchronized(_config_lock)
def SetVGName(self, vg_name):
"""Set the volume group name.
"""
self._OpenConfig()
self._config_data.cluster.volume_group_name = vg_name
+ self._config_data.cluster.serial_no += 1
self._WriteConfig()
+ @locking.ssynchronized(_config_lock, shared=1)
def GetDefBridge(self):
"""Return the default bridge.
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data.cluster.default_bridge
+ @locking.ssynchronized(_config_lock, shared=1)
def GetMACPrefix(self):
"""Return the mac prefix.
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data.cluster.mac_prefix
+ @locking.ssynchronized(_config_lock, shared=1)
def GetClusterInfo(self):
"""Returns informations about the cluster
"""
self._OpenConfig()
- self._ReleaseLock()
return self._config_data.cluster
+ @locking.ssynchronized(_config_lock)
def Update(self, target):
"""Notify function to be called after updates.
if not test:
raise errors.ConfigurationError("Configuration updated since object"
" has been read or unknown object")
+ target.serial_no += 1
+
self._WriteConfig()