Merge branch 'stable-2.6-hotplug' into stable-2.6-ippool-hotplug-esi
[ganeti-local] / lib / ssconf.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012 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 """Global Configuration data for Ganeti.
23
24 This module provides the interface to a special case of cluster
25 configuration data, which is mostly static and available to all nodes.
26
27 """
28
29 import sys
30 import re
31 import os
32 import errno
33
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import utils
37 from ganeti import serializer
38 from ganeti import objects
39 from ganeti import netutils
40
41
42 SSCONF_LOCK_TIMEOUT = 10
43
44 RE_VALID_SSCONF_NAME = re.compile(r"^[-_a-z0-9]+$")
45
46
47 class SimpleConfigReader(object):
48   """Simple class to read configuration file.
49
50   """
51   def __init__(self, file_name=constants.CLUSTER_CONF_FILE):
52     """Initializes this class.
53
54     @type file_name: string
55     @param file_name: Configuration file path
56
57     """
58     self._file_name = file_name
59     self._last_inode = None
60     self._last_mtime = None
61     self._last_size = None
62
63     self._config_data = None
64     self._inst_ips_by_link = None
65     self._ip_to_inst_by_link = None
66     self._instances_ips = None
67     self._mc_primary_ips = None
68     self._nodes_primary_ips = None
69
70     # we need a forced reload at class init time, to initialize _last_*
71     self._Load(force=True)
72
73   def _Load(self, force=False):
74     """Loads (or reloads) the config file.
75
76     @type force: boolean
77     @param force: whether to force the reload without checking the mtime
78     @rtype: boolean
79     @return: boolean value that says whether we reloaded the configuration or
80              not (because we decided it was already up-to-date)
81
82     """
83     try:
84       cfg_stat = os.stat(self._file_name)
85     except EnvironmentError, err:
86       raise errors.ConfigurationError("Cannot stat config file %s: %s" %
87                                       (self._file_name, err))
88     inode = cfg_stat.st_ino
89     mtime = cfg_stat.st_mtime
90     size = cfg_stat.st_size
91
92     if (force or inode != self._last_inode or
93         mtime > self._last_mtime or
94         size != self._last_size):
95       self._last_inode = inode
96       self._last_mtime = mtime
97       self._last_size = size
98     else:
99       # Don't reload
100       return False
101
102     try:
103       self._config_data = serializer.Load(utils.ReadFile(self._file_name))
104     except EnvironmentError, err:
105       raise errors.ConfigurationError("Cannot read config file %s: %s" %
106                                       (self._file_name, err))
107     except ValueError, err:
108       raise errors.ConfigurationError("Cannot load config file %s: %s" %
109                                       (self._file_name, err))
110
111     self._ip_to_inst_by_link = {}
112     self._instances_ips = []
113     self._inst_ips_by_link = {}
114     c_nparams = self._config_data["cluster"]["nicparams"][constants.PP_DEFAULT]
115     for iname in self._config_data["instances"]:
116       instance = self._config_data["instances"][iname]
117       for nic in instance["nics"]:
118         if "ip" in nic and nic["ip"]:
119           params = objects.FillDict(c_nparams, nic["nicparams"])
120           if not params["link"] in self._inst_ips_by_link:
121             self._inst_ips_by_link[params["link"]] = []
122             self._ip_to_inst_by_link[params["link"]] = {}
123           self._ip_to_inst_by_link[params["link"]][nic["ip"]] = iname
124           self._inst_ips_by_link[params["link"]].append(nic["ip"])
125
126     self._nodes_primary_ips = []
127     self._mc_primary_ips = []
128     for node_name in self._config_data["nodes"]:
129       node = self._config_data["nodes"][node_name]
130       self._nodes_primary_ips.append(node["primary_ip"])
131       if node["master_candidate"]:
132         self._mc_primary_ips.append(node["primary_ip"])
133
134     return True
135
136   # Clients can request a reload of the config file, so we export our internal
137   # _Load function as Reload.
138   Reload = _Load
139
140   def GetClusterName(self):
141     return self._config_data["cluster"]["cluster_name"]
142
143   def GetHostKey(self):
144     return self._config_data["cluster"]["rsahostkeypub"]
145
146   def GetMasterNode(self):
147     return self._config_data["cluster"]["master_node"]
148
149   def GetMasterIP(self):
150     return self._config_data["cluster"]["master_ip"]
151
152   def GetMasterNetdev(self):
153     return self._config_data["cluster"]["master_netdev"]
154
155   def GetMasterNetmask(self):
156     return self._config_data["cluster"]["master_netmask"]
157
158   def GetFileStorageDir(self):
159     return self._config_data["cluster"]["file_storage_dir"]
160
161   def GetSharedFileStorageDir(self):
162     return self._config_data["cluster"]["shared_file_storage_dir"]
163
164   def GetNodeList(self):
165     return self._config_data["nodes"].keys()
166
167   def GetConfigSerialNo(self):
168     return self._config_data["serial_no"]
169
170   def GetClusterSerialNo(self):
171     return self._config_data["cluster"]["serial_no"]
172
173   def GetDefaultNicParams(self):
174     return self._config_data["cluster"]["nicparams"][constants.PP_DEFAULT]
175
176   def GetDefaultNicLink(self):
177     return self.GetDefaultNicParams()[constants.NIC_LINK]
178
179   def GetNodeStatusFlags(self, node):
180     """Get a node's status flags
181
182     @type node: string
183     @param node: node name
184     @rtype: (bool, bool, bool)
185     @return: (master_candidate, drained, offline) (or None if no such node)
186
187     """
188     if node not in self._config_data["nodes"]:
189       return None
190
191     master_candidate = self._config_data["nodes"][node]["master_candidate"]
192     drained = self._config_data["nodes"][node]["drained"]
193     offline = self._config_data["nodes"][node]["offline"]
194     return master_candidate, drained, offline
195
196   def GetInstanceByLinkIp(self, ip, link):
197     """Get instance name from its link and ip address.
198
199     @type ip: string
200     @param ip: ip address
201     @type link: string
202     @param link: nic link
203     @rtype: string
204     @return: instance name
205
206     """
207     if not link:
208       link = self.GetDefaultNicLink()
209     if not link in self._ip_to_inst_by_link:
210       return None
211     if not ip in self._ip_to_inst_by_link[link]:
212       return None
213     return self._ip_to_inst_by_link[link][ip]
214
215   def GetNodePrimaryIp(self, node):
216     """Get a node's primary ip
217
218     @type node: string
219     @param node: node name
220     @rtype: string, or None
221     @return: node's primary ip, or None if no such node
222
223     """
224     if node not in self._config_data["nodes"]:
225       return None
226     return self._config_data["nodes"][node]["primary_ip"]
227
228   def GetInstancePrimaryNode(self, instance):
229     """Get an instance's primary node
230
231     @type instance: string
232     @param instance: instance name
233     @rtype: string, or None
234     @return: primary node, or None if no such instance
235
236     """
237     if instance not in self._config_data["instances"]:
238       return None
239     return self._config_data["instances"][instance]["primary_node"]
240
241   def GetNodesPrimaryIps(self):
242     return self._nodes_primary_ips
243
244   def GetMasterCandidatesPrimaryIps(self):
245     return self._mc_primary_ips
246
247   def GetInstancesIps(self, link):
248     """Get list of nic ips connected to a certain link.
249
250     @type link: string
251     @param link: nic link
252     @rtype: list
253     @return: list of ips connected to that link
254
255     """
256     if not link:
257       link = self.GetDefaultNicLink()
258
259     if link in self._inst_ips_by_link:
260       return self._inst_ips_by_link[link]
261     else:
262       return []
263
264
265 class SimpleStore(object):
266   """Interface to static cluster data.
267
268   This is different that the config.ConfigWriter and
269   SimpleConfigReader classes in that it holds data that will always be
270   present, even on nodes which don't have all the cluster data.
271
272   Other particularities of the datastore:
273     - keys are restricted to predefined values
274
275   """
276   _VALID_KEYS = (
277     constants.SS_CLUSTER_NAME,
278     constants.SS_CLUSTER_TAGS,
279     constants.SS_FILE_STORAGE_DIR,
280     constants.SS_SHARED_FILE_STORAGE_DIR,
281     constants.SS_MASTER_CANDIDATES,
282     constants.SS_MASTER_CANDIDATES_IPS,
283     constants.SS_MASTER_IP,
284     constants.SS_MASTER_NETDEV,
285     constants.SS_MASTER_NETMASK,
286     constants.SS_MASTER_NODE,
287     constants.SS_NODE_LIST,
288     constants.SS_NODE_PRIMARY_IPS,
289     constants.SS_NODE_SECONDARY_IPS,
290     constants.SS_OFFLINE_NODES,
291     constants.SS_ONLINE_NODES,
292     constants.SS_PRIMARY_IP_FAMILY,
293     constants.SS_INSTANCE_LIST,
294     constants.SS_RELEASE_VERSION,
295     constants.SS_HYPERVISOR_LIST,
296     constants.SS_MAINTAIN_NODE_HEALTH,
297     constants.SS_UID_POOL,
298     constants.SS_NODEGROUPS,
299     constants.SS_NETWORKS,
300     )
301   _MAX_SIZE = 131072
302
303   def __init__(self, cfg_location=None):
304     if cfg_location is None:
305       self._cfg_dir = constants.DATA_DIR
306     else:
307       self._cfg_dir = cfg_location
308
309   def KeyToFilename(self, key):
310     """Convert a given key into filename.
311
312     """
313     if key not in self._VALID_KEYS:
314       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
315                                    % str(key))
316
317     filename = self._cfg_dir + "/" + constants.SSCONF_FILEPREFIX + key
318     return filename
319
320   def _ReadFile(self, key, default=None):
321     """Generic routine to read keys.
322
323     This will read the file which holds the value requested. Errors
324     will be changed into ConfigurationErrors.
325
326     """
327     filename = self.KeyToFilename(key)
328     try:
329       data = utils.ReadFile(filename, size=self._MAX_SIZE)
330     except EnvironmentError, err:
331       if err.errno == errno.ENOENT and default is not None:
332         return default
333       raise errors.ConfigurationError("Can't read from the ssconf file:"
334                                       " '%s'" % str(err))
335     data = data.rstrip("\n")
336     return data
337
338   def WriteFiles(self, values):
339     """Writes ssconf files used by external scripts.
340
341     @type values: dict
342     @param values: Dictionary of (name, value)
343
344     """
345     ssconf_lock = utils.FileLock.Open(constants.SSCONF_LOCK_FILE)
346
347     # Get lock while writing files
348     ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
349     try:
350       for name, value in values.iteritems():
351         if value and not value.endswith("\n"):
352           value += "\n"
353         if len(value) > self._MAX_SIZE:
354           raise errors.ConfigurationError("ssconf file %s above maximum size" %
355                                           name)
356         utils.WriteFile(self.KeyToFilename(name), data=value,
357                         mode=constants.SS_FILE_PERMS)
358     finally:
359       ssconf_lock.Unlock()
360
361   def GetFileList(self):
362     """Return the list of all config files.
363
364     This is used for computing node replication data.
365
366     """
367     return [self.KeyToFilename(key) for key in self._VALID_KEYS]
368
369   def GetClusterName(self):
370     """Get the cluster name.
371
372     """
373     return self._ReadFile(constants.SS_CLUSTER_NAME)
374
375   def GetFileStorageDir(self):
376     """Get the file storage dir.
377
378     """
379     return self._ReadFile(constants.SS_FILE_STORAGE_DIR)
380
381   def GetSharedFileStorageDir(self):
382     """Get the shared file storage dir.
383
384     """
385     return self._ReadFile(constants.SS_SHARED_FILE_STORAGE_DIR)
386
387   def GetMasterCandidates(self):
388     """Return the list of master candidates.
389
390     """
391     data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
392     nl = data.splitlines(False)
393     return nl
394
395   def GetMasterCandidatesIPList(self):
396     """Return the list of master candidates' primary IP.
397
398     """
399     data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
400     nl = data.splitlines(False)
401     return nl
402
403   def GetMasterIP(self):
404     """Get the IP of the master node for this cluster.
405
406     """
407     return self._ReadFile(constants.SS_MASTER_IP)
408
409   def GetMasterNetdev(self):
410     """Get the netdev to which we'll add the master ip.
411
412     """
413     return self._ReadFile(constants.SS_MASTER_NETDEV)
414
415   def GetMasterNetmask(self):
416     """Get the master netmask.
417
418     """
419     try:
420       return self._ReadFile(constants.SS_MASTER_NETMASK)
421     except errors.ConfigurationError:
422       family = self.GetPrimaryIPFamily()
423       ipcls = netutils.IPAddress.GetClassFromIpFamily(family)
424       return ipcls.iplen
425
426   def GetMasterNode(self):
427     """Get the hostname of the master node for this cluster.
428
429     """
430     return self._ReadFile(constants.SS_MASTER_NODE)
431
432   def GetNodeList(self):
433     """Return the list of cluster nodes.
434
435     """
436     data = self._ReadFile(constants.SS_NODE_LIST)
437     nl = data.splitlines(False)
438     return nl
439
440   def GetNodePrimaryIPList(self):
441     """Return the list of cluster nodes' primary IP.
442
443     """
444     data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
445     nl = data.splitlines(False)
446     return nl
447
448   def GetNodeSecondaryIPList(self):
449     """Return the list of cluster nodes' secondary IP.
450
451     """
452     data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
453     nl = data.splitlines(False)
454     return nl
455
456   def GetNodegroupList(self):
457     """Return the list of nodegroups.
458
459     """
460     data = self._ReadFile(constants.SS_NODEGROUPS)
461     nl = data.splitlines(False)
462     return nl
463
464   def GetNetworkList(self):
465     """Return the list of networks.
466
467     """
468     data = self._ReadFile(constants.SS_NETWORKS)
469     nl = data.splitlines(False)
470     return nl
471
472   def GetClusterTags(self):
473     """Return the cluster tags.
474
475     """
476     data = self._ReadFile(constants.SS_CLUSTER_TAGS)
477     nl = data.splitlines(False)
478     return nl
479
480   def GetHypervisorList(self):
481     """Return the list of enabled hypervisors.
482
483     """
484     data = self._ReadFile(constants.SS_HYPERVISOR_LIST)
485     nl = data.splitlines(False)
486     return nl
487
488   def GetMaintainNodeHealth(self):
489     """Return the value of the maintain_node_health option.
490
491     """
492     data = self._ReadFile(constants.SS_MAINTAIN_NODE_HEALTH)
493     # we rely on the bool serialization here
494     return data == "True"
495
496   def GetUidPool(self):
497     """Return the user-id pool definition string.
498
499     The separator character is a newline.
500
501     The return value can be parsed using uidpool.ParseUidPool()::
502
503       ss = ssconf.SimpleStore()
504       uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
505
506     """
507     data = self._ReadFile(constants.SS_UID_POOL)
508     return data
509
510   def GetPrimaryIPFamily(self):
511     """Return the cluster-wide primary address family.
512
513     """
514     try:
515       return int(self._ReadFile(constants.SS_PRIMARY_IP_FAMILY,
516                                 default=netutils.IP4Address.family))
517     except (ValueError, TypeError), err:
518       raise errors.ConfigurationError("Error while trying to parse primary ip"
519                                       " family: %s" % err)
520
521
522 def GetMasterAndMyself(ss=None):
523   """Get the master node and my own hostname.
524
525   This can be either used for a 'soft' check (compared to CheckMaster,
526   which exits) or just for computing both at the same time.
527
528   The function does not handle any errors, these should be handled in
529   the caller (errors.ConfigurationError, errors.ResolverError).
530
531   @param ss: either a sstore.SimpleConfigReader or a
532       sstore.SimpleStore instance
533   @rtype: tuple
534   @return: a tuple (master node name, my own name)
535
536   """
537   if ss is None:
538     ss = SimpleStore()
539   return ss.GetMasterNode(), netutils.Hostname.GetSysName()
540
541
542 def CheckMaster(debug, ss=None):
543   """Checks the node setup.
544
545   If this is the master, the function will return. Otherwise it will
546   exit with an exit code based on the node status.
547
548   """
549   try:
550     master_name, myself = GetMasterAndMyself(ss)
551   except errors.ConfigurationError, err:
552     print "Cluster configuration incomplete: '%s'" % str(err)
553     sys.exit(constants.EXIT_NODESETUP_ERROR)
554   except errors.ResolverError, err:
555     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
556     sys.exit(constants.EXIT_NODESETUP_ERROR)
557
558   if myself != master_name:
559     if debug:
560       sys.stderr.write("Not master, exiting.\n")
561     sys.exit(constants.EXIT_NOTMASTER)