Move instantiation of config into bootstrap.py
[ganeti-local] / lib / config.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Configuration management for Ganeti
23
24 This module provides the interface to the Ganeti cluster configuration.
25
26 The configuration data is stored on every node but is updated on the master
27 only. After each update, the master distributes the data to the other nodes.
28
29 Currently, the data storage format is JSON. YAML was slow and consuming too
30 much memory.
31
32 """
33
34 import os
35 import tempfile
36 import random
37 import logging
38
39 from ganeti import errors
40 from ganeti import locking
41 from ganeti import utils
42 from ganeti import constants
43 from ganeti import rpc
44 from ganeti import objects
45 from ganeti import serializer
46 from ganeti import ssconf
47
48
49 _config_lock = locking.SharedLock()
50
51
52 def ValidateConfig():
53   sstore = ssconf.SimpleStore()
54
55   if sstore.GetConfigVersion() != constants.CONFIG_VERSION:
56     raise errors.ConfigurationError("Cluster configuration version"
57                                     " mismatch, got %s instead of %s" %
58                                     (sstore.GetConfigVersion(),
59                                      constants.CONFIG_VERSION))
60
61
62 class ConfigWriter:
63   """The interface to the cluster configuration.
64
65   """
66   def __init__(self, cfg_file=None, offline=False):
67     self.write_count = 0
68     self._lock = _config_lock
69     self._config_data = None
70     self._config_time = None
71     self._config_size = None
72     self._config_inode = None
73     self._offline = offline
74     if cfg_file is None:
75       self._cfg_file = constants.CLUSTER_CONF_FILE
76     else:
77       self._cfg_file = cfg_file
78     self._temporary_ids = set()
79     self._temporary_drbds = {}
80     # Note: in order to prevent errors when resolving our name in
81     # _DistributeConfig, we compute it here once and reuse it; it's
82     # better to raise an error before starting to modify the config
83     # file than after it was modified
84     self._my_hostname = utils.HostInfo().name
85
86   # this method needs to be static, so that we can call it on the class
87   @staticmethod
88   def IsCluster():
89     """Check if the cluster is configured.
90
91     """
92     return os.path.exists(constants.CLUSTER_CONF_FILE)
93
94   @locking.ssynchronized(_config_lock, shared=1)
95   def GenerateMAC(self):
96     """Generate a MAC for an instance.
97
98     This should check the current instances for duplicates.
99
100     """
101     self._OpenConfig()
102     prefix = self._config_data.cluster.mac_prefix
103     all_macs = self._AllMACs()
104     retries = 64
105     while retries > 0:
106       byte1 = random.randrange(0, 256)
107       byte2 = random.randrange(0, 256)
108       byte3 = random.randrange(0, 256)
109       mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
110       if mac not in all_macs:
111         break
112       retries -= 1
113     else:
114       raise errors.ConfigurationError("Can't generate unique MAC")
115     return mac
116
117   @locking.ssynchronized(_config_lock, shared=1)
118   def IsMacInUse(self, mac):
119     """Predicate: check if the specified MAC is in use in the Ganeti cluster.
120
121     This only checks instances managed by this cluster, it does not
122     check for potential collisions elsewhere.
123
124     """
125     self._OpenConfig()
126     all_macs = self._AllMACs()
127     return mac in all_macs
128
129   @locking.ssynchronized(_config_lock, shared=1)
130   def GenerateDRBDSecret(self):
131     """Generate a DRBD secret.
132
133     This checks the current disks for duplicates.
134
135     """
136     self._OpenConfig()
137     all_secrets = self._AllDRBDSecrets()
138     retries = 64
139     while retries > 0:
140       secret = utils.GenerateSecret()
141       if secret not in all_secrets:
142         break
143       retries -= 1
144     else:
145       raise errors.ConfigurationError("Can't generate unique DRBD secret")
146     return secret
147
148   def _ComputeAllLVs(self):
149     """Compute the list of all LVs.
150
151     """
152     self._OpenConfig()
153     lvnames = set()
154     for instance in self._config_data.instances.values():
155       node_data = instance.MapLVsByNode()
156       for lv_list in node_data.values():
157         lvnames.update(lv_list)
158     return lvnames
159
160   @locking.ssynchronized(_config_lock, shared=1)
161   def GenerateUniqueID(self, exceptions=None):
162     """Generate an unique disk name.
163
164     This checks the current node, instances and disk names for
165     duplicates.
166
167     Args:
168       - exceptions: a list with some other names which should be checked
169                     for uniqueness (used for example when you want to get
170                     more than one id at one time without adding each one in
171                     turn to the config file
172
173     Returns: the unique id as a string
174
175     """
176     existing = set()
177     existing.update(self._temporary_ids)
178     existing.update(self._ComputeAllLVs())
179     existing.update(self._config_data.instances.keys())
180     existing.update(self._config_data.nodes.keys())
181     if exceptions is not None:
182       existing.update(exceptions)
183     retries = 64
184     while retries > 0:
185       unique_id = utils.NewUUID()
186       if unique_id not in existing and unique_id is not None:
187         break
188     else:
189       raise errors.ConfigurationError("Not able generate an unique ID"
190                                       " (last tried ID: %s" % unique_id)
191     self._temporary_ids.add(unique_id)
192     return unique_id
193
194   def _AllMACs(self):
195     """Return all MACs present in the config.
196
197     """
198     self._OpenConfig()
199
200     result = []
201     for instance in self._config_data.instances.values():
202       for nic in instance.nics:
203         result.append(nic.mac)
204
205     return result
206
207   def _AllDRBDSecrets(self):
208     """Return all DRBD secrets present in the config.
209
210     """
211     def helper(disk, result):
212       """Recursively gather secrets from this disk."""
213       if disk.dev_type == constants.DT_DRBD8:
214         result.append(disk.logical_id[5])
215       if disk.children:
216         for child in disk.children:
217           helper(child, result)
218
219     result = []
220     for instance in self._config_data.instances.values():
221       for disk in instance.disks:
222         helper(disk, result)
223
224     return result
225
226   @locking.ssynchronized(_config_lock, shared=1)
227   def VerifyConfig(self):
228     """Stub verify function.
229     """
230     self._OpenConfig()
231
232     result = []
233     seen_macs = []
234     ports = {}
235     data = self._config_data
236     for instance_name in data.instances:
237       instance = data.instances[instance_name]
238       if instance.primary_node not in data.nodes:
239         result.append("instance '%s' has invalid primary node '%s'" %
240                       (instance_name, instance.primary_node))
241       for snode in instance.secondary_nodes:
242         if snode not in data.nodes:
243           result.append("instance '%s' has invalid secondary node '%s'" %
244                         (instance_name, snode))
245       for idx, nic in enumerate(instance.nics):
246         if nic.mac in seen_macs:
247           result.append("instance '%s' has NIC %d mac %s duplicate" %
248                         (instance_name, idx, nic.mac))
249         else:
250           seen_macs.append(nic.mac)
251
252       # gather the drbd ports for duplicate checks
253       for dsk in instance.disks:
254         if dsk.dev_type in constants.LDS_DRBD:
255           tcp_port = dsk.logical_id[2]
256           if tcp_port not in ports:
257             ports[tcp_port] = []
258           ports[tcp_port].append((instance.name, "drbd disk %s" % dsk.iv_name))
259       # gather network port reservation
260       net_port = getattr(instance, "network_port", None)
261       if net_port is not None:
262         if net_port not in ports:
263           ports[net_port] = []
264         ports[net_port].append((instance.name, "network port"))
265
266     # cluster-wide pool of free ports
267     for free_port in self._config_data.cluster.tcpudp_port_pool:
268       if free_port not in ports:
269         ports[free_port] = []
270       ports[free_port].append(("cluster", "port marked as free"))
271
272     # compute tcp/udp duplicate ports
273     keys = ports.keys()
274     keys.sort()
275     for pnum in keys:
276       pdata = ports[pnum]
277       if len(pdata) > 1:
278         txt = ", ".join(["%s/%s" % val for val in pdata])
279         result.append("tcp/udp port %s has duplicates: %s" % (pnum, txt))
280
281     # highest used tcp port check
282     if keys:
283       if keys[-1] > self._config_data.cluster.highest_used_port:
284         result.append("Highest used port mismatch, saved %s, computed %s" %
285                       (self._config_data.cluster.highest_used_port,
286                        keys[-1]))
287
288     return result
289
290   def _UnlockedSetDiskID(self, disk, node_name):
291     """Convert the unique ID to the ID needed on the target nodes.
292
293     This is used only for drbd, which needs ip/port configuration.
294
295     The routine descends down and updates its children also, because
296     this helps when the only the top device is passed to the remote
297     node.
298
299     This function is for internal use, when the config lock is already held.
300
301     """
302     if disk.children:
303       for child in disk.children:
304         self._UnlockedSetDiskID(child, node_name)
305
306     if disk.logical_id is None and disk.physical_id is not None:
307       return
308     if disk.dev_type == constants.LD_DRBD8:
309       pnode, snode, port, pminor, sminor, secret = disk.logical_id
310       if node_name not in (pnode, snode):
311         raise errors.ConfigurationError("DRBD device not knowing node %s" %
312                                         node_name)
313       pnode_info = self._UnlockedGetNodeInfo(pnode)
314       snode_info = self._UnlockedGetNodeInfo(snode)
315       if pnode_info is None or snode_info is None:
316         raise errors.ConfigurationError("Can't find primary or secondary node"
317                                         " for %s" % str(disk))
318       p_data = (pnode_info.secondary_ip, port)
319       s_data = (snode_info.secondary_ip, port)
320       if pnode == node_name:
321         disk.physical_id = p_data + s_data + (pminor, secret)
322       else: # it must be secondary, we tested above
323         disk.physical_id = s_data + p_data + (sminor, secret)
324     else:
325       disk.physical_id = disk.logical_id
326     return
327
328   @locking.ssynchronized(_config_lock)
329   def SetDiskID(self, disk, node_name):
330     """Convert the unique ID to the ID needed on the target nodes.
331
332     This is used only for drbd, which needs ip/port configuration.
333
334     The routine descends down and updates its children also, because
335     this helps when the only the top device is passed to the remote
336     node.
337
338     """
339     return self._UnlockedSetDiskID(disk, node_name)
340
341   @locking.ssynchronized(_config_lock)
342   def AddTcpUdpPort(self, port):
343     """Adds a new port to the available port pool.
344
345     """
346     if not isinstance(port, int):
347       raise errors.ProgrammerError("Invalid type passed for port")
348
349     self._OpenConfig()
350     self._config_data.cluster.tcpudp_port_pool.add(port)
351     self._WriteConfig()
352
353   @locking.ssynchronized(_config_lock, shared=1)
354   def GetPortList(self):
355     """Returns a copy of the current port list.
356
357     """
358     self._OpenConfig()
359     return self._config_data.cluster.tcpudp_port_pool.copy()
360
361   @locking.ssynchronized(_config_lock)
362   def AllocatePort(self):
363     """Allocate a port.
364
365     The port will be taken from the available port pool or from the
366     default port range (and in this case we increase
367     highest_used_port).
368
369     """
370     self._OpenConfig()
371
372     # If there are TCP/IP ports configured, we use them first.
373     if self._config_data.cluster.tcpudp_port_pool:
374       port = self._config_data.cluster.tcpudp_port_pool.pop()
375     else:
376       port = self._config_data.cluster.highest_used_port + 1
377       if port >= constants.LAST_DRBD_PORT:
378         raise errors.ConfigurationError("The highest used port is greater"
379                                         " than %s. Aborting." %
380                                         constants.LAST_DRBD_PORT)
381       self._config_data.cluster.highest_used_port = port
382
383     self._WriteConfig()
384     return port
385
386   def _ComputeDRBDMap(self, instance):
387     """Compute the used DRBD minor/nodes.
388
389     Return: dictionary of node_name: dict of minor: instance_name. The
390     returned dict will have all the nodes in it (even if with an empty
391     list).
392
393     """
394     def _AppendUsedPorts(instance_name, disk, used):
395       if disk.dev_type == constants.LD_DRBD8 and len(disk.logical_id) >= 5:
396         nodeA, nodeB, dummy, minorA, minorB = disk.logical_id[:5]
397         for node, port in ((nodeA, minorA), (nodeB, minorB)):
398           assert node in used, "Instance node not found in node list"
399           if port in used[node]:
400             raise errors.ProgrammerError("DRBD minor already used:"
401                                          " %s/%s, %s/%s" %
402                                          (node, port, instance_name,
403                                           used[node][port]))
404
405           used[node][port] = instance_name
406       if disk.children:
407         for child in disk.children:
408           _AppendUsedPorts(instance_name, child, used)
409
410     my_dict = dict((node, {}) for node in self._config_data.nodes)
411     for (node, minor), instance in self._temporary_drbds.iteritems():
412       my_dict[node][minor] = instance
413     for instance in self._config_data.instances.itervalues():
414       for disk in instance.disks:
415         _AppendUsedPorts(instance.name, disk, my_dict)
416     return my_dict
417
418   @locking.ssynchronized(_config_lock)
419   def AllocateDRBDMinor(self, nodes, instance):
420     """Allocate a drbd minor.
421
422     The free minor will be automatically computed from the existing
423     devices. A node can be given multiple times in order to allocate
424     multiple minors. The result is the list of minors, in the same
425     order as the passed nodes.
426
427     """
428     self._OpenConfig()
429
430     d_map = self._ComputeDRBDMap(instance)
431     result = []
432     for nname in nodes:
433       ndata = d_map[nname]
434       if not ndata:
435         # no minors used, we can start at 0
436         result.append(0)
437         ndata[0] = instance
438         self._temporary_drbds[(nname, 0)] = instance
439         continue
440       keys = ndata.keys()
441       keys.sort()
442       ffree = utils.FirstFree(keys)
443       if ffree is None:
444         # return the next minor
445         # TODO: implement high-limit check
446         minor = keys[-1] + 1
447       else:
448         minor = ffree
449       result.append(minor)
450       ndata[minor] = instance
451       assert (nname, minor) not in self._temporary_drbds, \
452              "Attempt to reuse reserved DRBD minor"
453       self._temporary_drbds[(nname, minor)] = instance
454     logging.debug("Request to allocate drbd minors, input: %s, returning %s",
455                   nodes, result)
456     return result
457
458   @locking.ssynchronized(_config_lock)
459   def ReleaseDRBDMinors(self, instance):
460     """Release temporary drbd minors allocated for a given instance.
461
462     This should be called on both the error paths and on the success
463     paths (after the instance has been added or updated).
464
465     @type instance: string
466     @param instance: the instance for which temporary minors should be
467                      released
468
469     """
470     for key, name in self._temporary_drbds.items():
471       if name == instance:
472         del self._temporary_drbds[key]
473
474   @locking.ssynchronized(_config_lock, shared=1)
475   def GetHostKey(self):
476     """Return the rsa hostkey from the config.
477
478     Args: None
479
480     Returns: rsa hostkey
481     """
482     self._OpenConfig()
483     return self._config_data.cluster.rsahostkeypub
484
485   @locking.ssynchronized(_config_lock)
486   def AddInstance(self, instance):
487     """Add an instance to the config.
488
489     This should be used after creating a new instance.
490
491     Args:
492       instance: the instance object
493     """
494     if not isinstance(instance, objects.Instance):
495       raise errors.ProgrammerError("Invalid type passed to AddInstance")
496
497     if instance.disk_template != constants.DT_DISKLESS:
498       all_lvs = instance.MapLVsByNode()
499       logging.info("Instance '%s' DISK_LAYOUT: %s", instance.name, all_lvs)
500
501     self._OpenConfig()
502     instance.serial_no = 1
503     self._config_data.instances[instance.name] = instance
504     self._config_data.cluster.serial_no += 1
505     self._WriteConfig()
506
507   def _SetInstanceStatus(self, instance_name, status):
508     """Set the instance's status to a given value.
509
510     """
511     if status not in ("up", "down"):
512       raise errors.ProgrammerError("Invalid status '%s' passed to"
513                                    " ConfigWriter._SetInstanceStatus()" %
514                                    status)
515     self._OpenConfig()
516
517     if instance_name not in self._config_data.instances:
518       raise errors.ConfigurationError("Unknown instance '%s'" %
519                                       instance_name)
520     instance = self._config_data.instances[instance_name]
521     if instance.status != status:
522       instance.status = status
523       instance.serial_no += 1
524       self._WriteConfig()
525
526   @locking.ssynchronized(_config_lock)
527   def MarkInstanceUp(self, instance_name):
528     """Mark the instance status to up in the config.
529
530     """
531     self._SetInstanceStatus(instance_name, "up")
532
533   @locking.ssynchronized(_config_lock)
534   def RemoveInstance(self, instance_name):
535     """Remove the instance from the configuration.
536
537     """
538     self._OpenConfig()
539
540     if instance_name not in self._config_data.instances:
541       raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
542     del self._config_data.instances[instance_name]
543     self._config_data.cluster.serial_no += 1
544     self._WriteConfig()
545
546   @locking.ssynchronized(_config_lock)
547   def RenameInstance(self, old_name, new_name):
548     """Rename an instance.
549
550     This needs to be done in ConfigWriter and not by RemoveInstance
551     combined with AddInstance as only we can guarantee an atomic
552     rename.
553
554     """
555     self._OpenConfig()
556     if old_name not in self._config_data.instances:
557       raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
558     inst = self._config_data.instances[old_name]
559     del self._config_data.instances[old_name]
560     inst.name = new_name
561
562     for disk in inst.disks:
563       if disk.dev_type == constants.LD_FILE:
564         # rename the file paths in logical and physical id
565         file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1]))
566         disk.physical_id = disk.logical_id = (disk.logical_id[0],
567                                               os.path.join(file_storage_dir,
568                                                            inst.name,
569                                                            disk.iv_name))
570
571     self._config_data.instances[inst.name] = inst
572     self._config_data.cluster.serial_no += 1
573     self._WriteConfig()
574
575   @locking.ssynchronized(_config_lock)
576   def MarkInstanceDown(self, instance_name):
577     """Mark the status of an instance to down in the configuration.
578
579     """
580     self._SetInstanceStatus(instance_name, "down")
581
582   def _UnlockedGetInstanceList(self):
583     """Get the list of instances.
584
585     This function is for internal use, when the config lock is already held.
586
587     """
588     self._OpenConfig()
589     return self._config_data.instances.keys()
590
591   @locking.ssynchronized(_config_lock, shared=1)
592   def GetInstanceList(self):
593     """Get the list of instances.
594
595     Returns:
596       array of instances, ex. ['instance2.example.com','instance1.example.com']
597       these contains all the instances, also the ones in Admin_down state
598
599     """
600     return self._UnlockedGetInstanceList()
601
602   @locking.ssynchronized(_config_lock, shared=1)
603   def ExpandInstanceName(self, short_name):
604     """Attempt to expand an incomplete instance name.
605
606     """
607     self._OpenConfig()
608
609     return utils.MatchNameComponent(short_name,
610                                     self._config_data.instances.keys())
611
612   def _UnlockedGetInstanceInfo(self, instance_name):
613     """Returns informations about an instance.
614
615     This function is for internal use, when the config lock is already held.
616
617     """
618     self._OpenConfig()
619
620     if instance_name not in self._config_data.instances:
621       return None
622
623     return self._config_data.instances[instance_name]
624
625   @locking.ssynchronized(_config_lock, shared=1)
626   def GetInstanceInfo(self, instance_name):
627     """Returns informations about an instance.
628
629     It takes the information from the configuration file. Other informations of
630     an instance are taken from the live systems.
631
632     Args:
633       instance: name of the instance, ex instance1.example.com
634
635     Returns:
636       the instance object
637
638     """
639     return self._UnlockedGetInstanceInfo(instance_name)
640
641   @locking.ssynchronized(_config_lock, shared=1)
642   def GetAllInstancesInfo(self):
643     """Get the configuration of all instances.
644
645     @rtype: dict
646     @returns: dict of (instance, instance_info), where instance_info is what
647               would GetInstanceInfo return for the node
648
649     """
650     my_dict = dict([(instance, self._UnlockedGetInstanceInfo(instance))
651                     for instance in self._UnlockedGetInstanceList()])
652     return my_dict
653
654   @locking.ssynchronized(_config_lock)
655   def AddNode(self, node):
656     """Add a node to the configuration.
657
658     Args:
659       node: an object.Node instance
660
661     """
662     logging.info("Adding node %s to configuration" % node.name)
663
664     self._OpenConfig()
665     node.serial_no = 1
666     self._config_data.nodes[node.name] = node
667     self._config_data.cluster.serial_no += 1
668     self._WriteConfig()
669
670   @locking.ssynchronized(_config_lock)
671   def RemoveNode(self, node_name):
672     """Remove a node from the configuration.
673
674     """
675     logging.info("Removing node %s from configuration" % node_name)
676
677     self._OpenConfig()
678     if node_name not in self._config_data.nodes:
679       raise errors.ConfigurationError("Unknown node '%s'" % node_name)
680
681     del self._config_data.nodes[node_name]
682     self._config_data.cluster.serial_no += 1
683     self._WriteConfig()
684
685   @locking.ssynchronized(_config_lock, shared=1)
686   def ExpandNodeName(self, short_name):
687     """Attempt to expand an incomplete instance name.
688
689     """
690     self._OpenConfig()
691
692     return utils.MatchNameComponent(short_name,
693                                     self._config_data.nodes.keys())
694
695   def _UnlockedGetNodeInfo(self, node_name):
696     """Get the configuration of a node, as stored in the config.
697
698     This function is for internal use, when the config lock is already held.
699
700     Args: node: nodename (tuple) of the node
701
702     Returns: the node object
703
704     """
705     self._OpenConfig()
706
707     if node_name not in self._config_data.nodes:
708       return None
709
710     return self._config_data.nodes[node_name]
711
712
713   @locking.ssynchronized(_config_lock, shared=1)
714   def GetNodeInfo(self, node_name):
715     """Get the configuration of a node, as stored in the config.
716
717     Args: node: nodename (tuple) of the node
718
719     Returns: the node object
720
721     """
722     return self._UnlockedGetNodeInfo(node_name)
723
724   def _UnlockedGetNodeList(self):
725     """Return the list of nodes which are in the configuration.
726
727     This function is for internal use, when the config lock is already held.
728
729     """
730     self._OpenConfig()
731     return self._config_data.nodes.keys()
732
733
734   @locking.ssynchronized(_config_lock, shared=1)
735   def GetNodeList(self):
736     """Return the list of nodes which are in the configuration.
737
738     """
739     return self._UnlockedGetNodeList()
740
741   @locking.ssynchronized(_config_lock, shared=1)
742   def GetAllNodesInfo(self):
743     """Get the configuration of all nodes.
744
745     @rtype: dict
746     @returns: dict of (node, node_info), where node_info is what
747               would GetNodeInfo return for the node
748
749     """
750     my_dict = dict([(node, self._UnlockedGetNodeInfo(node))
751                     for node in self._UnlockedGetNodeList()])
752     return my_dict
753
754   @locking.ssynchronized(_config_lock, shared=1)
755   def DumpConfig(self):
756     """Return the entire configuration of the cluster.
757     """
758     self._OpenConfig()
759     return self._config_data
760
761   def _BumpSerialNo(self):
762     """Bump up the serial number of the config.
763
764     """
765     self._config_data.serial_no += 1
766
767   def _OpenConfig(self):
768     """Read the config data from disk.
769
770     In case we already have configuration data and the config file has
771     the same mtime as when we read it, we skip the parsing of the
772     file, since de-serialisation could be slow.
773
774     """
775     try:
776       st = os.stat(self._cfg_file)
777     except OSError, err:
778       raise errors.ConfigurationError("Can't stat config file: %s" % err)
779     if (self._config_data is not None and
780         self._config_time is not None and
781         self._config_time == st.st_mtime and
782         self._config_size == st.st_size and
783         self._config_inode == st.st_ino):
784       # data is current, so skip loading of config file
785       return
786
787     # Make sure the configuration has the right version
788     ValidateConfig()
789
790     f = open(self._cfg_file, 'r')
791     try:
792       try:
793         data = objects.ConfigData.FromDict(serializer.Load(f.read()))
794       except Exception, err:
795         raise errors.ConfigurationError(err)
796     finally:
797       f.close()
798     if (not hasattr(data, 'cluster') or
799         not hasattr(data.cluster, 'rsahostkeypub')):
800       raise errors.ConfigurationError("Incomplete configuration"
801                                       " (missing cluster.rsahostkeypub)")
802     self._config_data = data
803     self._config_time = st.st_mtime
804     self._config_size = st.st_size
805     self._config_inode = st.st_ino
806
807   def _DistributeConfig(self):
808     """Distribute the configuration to the other nodes.
809
810     Currently, this only copies the configuration file. In the future,
811     it could be used to encapsulate the 2/3-phase update mechanism.
812
813     """
814     if self._offline:
815       return True
816     bad = False
817     nodelist = self._UnlockedGetNodeList()
818     myhostname = self._my_hostname
819
820     try:
821       nodelist.remove(myhostname)
822     except ValueError:
823       pass
824
825     result = rpc.call_upload_file(nodelist, self._cfg_file)
826     for node in nodelist:
827       if not result[node]:
828         logging.error("copy of file %s to node %s failed",
829                       self._cfg_file, node)
830         bad = True
831     return not bad
832
833   def _WriteConfig(self, destination=None):
834     """Write the configuration data to persistent storage.
835
836     """
837     if destination is None:
838       destination = self._cfg_file
839     self._BumpSerialNo()
840     txt = serializer.Dump(self._config_data.ToDict())
841     dir_name, file_name = os.path.split(destination)
842     fd, name = tempfile.mkstemp('.newconfig', file_name, dir_name)
843     f = os.fdopen(fd, 'w')
844     try:
845       f.write(txt)
846       os.fsync(f.fileno())
847     finally:
848       f.close()
849     # we don't need to do os.close(fd) as f.close() did it
850     os.rename(name, destination)
851     self.write_count += 1
852     # re-set our cache as not to re-read the config file
853     try:
854       st = os.stat(destination)
855     except OSError, err:
856       raise errors.ConfigurationError("Can't stat config file: %s" % err)
857     self._config_time = st.st_mtime
858     self._config_size = st.st_size
859     self._config_inode = st.st_ino
860     # and redistribute the config file
861     self._DistributeConfig()
862
863   @locking.ssynchronized(_config_lock)
864   def InitConfig(self, cluster_config, master_node_config):
865     """Create the initial cluster configuration.
866
867     It will contain the current node, which will also be the master
868     node, and no instances.
869
870     @type cluster_config: objects.Cluster
871     @param cluster_config: Cluster configuration
872     @type master_node_config: objects.Node
873     @param master_node_config: Master node configuration
874
875     """
876     nodes = {
877       master_node_config.name: master_node_config,
878       }
879
880     self._config_data = objects.ConfigData(cluster=cluster_config,
881                                            nodes=nodes,
882                                            instances={},
883                                            serial_no=1)
884     self._WriteConfig()
885
886   @locking.ssynchronized(_config_lock, shared=1)
887   def GetVGName(self):
888     """Return the volume group name.
889
890     """
891     self._OpenConfig()
892     return self._config_data.cluster.volume_group_name
893
894   @locking.ssynchronized(_config_lock)
895   def SetVGName(self, vg_name):
896     """Set the volume group name.
897
898     """
899     self._OpenConfig()
900     self._config_data.cluster.volume_group_name = vg_name
901     self._config_data.cluster.serial_no += 1
902     self._WriteConfig()
903
904   @locking.ssynchronized(_config_lock, shared=1)
905   def GetDefBridge(self):
906     """Return the default bridge.
907
908     """
909     self._OpenConfig()
910     return self._config_data.cluster.default_bridge
911
912   @locking.ssynchronized(_config_lock, shared=1)
913   def GetMACPrefix(self):
914     """Return the mac prefix.
915
916     """
917     self._OpenConfig()
918     return self._config_data.cluster.mac_prefix
919
920   @locking.ssynchronized(_config_lock, shared=1)
921   def GetClusterInfo(self):
922     """Returns informations about the cluster
923
924     Returns:
925       the cluster object
926
927     """
928     self._OpenConfig()
929
930     return self._config_data.cluster
931
932   @locking.ssynchronized(_config_lock)
933   def Update(self, target):
934     """Notify function to be called after updates.
935
936     This function must be called when an object (as returned by
937     GetInstanceInfo, GetNodeInfo, GetCluster) has been updated and the
938     caller wants the modifications saved to the backing store. Note
939     that all modified objects will be saved, but the target argument
940     is the one the caller wants to ensure that it's saved.
941
942     """
943     if self._config_data is None:
944       raise errors.ProgrammerError("Configuration file not read,"
945                                    " cannot save.")
946     if isinstance(target, objects.Cluster):
947       test = target == self._config_data.cluster
948     elif isinstance(target, objects.Node):
949       test = target in self._config_data.nodes.values()
950     elif isinstance(target, objects.Instance):
951       test = target in self._config_data.instances.values()
952     else:
953       raise errors.ProgrammerError("Invalid object type (%s) passed to"
954                                    " ConfigWriter.Update" % type(target))
955     if not test:
956       raise errors.ConfigurationError("Configuration updated since object"
957                                       " has been read or unknown object")
958     target.serial_no += 1
959
960     self._WriteConfig()