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