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