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