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