-#!/usr/bin/python
+#
#
# Copyright (C) 2006, 2007 Google Inc.
"""Configuration management for Ganeti
-This module provides the interface to the ganeti cluster configuration.
-
+This module provides the interface to the Ganeti cluster configuration.
-The configuration data is stored on every node but is updated on the
-master only. After each update, the master distributes the data to the
-other nodes.
+The configuration data is stored on every node but is updated on the master
+only. After each update, the master distributes the data to the other nodes.
-Currently the data storage format is pickle as yaml was initially not
-available, then we used it but it was a memory-eating slow beast, so
-we reverted to pickle using custom Unpicklers.
+Currently, the data storage format is JSON. YAML was slow and consuming too
+much memory.
"""
import os
-import socket
import tempfile
import random
class ConfigWriter:
- """The interface to the cluster configuration"""
+ """The interface to the cluster configuration.
+ """
def __init__(self, cfg_file=None, offline=False):
+ self.write_count = 0
self._config_data = None
self._config_time = None
self._config_size = None
self._cfg_file = constants.CLUSTER_CONF_FILE
else:
self._cfg_file = cfg_file
+ self._temporary_ids = set()
+ # 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
+ # file than after it was modified
+ self._my_hostname = utils.HostInfo().name
# this method needs to be static, so that we can call it on the class
@staticmethod
break
retries -= 1
else:
- raise errors.ConfigurationError, ("Can't generate unique MAC")
+ raise errors.ConfigurationError("Can't generate unique MAC")
return mac
+ def _ComputeAllLVs(self):
+ """Compute the list of all LVs.
+
+ """
+ self._OpenConfig()
+ self._ReleaseLock()
+ lvnames = set()
+ for instance in self._config_data.instances.values():
+ node_data = instance.MapLVsByNode()
+ for lv_list in node_data.values():
+ lvnames.update(lv_list)
+ return lvnames
+
+ def GenerateUniqueID(self, exceptions=None):
+ """Generate an unique disk name.
+
+ This checks the current node, instances and disk names for
+ duplicates.
+
+ Args:
+ - exceptions: a list with some other names which should be checked
+ for uniqueness (used for example when you want to get
+ more than one id at one time without adding each one in
+ turn to the config file
+
+ Returns: the unique id as a string
+
+ """
+ existing = set()
+ existing.update(self._temporary_ids)
+ existing.update(self._ComputeAllLVs())
+ existing.update(self._config_data.instances.keys())
+ existing.update(self._config_data.nodes.keys())
+ if exceptions is not None:
+ existing.update(exceptions)
+ retries = 64
+ while retries > 0:
+ unique_id = utils.GetUUID()
+ if unique_id not in existing and unique_id is not None:
+ break
+ else:
+ raise errors.ConfigurationError("Not able generate an unique ID"
+ " (last tried ID: %s" % unique_id)
+ self._temporary_ids.add(unique_id)
+ return unique_id
+
def _AllMACs(self):
"""Return all MACs present in the config.
seen_macs.append(nic.mac)
return result
-
def SetDiskID(self, disk, node_name):
"""Convert the unique ID to the ID needed on the target nodes.
if disk.dev_type == "drbd":
pnode, snode, port = disk.logical_id
if node_name not in (pnode, snode):
- raise errors.ConfigurationError, ("DRBD device not knowing node %s" %
- node_name)
+ raise errors.ConfigurationError("DRBD device not knowing node %s" %
+ node_name)
pnode_info = self.GetNodeInfo(pnode)
snode_info = self.GetNodeInfo(snode)
if pnode_info is None or snode_info is None:
disk.physical_id = disk.logical_id
return
- def AddTcpIpPort(self, port):
+ def AddTcpUdpPort(self, port):
+ """Adds a new port to the available port pool.
+
+ """
if not isinstance(port, int):
raise errors.ProgrammerError("Invalid type passed for port")
self._OpenConfig()
- self._config_data.tcpudp_port_pool.add(port)
+ self._config_data.cluster.tcpudp_port_pool.add(port)
self._WriteConfig()
- def GetPortList():
+ def GetPortList(self):
"""Returns a copy of the current port list.
"""
self._OpenConfig()
self._ReleaseLock()
- return self._config_data.tcpudp_port_pool.copy()
+ return self._config_data.cluster.tcpudp_port_pool.copy()
def AllocatePort(self):
"""Allocate a port.
- The port will be recorded in the cluster config.
+ The port will be taken from the available port pool or from the
+ default port range (and in this case we increase
+ highest_used_port).
"""
self._OpenConfig()
# If there are TCP/IP ports configured, we use them first.
- if self._config_data.tcpudp_port_pool:
- port = self._config_data.tcpudp_port_pool.pop()
+ if self._config_data.cluster.tcpudp_port_pool:
+ port = self._config_data.cluster.tcpudp_port_pool.pop()
else:
port = self._config_data.cluster.highest_used_port + 1
if port >= constants.LAST_DRBD_PORT:
- raise errors.ConfigurationError, ("The highest used port is greater"
- " than %s. Aborting." %
- constants.LAST_DRBD_PORT)
+ raise errors.ConfigurationError("The highest used port is greater"
+ " than %s. Aborting." %
+ constants.LAST_DRBD_PORT)
self._config_data.cluster.highest_used_port = port
self._WriteConfig()
if not isinstance(instance, objects.Instance):
raise errors.ProgrammerError("Invalid type passed to AddInstance")
+ if instance.disk_template != constants.DT_DISKLESS:
+ all_lvs = instance.MapLVsByNode()
+ logger.Info("Instance '%s' DISK_LAYOUT: %s" % (instance.name, all_lvs))
+
self._OpenConfig()
self._config_data.instances[instance.name] = instance
self._WriteConfig()
self._OpenConfig()
if instance_name not in self._config_data.instances:
- raise errors.ConfigurationError, ("Unknown instance '%s'" %
- instance_name)
+ raise errors.ConfigurationError("Unknown instance '%s'" %
+ instance_name)
instance = self._config_data.instances[instance_name]
instance.status = "up"
self._WriteConfig()
self._OpenConfig()
if instance_name not in self._config_data.instances:
- raise errors.ConfigurationError, ("Unknown instance '%s'" %
- instance_name)
+ raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
del self._config_data.instances[instance_name]
self._WriteConfig()
+ def RenameInstance(self, old_name, new_name):
+ """Rename an instance.
+
+ This needs to be done in ConfigWriter and not by RemoveInstance
+ combined with AddInstance as only we can guarantee an atomic
+ rename.
+
+ """
+ self._OpenConfig()
+ if old_name not in self._config_data.instances:
+ raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
+ inst = self._config_data.instances[old_name]
+ del self._config_data.instances[old_name]
+ inst.name = new_name
+ self._config_data.instances[inst.name] = inst
+ self._WriteConfig()
+
def MarkInstanceDown(self, instance_name):
"""Mark the status of an instance to down in the configuration.
"""
-
self._OpenConfig()
if instance_name not in self._config_data.instances:
- raise errors.ConfigurationError, ("Unknown instance '%s'" %
- instance_name)
+ raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
instance = self._config_data.instances[instance_name]
instance.status = "down"
self._WriteConfig()
"""
self._OpenConfig()
if node_name not in self._config_data.nodes:
- raise errors.ConfigurationError, ("Unknown node '%s'" % node_name)
+ raise errors.ConfigurationError("Unknown node '%s'" % node_name)
del self._config_data.nodes[node_name]
self._WriteConfig()
try:
st = os.stat(self._cfg_file)
except OSError, err:
- raise errors.ConfigurationError, "Can't stat config file: %s" % err
+ raise errors.ConfigurationError("Can't stat config file: %s" % err)
if (self._config_data is not None and
self._config_time is not None and
self._config_time == st.st_mtime and
f = open(self._cfg_file, 'r')
try:
try:
- data = objects.ConfigObject.Load(f)
+ data = objects.ConfigData.Load(f)
except Exception, err:
- raise errors.ConfigurationError, err
+ raise errors.ConfigurationError(err)
finally:
f.close()
if (not hasattr(data, 'cluster') or
not hasattr(data.cluster, 'config_version')):
- raise errors.ConfigurationError, ("Incomplete configuration"
- " (missing cluster.config_version)")
+ 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))
+ raise errors.ConfigurationError("Cluster configuration version"
+ " mismatch, got %s instead of %s" %
+ (data.cluster.config_version,
+ constants.CONFIG_VERSION))
self._config_data = data
self._config_time = st.st_mtime
self._config_size = st.st_size
return True
bad = False
nodelist = self.GetNodeList()
- myhostname = socket.gethostname()
+ myhostname = self._my_hostname
tgt_list = []
for node in nodelist:
f.close()
# we don't need to do os.close(fd) as f.close() did it
os.rename(name, destination)
+ self.write_count += 1
+ # re-set our cache as not to re-read the config file
+ try:
+ st = os.stat(destination)
+ except OSError, err:
+ raise errors.ConfigurationError("Can't stat config file: %s" % err)
+ self._config_time = st.st_mtime
+ self._config_size = st.st_size
+ self._config_inode = st.st_ino
+ # and redistribute the config file
self._DistributeConfig()
def InitConfig(self, node, primary_ip, secondary_ip,
- clustername, hostkeypub, mac_prefix, vg_name, def_bridge):
+ hostkeypub, mac_prefix, vg_name, def_bridge):
"""Create the initial cluster configuration.
It will contain the current node, which will also be the master
node: the nodename of the initial node
primary_ip: the IP address of the current host
secondary_ip: the secondary IP of the current host or None
- clustername: the name of the cluster
hostkeypub: the public hostkey of this host
"""
hu_port = constants.FIRST_DRBD_PORT - 1
globalconfig = objects.Cluster(config_version=constants.CONFIG_VERSION,
- serial_no=1, master_node=node,
- name=clustername,
+ serial_no=1,
rsahostkeypub=hostkeypub,
highest_used_port=hu_port,
mac_prefix=mac_prefix,
volume_group_name=vg_name,
- default_bridge=def_bridge)
+ default_bridge=def_bridge,
+ tcpudp_port_pool=set())
if secondary_ip is None:
secondary_ip = primary_ip
nodeconfig = objects.Node(name=node, primary_ip=primary_ip,
self._config_data = objects.ConfigData(nodes={node: nodeconfig},
instances={},
- cluster=globalconfig,
- tcpudp_port_pool=set())
+ cluster=globalconfig)
self._WriteConfig()
- def GetClusterName(self):
- """Return the cluster name.
-
- """
- self._OpenConfig()
- self._ReleaseLock()
- return self._config_data.cluster.name
-
def GetVGName(self):
"""Return the volume group name.
self._OpenConfig()
self._ReleaseLock()
return self._config_data.cluster.mac_prefix
+
+ def GetClusterInfo(self):
+ """Returns informations about the cluster
+
+ Returns:
+ the cluster object
+
+ """
+ self._OpenConfig()
+ self._ReleaseLock()
+
+ return self._config_data.cluster
+
+ def Update(self, target):
+ """Notify function to be called after updates.
+
+ This function must be called when an object (as returned by
+ GetInstanceInfo, GetNodeInfo, GetCluster) has been updated and the
+ caller wants the modifications saved to the backing store. Note
+ that all modified objects will be saved, but the target argument
+ is the one the caller wants to ensure that it's saved.
+
+ """
+ if self._config_data is None:
+ raise errors.ProgrammerError("Configuration file not read,"
+ " cannot save.")
+ if isinstance(target, objects.Cluster):
+ test = target == self._config_data.cluster
+ elif isinstance(target, objects.Node):
+ test = target in self._config_data.nodes.values()
+ elif isinstance(target, objects.Instance):
+ test = target in self._config_data.instances.values()
+ else:
+ raise errors.ProgrammerError("Invalid object type (%s) passed to"
+ " ConfigWriter.Update" % type(target))
+ if not test:
+ raise errors.ConfigurationError("Configuration updated since object"
+ " has been read or unknown object")
+ self._WriteConfig()