Fix a misuse of exc_info in logging.info
[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
38 from ganeti import errors
39 from ganeti import locking
40 from ganeti import logger
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     # Note: in order to prevent errors when resolving our name in
80     # _DistributeConfig, we compute it here once and reuse it; it's
81     # better to raise an error before starting to modify the config
82     # file than after it was modified
83     self._my_hostname = utils.HostInfo().name
84
85   # this method needs to be static, so that we can call it on the class
86   @staticmethod
87   def IsCluster():
88     """Check if the cluster is configured.
89
90     """
91     return os.path.exists(constants.CLUSTER_CONF_FILE)
92
93   @locking.ssynchronized(_config_lock, shared=1)
94   def GenerateMAC(self):
95     """Generate a MAC for an instance.
96
97     This should check the current instances for duplicates.
98
99     """
100     self._OpenConfig()
101     prefix = self._config_data.cluster.mac_prefix
102     all_macs = self._AllMACs()
103     retries = 64
104     while retries > 0:
105       byte1 = random.randrange(0, 256)
106       byte2 = random.randrange(0, 256)
107       byte3 = random.randrange(0, 256)
108       mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
109       if mac not in all_macs:
110         break
111       retries -= 1
112     else:
113       raise errors.ConfigurationError("Can't generate unique MAC")
114     return mac
115
116   @locking.ssynchronized(_config_lock, shared=1)
117   def IsMacInUse(self, mac):
118     """Predicate: check if the specified MAC is in use in the Ganeti cluster.
119
120     This only checks instances managed by this cluster, it does not
121     check for potential collisions elsewhere.
122
123     """
124     self._OpenConfig()
125     all_macs = self._AllMACs()
126     return mac in all_macs
127
128   def _ComputeAllLVs(self):
129     """Compute the list of all LVs.
130
131     """
132     self._OpenConfig()
133     lvnames = set()
134     for instance in self._config_data.instances.values():
135       node_data = instance.MapLVsByNode()
136       for lv_list in node_data.values():
137         lvnames.update(lv_list)
138     return lvnames
139
140   @locking.ssynchronized(_config_lock, shared=1)
141   def GenerateUniqueID(self, exceptions=None):
142     """Generate an unique disk name.
143
144     This checks the current node, instances and disk names for
145     duplicates.
146
147     Args:
148       - exceptions: a list with some other names which should be checked
149                     for uniqueness (used for example when you want to get
150                     more than one id at one time without adding each one in
151                     turn to the config file
152
153     Returns: the unique id as a string
154
155     """
156     existing = set()
157     existing.update(self._temporary_ids)
158     existing.update(self._ComputeAllLVs())
159     existing.update(self._config_data.instances.keys())
160     existing.update(self._config_data.nodes.keys())
161     if exceptions is not None:
162       existing.update(exceptions)
163     retries = 64
164     while retries > 0:
165       unique_id = utils.NewUUID()
166       if unique_id not in existing and unique_id is not None:
167         break
168     else:
169       raise errors.ConfigurationError("Not able generate an unique ID"
170                                       " (last tried ID: %s" % unique_id)
171     self._temporary_ids.add(unique_id)
172     return unique_id
173
174   def _AllMACs(self):
175     """Return all MACs present in the config.
176
177     """
178     self._OpenConfig()
179
180     result = []
181     for instance in self._config_data.instances.values():
182       for nic in instance.nics:
183         result.append(nic.mac)
184
185     return result
186
187   @locking.ssynchronized(_config_lock, shared=1)
188   def VerifyConfig(self):
189     """Stub verify function.
190     """
191     self._OpenConfig()
192
193     result = []
194     seen_macs = []
195     data = self._config_data
196     for instance_name in data.instances:
197       instance = data.instances[instance_name]
198       if instance.primary_node not in data.nodes:
199         result.append("instance '%s' has invalid primary node '%s'" %
200                       (instance_name, instance.primary_node))
201       for snode in instance.secondary_nodes:
202         if snode not in data.nodes:
203           result.append("instance '%s' has invalid secondary node '%s'" %
204                         (instance_name, snode))
205       for idx, nic in enumerate(instance.nics):
206         if nic.mac in seen_macs:
207           result.append("instance '%s' has NIC %d mac %s duplicate" %
208                         (instance_name, idx, nic.mac))
209         else:
210           seen_macs.append(nic.mac)
211     return result
212
213   def _UnlockedSetDiskID(self, disk, node_name):
214     """Convert the unique ID to the ID needed on the target nodes.
215
216     This is used only for drbd, which needs ip/port configuration.
217
218     The routine descends down and updates its children also, because
219     this helps when the only the top device is passed to the remote
220     node.
221
222     This function is for internal use, when the config lock is already held.
223
224     """
225     if disk.children:
226       for child in disk.children:
227         self._UnlockedSetDiskID(child, node_name)
228
229     if disk.logical_id is None and disk.physical_id is not None:
230       return
231     if disk.dev_type in constants.LDS_DRBD:
232       pnode, snode, port = disk.logical_id
233       if node_name not in (pnode, snode):
234         raise errors.ConfigurationError("DRBD device not knowing node %s" %
235                                         node_name)
236       pnode_info = self._UnlockedGetNodeInfo(pnode)
237       snode_info = self._UnlockedGetNodeInfo(snode)
238       if pnode_info is None or snode_info is None:
239         raise errors.ConfigurationError("Can't find primary or secondary node"
240                                         " for %s" % str(disk))
241       if pnode == node_name:
242         disk.physical_id = (pnode_info.secondary_ip, port,
243                             snode_info.secondary_ip, port)
244       else: # it must be secondary, we tested above
245         disk.physical_id = (snode_info.secondary_ip, port,
246                             pnode_info.secondary_ip, port)
247     else:
248       disk.physical_id = disk.logical_id
249     return
250
251   @locking.ssynchronized(_config_lock)
252   def SetDiskID(self, disk, node_name):
253     """Convert the unique ID to the ID needed on the target nodes.
254
255     This is used only for drbd, which needs ip/port configuration.
256
257     The routine descends down and updates its children also, because
258     this helps when the only the top device is passed to the remote
259     node.
260
261     """
262     return self._UnlockedSetDiskID(disk, node_name)
263
264   @locking.ssynchronized(_config_lock)
265   def AddTcpUdpPort(self, port):
266     """Adds a new port to the available port pool.
267
268     """
269     if not isinstance(port, int):
270       raise errors.ProgrammerError("Invalid type passed for port")
271
272     self._OpenConfig()
273     self._config_data.cluster.tcpudp_port_pool.add(port)
274     self._WriteConfig()
275
276   @locking.ssynchronized(_config_lock, shared=1)
277   def GetPortList(self):
278     """Returns a copy of the current port list.
279
280     """
281     self._OpenConfig()
282     return self._config_data.cluster.tcpudp_port_pool.copy()
283
284   @locking.ssynchronized(_config_lock)
285   def AllocatePort(self):
286     """Allocate a port.
287
288     The port will be taken from the available port pool or from the
289     default port range (and in this case we increase
290     highest_used_port).
291
292     """
293     self._OpenConfig()
294
295     # If there are TCP/IP ports configured, we use them first.
296     if self._config_data.cluster.tcpudp_port_pool:
297       port = self._config_data.cluster.tcpudp_port_pool.pop()
298     else:
299       port = self._config_data.cluster.highest_used_port + 1
300       if port >= constants.LAST_DRBD_PORT:
301         raise errors.ConfigurationError("The highest used port is greater"
302                                         " than %s. Aborting." %
303                                         constants.LAST_DRBD_PORT)
304       self._config_data.cluster.highest_used_port = port
305
306     self._WriteConfig()
307     return port
308
309   @locking.ssynchronized(_config_lock, shared=1)
310   def GetHostKey(self):
311     """Return the rsa hostkey from the config.
312
313     Args: None
314
315     Returns: rsa hostkey
316     """
317     self._OpenConfig()
318     return self._config_data.cluster.rsahostkeypub
319
320   @locking.ssynchronized(_config_lock)
321   def AddInstance(self, instance):
322     """Add an instance to the config.
323
324     This should be used after creating a new instance.
325
326     Args:
327       instance: the instance object
328     """
329     if not isinstance(instance, objects.Instance):
330       raise errors.ProgrammerError("Invalid type passed to AddInstance")
331
332     if instance.disk_template != constants.DT_DISKLESS:
333       all_lvs = instance.MapLVsByNode()
334       logger.Info("Instance '%s' DISK_LAYOUT: %s" % (instance.name, all_lvs))
335
336     self._OpenConfig()
337     self._config_data.instances[instance.name] = instance
338     self._WriteConfig()
339
340   def _SetInstanceStatus(self, instance_name, status):
341     """Set the instance's status to a given value.
342
343     """
344     if status not in ("up", "down"):
345       raise errors.ProgrammerError("Invalid status '%s' passed to"
346                                    " ConfigWriter._SetInstanceStatus()" %
347                                    status)
348     self._OpenConfig()
349
350     if instance_name not in self._config_data.instances:
351       raise errors.ConfigurationError("Unknown instance '%s'" %
352                                       instance_name)
353     instance = self._config_data.instances[instance_name]
354     if instance.status != status:
355       instance.status = status
356       self._WriteConfig()
357
358   @locking.ssynchronized(_config_lock)
359   def MarkInstanceUp(self, instance_name):
360     """Mark the instance status to up in the config.
361
362     """
363     self._SetInstanceStatus(instance_name, "up")
364
365   @locking.ssynchronized(_config_lock)
366   def RemoveInstance(self, instance_name):
367     """Remove the instance from the configuration.
368
369     """
370     self._OpenConfig()
371
372     if instance_name not in self._config_data.instances:
373       raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
374     del self._config_data.instances[instance_name]
375     self._WriteConfig()
376
377   @locking.ssynchronized(_config_lock)
378   def RenameInstance(self, old_name, new_name):
379     """Rename an instance.
380
381     This needs to be done in ConfigWriter and not by RemoveInstance
382     combined with AddInstance as only we can guarantee an atomic
383     rename.
384
385     """
386     self._OpenConfig()
387     if old_name not in self._config_data.instances:
388       raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
389     inst = self._config_data.instances[old_name]
390     del self._config_data.instances[old_name]
391     inst.name = new_name
392
393     for disk in inst.disks:
394       if disk.dev_type == constants.LD_FILE:
395         # rename the file paths in logical and physical id
396         file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1]))
397         disk.physical_id = disk.logical_id = (disk.logical_id[0],
398                                               os.path.join(file_storage_dir,
399                                                            inst.name,
400                                                            disk.iv_name))
401
402     self._config_data.instances[inst.name] = inst
403     self._WriteConfig()
404
405   @locking.ssynchronized(_config_lock)
406   def MarkInstanceDown(self, instance_name):
407     """Mark the status of an instance to down in the configuration.
408
409     """
410     self._SetInstanceStatus(instance_name, "down")
411
412   @locking.ssynchronized(_config_lock, shared=1)
413   def GetInstanceList(self):
414     """Get the list of instances.
415
416     Returns:
417       array of instances, ex. ['instance2.example.com','instance1.example.com']
418       these contains all the instances, also the ones in Admin_down state
419
420     """
421     self._OpenConfig()
422
423     return self._config_data.instances.keys()
424
425   @locking.ssynchronized(_config_lock, shared=1)
426   def ExpandInstanceName(self, short_name):
427     """Attempt to expand an incomplete instance name.
428
429     """
430     self._OpenConfig()
431
432     return utils.MatchNameComponent(short_name,
433                                     self._config_data.instances.keys())
434
435   @locking.ssynchronized(_config_lock, shared=1)
436   def GetInstanceInfo(self, instance_name):
437     """Returns informations about an instance.
438
439     It takes the information from the configuration file. Other informations of
440     an instance are taken from the live systems.
441
442     Args:
443       instance: name of the instance, ex instance1.example.com
444
445     Returns:
446       the instance object
447
448     """
449     self._OpenConfig()
450
451     if instance_name not in self._config_data.instances:
452       return None
453
454     return self._config_data.instances[instance_name]
455
456   @locking.ssynchronized(_config_lock)
457   def AddNode(self, node):
458     """Add a node to the configuration.
459
460     Args:
461       node: an object.Node instance
462
463     """
464     self._OpenConfig()
465     self._config_data.nodes[node.name] = node
466     self._WriteConfig()
467
468   @locking.ssynchronized(_config_lock)
469   def RemoveNode(self, node_name):
470     """Remove a node from the configuration.
471
472     """
473     self._OpenConfig()
474     if node_name not in self._config_data.nodes:
475       raise errors.ConfigurationError("Unknown node '%s'" % node_name)
476
477     del self._config_data.nodes[node_name]
478     self._WriteConfig()
479
480   @locking.ssynchronized(_config_lock, shared=1)
481   def ExpandNodeName(self, short_name):
482     """Attempt to expand an incomplete instance name.
483
484     """
485     self._OpenConfig()
486
487     return utils.MatchNameComponent(short_name,
488                                     self._config_data.nodes.keys())
489
490   def _UnlockedGetNodeInfo(self, node_name):
491     """Get the configuration of a node, as stored in the config.
492
493     This function is for internal use, when the config lock is already held.
494
495     Args: node: nodename (tuple) of the node
496
497     Returns: the node object
498
499     """
500     self._OpenConfig()
501
502     if node_name not in self._config_data.nodes:
503       return None
504
505     return self._config_data.nodes[node_name]
506
507
508   @locking.ssynchronized(_config_lock, shared=1)
509   def GetNodeInfo(self, node_name):
510     """Get the configuration of a node, as stored in the config.
511
512     Args: node: nodename (tuple) of the node
513
514     Returns: the node object
515
516     """
517     return self._UnlockedGetNodeInfo(node_name)
518
519   def _UnlockedGetNodeList(self):
520     """Return the list of nodes which are in the configuration.
521
522     This function is for internal use, when the config lock is already held.
523
524     """
525     self._OpenConfig()
526     return self._config_data.nodes.keys()
527
528
529   @locking.ssynchronized(_config_lock, shared=1)
530   def GetNodeList(self):
531     """Return the list of nodes which are in the configuration.
532
533     """
534     return self._UnlockedGetNodeList()
535
536   @locking.ssynchronized(_config_lock, shared=1)
537   def DumpConfig(self):
538     """Return the entire configuration of the cluster.
539     """
540     self._OpenConfig()
541     return self._config_data
542
543   def _BumpSerialNo(self):
544     """Bump up the serial number of the config.
545
546     """
547     self._config_data.cluster.serial_no += 1
548
549   def _OpenConfig(self):
550     """Read the config data from disk.
551
552     In case we already have configuration data and the config file has
553     the same mtime as when we read it, we skip the parsing of the
554     file, since de-serialisation could be slow.
555
556     """
557     try:
558       st = os.stat(self._cfg_file)
559     except OSError, err:
560       raise errors.ConfigurationError("Can't stat config file: %s" % err)
561     if (self._config_data is not None and
562         self._config_time is not None and
563         self._config_time == st.st_mtime and
564         self._config_size == st.st_size and
565         self._config_inode == st.st_ino):
566       # data is current, so skip loading of config file
567       return
568
569     # Make sure the configuration has the right version
570     ValidateConfig()
571
572     f = open(self._cfg_file, 'r')
573     try:
574       try:
575         data = objects.ConfigData.FromDict(serializer.Load(f.read()))
576       except Exception, err:
577         raise errors.ConfigurationError(err)
578     finally:
579       f.close()
580     if (not hasattr(data, 'cluster') or
581         not hasattr(data.cluster, 'rsahostkeypub')):
582       raise errors.ConfigurationError("Incomplete configuration"
583                                       " (missing cluster.rsahostkeypub)")
584     self._config_data = data
585     self._config_time = st.st_mtime
586     self._config_size = st.st_size
587     self._config_inode = st.st_ino
588
589   def _DistributeConfig(self):
590     """Distribute the configuration to the other nodes.
591
592     Currently, this only copies the configuration file. In the future,
593     it could be used to encapsulate the 2/3-phase update mechanism.
594
595     """
596     if self._offline:
597       return True
598     bad = False
599     nodelist = self._UnlockedGetNodeList()
600     myhostname = self._my_hostname
601
602     try:
603       nodelist.remove(myhostname)
604     except ValueError:
605       pass
606
607     result = rpc.call_upload_file(nodelist, self._cfg_file)
608     for node in nodelist:
609       if not result[node]:
610         logger.Error("copy of file %s to node %s failed" %
611                      (self._cfg_file, node))
612         bad = True
613     return not bad
614
615   def _WriteConfig(self, destination=None):
616     """Write the configuration data to persistent storage.
617
618     """
619     if destination is None:
620       destination = self._cfg_file
621     self._BumpSerialNo()
622     txt = serializer.Dump(self._config_data.ToDict())
623     dir_name, file_name = os.path.split(destination)
624     fd, name = tempfile.mkstemp('.newconfig', file_name, dir_name)
625     f = os.fdopen(fd, 'w')
626     try:
627       f.write(txt)
628       os.fsync(f.fileno())
629     finally:
630       f.close()
631     # we don't need to do os.close(fd) as f.close() did it
632     os.rename(name, destination)
633     self.write_count += 1
634     # re-set our cache as not to re-read the config file
635     try:
636       st = os.stat(destination)
637     except OSError, err:
638       raise errors.ConfigurationError("Can't stat config file: %s" % err)
639     self._config_time = st.st_mtime
640     self._config_size = st.st_size
641     self._config_inode = st.st_ino
642     # and redistribute the config file
643     self._DistributeConfig()
644
645   @locking.ssynchronized(_config_lock)
646   def InitConfig(self, node, primary_ip, secondary_ip,
647                  hostkeypub, mac_prefix, vg_name, def_bridge):
648     """Create the initial cluster configuration.
649
650     It will contain the current node, which will also be the master
651     node, and no instances or operating systmes.
652
653     Args:
654       node: the nodename of the initial node
655       primary_ip: the IP address of the current host
656       secondary_ip: the secondary IP of the current host or None
657       hostkeypub: the public hostkey of this host
658
659     """
660     hu_port = constants.FIRST_DRBD_PORT - 1
661     globalconfig = objects.Cluster(serial_no=1,
662                                    rsahostkeypub=hostkeypub,
663                                    highest_used_port=hu_port,
664                                    mac_prefix=mac_prefix,
665                                    volume_group_name=vg_name,
666                                    default_bridge=def_bridge,
667                                    tcpudp_port_pool=set())
668     if secondary_ip is None:
669       secondary_ip = primary_ip
670     nodeconfig = objects.Node(name=node, primary_ip=primary_ip,
671                               secondary_ip=secondary_ip)
672
673     self._config_data = objects.ConfigData(nodes={node: nodeconfig},
674                                            instances={},
675                                            cluster=globalconfig)
676     self._WriteConfig()
677
678   @locking.ssynchronized(_config_lock, shared=1)
679   def GetVGName(self):
680     """Return the volume group name.
681
682     """
683     self._OpenConfig()
684     return self._config_data.cluster.volume_group_name
685
686   @locking.ssynchronized(_config_lock)
687   def SetVGName(self, vg_name):
688     """Set the volume group name.
689
690     """
691     self._OpenConfig()
692     self._config_data.cluster.volume_group_name = vg_name
693     self._WriteConfig()
694
695   @locking.ssynchronized(_config_lock, shared=1)
696   def GetDefBridge(self):
697     """Return the default bridge.
698
699     """
700     self._OpenConfig()
701     return self._config_data.cluster.default_bridge
702
703   @locking.ssynchronized(_config_lock, shared=1)
704   def GetMACPrefix(self):
705     """Return the mac prefix.
706
707     """
708     self._OpenConfig()
709     return self._config_data.cluster.mac_prefix
710
711   @locking.ssynchronized(_config_lock, shared=1)
712   def GetClusterInfo(self):
713     """Returns informations about the cluster
714
715     Returns:
716       the cluster object
717
718     """
719     self._OpenConfig()
720
721     return self._config_data.cluster
722
723   @locking.ssynchronized(_config_lock)
724   def Update(self, target):
725     """Notify function to be called after updates.
726
727     This function must be called when an object (as returned by
728     GetInstanceInfo, GetNodeInfo, GetCluster) has been updated and the
729     caller wants the modifications saved to the backing store. Note
730     that all modified objects will be saved, but the target argument
731     is the one the caller wants to ensure that it's saved.
732
733     """
734     if self._config_data is None:
735       raise errors.ProgrammerError("Configuration file not read,"
736                                    " cannot save.")
737     if isinstance(target, objects.Cluster):
738       test = target == self._config_data.cluster
739     elif isinstance(target, objects.Node):
740       test = target in self._config_data.nodes.values()
741     elif isinstance(target, objects.Instance):
742       test = target in self._config_data.instances.values()
743     else:
744       raise errors.ProgrammerError("Invalid object type (%s) passed to"
745                                    " ConfigWriter.Update" % type(target))
746     if not test:
747       raise errors.ConfigurationError("Configuration updated since object"
748                                       " has been read or unknown object")
749     self._WriteConfig()