Merge branch 'devel-2.5'
[ganeti-local] / lib / ssconf.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2010 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 GetFileStorageDir(self):
156     return self._config_data["cluster"]["file_storage_dir"]
157
158   def GetSharedFileStorageDir(self):
159     return self._config_data["cluster"]["shared_file_storage_dir"]
160
161   def GetNodeList(self):
162     return self._config_data["nodes"].keys()
163
164   def GetConfigSerialNo(self):
165     return self._config_data["serial_no"]
166
167   def GetClusterSerialNo(self):
168     return self._config_data["cluster"]["serial_no"]
169
170   def GetDefaultNicParams(self):
171     return self._config_data["cluster"]["nicparams"][constants.PP_DEFAULT]
172
173   def GetDefaultNicLink(self):
174     return self.GetDefaultNicParams()[constants.NIC_LINK]
175
176   def GetNodeStatusFlags(self, node):
177     """Get a node's status flags
178
179     @type node: string
180     @param node: node name
181     @rtype: (bool, bool, bool)
182     @return: (master_candidate, drained, offline) (or None if no such node)
183
184     """
185     if node not in self._config_data["nodes"]:
186       return None
187
188     master_candidate = self._config_data["nodes"][node]["master_candidate"]
189     drained = self._config_data["nodes"][node]["drained"]
190     offline = self._config_data["nodes"][node]["offline"]
191     return master_candidate, drained, offline
192
193   def GetInstanceByLinkIp(self, ip, link):
194     """Get instance name from its link and ip address.
195
196     @type ip: string
197     @param ip: ip address
198     @type link: string
199     @param link: nic link
200     @rtype: string
201     @return: instance name
202
203     """
204     if not link:
205       link = self.GetDefaultNicLink()
206     if not link in self._ip_to_inst_by_link:
207       return None
208     if not ip in self._ip_to_inst_by_link[link]:
209       return None
210     return self._ip_to_inst_by_link[link][ip]
211
212   def GetNodePrimaryIp(self, node):
213     """Get a node's primary ip
214
215     @type node: string
216     @param node: node name
217     @rtype: string, or None
218     @return: node's primary ip, or None if no such node
219
220     """
221     if node not in self._config_data["nodes"]:
222       return None
223     return self._config_data["nodes"][node]["primary_ip"]
224
225   def GetInstancePrimaryNode(self, instance):
226     """Get an instance's primary node
227
228     @type instance: string
229     @param instance: instance name
230     @rtype: string, or None
231     @return: primary node, or None if no such instance
232
233     """
234     if instance not in self._config_data["instances"]:
235       return None
236     return self._config_data["instances"][instance]["primary_node"]
237
238   def GetNodesPrimaryIps(self):
239     return self._nodes_primary_ips
240
241   def GetMasterCandidatesPrimaryIps(self):
242     return self._mc_primary_ips
243
244   def GetInstancesIps(self, link):
245     """Get list of nic ips connected to a certain link.
246
247     @type link: string
248     @param link: nic link
249     @rtype: list
250     @return: list of ips connected to that link
251
252     """
253     if not link:
254       link = self.GetDefaultNicLink()
255
256     if link in self._inst_ips_by_link:
257       return self._inst_ips_by_link[link]
258     else:
259       return []
260
261
262 class SimpleStore(object):
263   """Interface to static cluster data.
264
265   This is different that the config.ConfigWriter and
266   SimpleConfigReader classes in that it holds data that will always be
267   present, even on nodes which don't have all the cluster data.
268
269   Other particularities of the datastore:
270     - keys are restricted to predefined values
271
272   """
273   _SS_FILEPREFIX = "ssconf_"
274   _VALID_KEYS = (
275     constants.SS_CLUSTER_NAME,
276     constants.SS_CLUSTER_TAGS,
277     constants.SS_FILE_STORAGE_DIR,
278     constants.SS_SHARED_FILE_STORAGE_DIR,
279     constants.SS_MASTER_CANDIDATES,
280     constants.SS_MASTER_CANDIDATES_IPS,
281     constants.SS_MASTER_IP,
282     constants.SS_MASTER_NETDEV,
283     constants.SS_MASTER_NODE,
284     constants.SS_NODE_LIST,
285     constants.SS_NODE_PRIMARY_IPS,
286     constants.SS_NODE_SECONDARY_IPS,
287     constants.SS_OFFLINE_NODES,
288     constants.SS_ONLINE_NODES,
289     constants.SS_PRIMARY_IP_FAMILY,
290     constants.SS_INSTANCE_LIST,
291     constants.SS_RELEASE_VERSION,
292     constants.SS_HYPERVISOR_LIST,
293     constants.SS_MAINTAIN_NODE_HEALTH,
294     constants.SS_UID_POOL,
295     constants.SS_NODEGROUPS,
296     )
297   _MAX_SIZE = 131072
298
299   def __init__(self, cfg_location=None):
300     if cfg_location is None:
301       self._cfg_dir = constants.DATA_DIR
302     else:
303       self._cfg_dir = cfg_location
304
305   def KeyToFilename(self, key):
306     """Convert a given key into filename.
307
308     """
309     if key not in self._VALID_KEYS:
310       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
311                                    % str(key))
312
313     filename = self._cfg_dir + "/" + self._SS_FILEPREFIX + key
314     return filename
315
316   def _ReadFile(self, key, default=None):
317     """Generic routine to read keys.
318
319     This will read the file which holds the value requested. Errors
320     will be changed into ConfigurationErrors.
321
322     """
323     filename = self.KeyToFilename(key)
324     try:
325       data = utils.ReadFile(filename, size=self._MAX_SIZE)
326     except EnvironmentError, err:
327       if err.errno == errno.ENOENT and default is not None:
328         return default
329       raise errors.ConfigurationError("Can't read from the ssconf file:"
330                                       " '%s'" % str(err))
331     data = data.rstrip("\n")
332     return data
333
334   def WriteFiles(self, values):
335     """Writes ssconf files used by external scripts.
336
337     @type values: dict
338     @param values: Dictionary of (name, value)
339
340     """
341     ssconf_lock = utils.FileLock.Open(constants.SSCONF_LOCK_FILE)
342
343     # Get lock while writing files
344     ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
345     try:
346       for name, value in values.iteritems():
347         if value and not value.endswith("\n"):
348           value += "\n"
349         if len(value) > self._MAX_SIZE:
350           raise errors.ConfigurationError("ssconf file %s above maximum size" %
351                                           name)
352         utils.WriteFile(self.KeyToFilename(name), data=value,
353                         mode=constants.SS_FILE_PERMS)
354     finally:
355       ssconf_lock.Unlock()
356
357   def GetFileList(self):
358     """Return the list of all config files.
359
360     This is used for computing node replication data.
361
362     """
363     return [self.KeyToFilename(key) for key in self._VALID_KEYS]
364
365   def GetClusterName(self):
366     """Get the cluster name.
367
368     """
369     return self._ReadFile(constants.SS_CLUSTER_NAME)
370
371   def GetFileStorageDir(self):
372     """Get the file storage dir.
373
374     """
375     return self._ReadFile(constants.SS_FILE_STORAGE_DIR)
376
377   def GetSharedFileStorageDir(self):
378     """Get the shared file storage dir.
379
380     """
381     return self._ReadFile(constants.SS_SHARED_FILE_STORAGE_DIR)
382
383   def GetMasterCandidates(self):
384     """Return the list of master candidates.
385
386     """
387     data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
388     nl = data.splitlines(False)
389     return nl
390
391   def GetMasterCandidatesIPList(self):
392     """Return the list of master candidates' primary IP.
393
394     """
395     data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
396     nl = data.splitlines(False)
397     return nl
398
399   def GetMasterIP(self):
400     """Get the IP of the master node for this cluster.
401
402     """
403     return self._ReadFile(constants.SS_MASTER_IP)
404
405   def GetMasterNetdev(self):
406     """Get the netdev to which we'll add the master ip.
407
408     """
409     return self._ReadFile(constants.SS_MASTER_NETDEV)
410
411   def GetMasterNode(self):
412     """Get the hostname of the master node for this cluster.
413
414     """
415     return self._ReadFile(constants.SS_MASTER_NODE)
416
417   def GetNodeList(self):
418     """Return the list of cluster nodes.
419
420     """
421     data = self._ReadFile(constants.SS_NODE_LIST)
422     nl = data.splitlines(False)
423     return nl
424
425   def GetNodePrimaryIPList(self):
426     """Return the list of cluster nodes' primary IP.
427
428     """
429     data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
430     nl = data.splitlines(False)
431     return nl
432
433   def GetNodeSecondaryIPList(self):
434     """Return the list of cluster nodes' secondary IP.
435
436     """
437     data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
438     nl = data.splitlines(False)
439     return nl
440
441   def GetNodegroupList(self):
442     """Return the list of nodegroups.
443
444     """
445     data = self._ReadFile(constants.SS_NODEGROUPS)
446     nl = data.splitlines(False)
447     return nl
448
449   def GetClusterTags(self):
450     """Return the cluster tags.
451
452     """
453     data = self._ReadFile(constants.SS_CLUSTER_TAGS)
454     nl = data.splitlines(False)
455     return nl
456
457   def GetHypervisorList(self):
458     """Return the list of enabled hypervisors.
459
460     """
461     data = self._ReadFile(constants.SS_HYPERVISOR_LIST)
462     nl = data.splitlines(False)
463     return nl
464
465   def GetMaintainNodeHealth(self):
466     """Return the value of the maintain_node_health option.
467
468     """
469     data = self._ReadFile(constants.SS_MAINTAIN_NODE_HEALTH)
470     # we rely on the bool serialization here
471     return data == "True"
472
473   def GetUidPool(self):
474     """Return the user-id pool definition string.
475
476     The separator character is a newline.
477
478     The return value can be parsed using uidpool.ParseUidPool()::
479
480       ss = ssconf.SimpleStore()
481       uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
482
483     """
484     data = self._ReadFile(constants.SS_UID_POOL)
485     return data
486
487   def GetPrimaryIPFamily(self):
488     """Return the cluster-wide primary address family.
489
490     """
491     try:
492       return int(self._ReadFile(constants.SS_PRIMARY_IP_FAMILY,
493                                 default=netutils.IP4Address.family))
494     except (ValueError, TypeError), err:
495       raise errors.ConfigurationError("Error while trying to parse primary ip"
496                                       " family: %s" % err)
497
498
499 def GetMasterAndMyself(ss=None):
500   """Get the master node and my own hostname.
501
502   This can be either used for a 'soft' check (compared to CheckMaster,
503   which exits) or just for computing both at the same time.
504
505   The function does not handle any errors, these should be handled in
506   the caller (errors.ConfigurationError, errors.ResolverError).
507
508   @param ss: either a sstore.SimpleConfigReader or a
509       sstore.SimpleStore instance
510   @rtype: tuple
511   @return: a tuple (master node name, my own name)
512
513   """
514   if ss is None:
515     ss = SimpleStore()
516   return ss.GetMasterNode(), netutils.Hostname.GetSysName()
517
518
519 def CheckMaster(debug, ss=None):
520   """Checks the node setup.
521
522   If this is the master, the function will return. Otherwise it will
523   exit with an exit code based on the node status.
524
525   """
526   try:
527     master_name, myself = GetMasterAndMyself(ss)
528   except errors.ConfigurationError, err:
529     print "Cluster configuration incomplete: '%s'" % str(err)
530     sys.exit(constants.EXIT_NODESETUP_ERROR)
531   except errors.ResolverError, err:
532     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
533     sys.exit(constants.EXIT_NODESETUP_ERROR)
534
535   if myself != master_name:
536     if debug:
537       sys.stderr.write("Not master, exiting.\n")
538     sys.exit(constants.EXIT_NOTMASTER)