KVM: Check instances for actual liveness
[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     )
286   _MAX_SIZE = 131072
287
288   def __init__(self, cfg_location=None):
289     if cfg_location is None:
290       self._cfg_dir = constants.DATA_DIR
291     else:
292       self._cfg_dir = cfg_location
293
294   def KeyToFilename(self, key):
295     """Convert a given key into filename.
296
297     """
298     if key not in self._VALID_KEYS:
299       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
300                                    % str(key))
301
302     filename = self._cfg_dir + '/' + self._SS_FILEPREFIX + key
303     return filename
304
305   def _ReadFile(self, key):
306     """Generic routine to read keys.
307
308     This will read the file which holds the value requested. Errors
309     will be changed into ConfigurationErrors.
310
311     """
312     filename = self.KeyToFilename(key)
313     try:
314       data = utils.ReadFile(filename, size=self._MAX_SIZE)
315     except EnvironmentError, err:
316       raise errors.ConfigurationError("Can't read from the ssconf file:"
317                                       " '%s'" % str(err))
318     data = data.rstrip('\n')
319     return data
320
321   def WriteFiles(self, values):
322     """Writes ssconf files used by external scripts.
323
324     @type values: dict
325     @param values: Dictionary of (name, value)
326
327     """
328     ssconf_lock = utils.FileLock.Open(constants.SSCONF_LOCK_FILE)
329
330     # Get lock while writing files
331     ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
332     try:
333       for name, value in values.iteritems():
334         if value and not value.endswith("\n"):
335           value += "\n"
336         utils.WriteFile(self.KeyToFilename(name), data=value, mode=0444)
337     finally:
338       ssconf_lock.Unlock()
339
340   def GetFileList(self):
341     """Return the list of all config files.
342
343     This is used for computing node replication data.
344
345     """
346     return [self.KeyToFilename(key) for key in self._VALID_KEYS]
347
348   def GetClusterName(self):
349     """Get the cluster name.
350
351     """
352     return self._ReadFile(constants.SS_CLUSTER_NAME)
353
354   def GetFileStorageDir(self):
355     """Get the file storage dir.
356
357     """
358     return self._ReadFile(constants.SS_FILE_STORAGE_DIR)
359
360   def GetMasterCandidates(self):
361     """Return the list of master candidates.
362
363     """
364     data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
365     nl = data.splitlines(False)
366     return nl
367
368   def GetMasterCandidatesIPList(self):
369     """Return the list of master candidates' primary IP.
370
371     """
372     data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
373     nl = data.splitlines(False)
374     return nl
375
376   def GetMasterIP(self):
377     """Get the IP of the master node for this cluster.
378
379     """
380     return self._ReadFile(constants.SS_MASTER_IP)
381
382   def GetMasterNetdev(self):
383     """Get the netdev to which we'll add the master ip.
384
385     """
386     return self._ReadFile(constants.SS_MASTER_NETDEV)
387
388   def GetMasterNode(self):
389     """Get the hostname of the master node for this cluster.
390
391     """
392     return self._ReadFile(constants.SS_MASTER_NODE)
393
394   def GetNodeList(self):
395     """Return the list of cluster nodes.
396
397     """
398     data = self._ReadFile(constants.SS_NODE_LIST)
399     nl = data.splitlines(False)
400     return nl
401
402   def GetNodePrimaryIPList(self):
403     """Return the list of cluster nodes' primary IP.
404
405     """
406     data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
407     nl = data.splitlines(False)
408     return nl
409
410   def GetNodeSecondaryIPList(self):
411     """Return the list of cluster nodes' secondary IP.
412
413     """
414     data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
415     nl = data.splitlines(False)
416     return nl
417
418   def GetClusterTags(self):
419     """Return the cluster tags.
420
421     """
422     data = self._ReadFile(constants.SS_CLUSTER_TAGS)
423     nl = data.splitlines(False)
424     return nl
425
426
427 def GetMasterAndMyself(ss=None):
428   """Get the master node and my own hostname.
429
430   This can be either used for a 'soft' check (compared to CheckMaster,
431   which exits) or just for computing both at the same time.
432
433   The function does not handle any errors, these should be handled in
434   the caller (errors.ConfigurationError, errors.ResolverError).
435
436   @param ss: either a sstore.SimpleConfigReader or a
437       sstore.SimpleStore instance
438   @rtype: tuple
439   @return: a tuple (master node name, my own name)
440
441   """
442   if ss is None:
443     ss = SimpleStore()
444   return ss.GetMasterNode(), utils.HostInfo().name
445
446
447 def CheckMaster(debug, ss=None):
448   """Checks the node setup.
449
450   If this is the master, the function will return. Otherwise it will
451   exit with an exit code based on the node status.
452
453   """
454   try:
455     master_name, myself = GetMasterAndMyself(ss)
456   except errors.ConfigurationError, err:
457     print "Cluster configuration incomplete: '%s'" % str(err)
458     sys.exit(constants.EXIT_NODESETUP_ERROR)
459   except errors.ResolverError, err:
460     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
461     sys.exit(constants.EXIT_NODESETUP_ERROR)
462
463   if myself != master_name:
464     if debug:
465       sys.stderr.write("Not master, exiting.\n")
466     sys.exit(constants.EXIT_NOTMASTER)
467
468
469 def CheckMasterCandidate(debug, ss=None):
470   """Checks the node setup.
471
472   If this is a master candidate, the function will return. Otherwise it will
473   exit with an exit code based on the node status.
474
475   """
476   try:
477     if ss is None:
478       ss = SimpleStore()
479     myself = utils.HostInfo().name
480     candidates = ss.GetMasterCandidates()
481   except errors.ConfigurationError, err:
482     print "Cluster configuration incomplete: '%s'" % str(err)
483     sys.exit(constants.EXIT_NODESETUP_ERROR)
484   except errors.ResolverError, err:
485     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
486     sys.exit(constants.EXIT_NODESETUP_ERROR)
487
488   if myself not in candidates:
489     if debug:
490       sys.stderr.write("Not master candidate, exiting.\n")
491     sys.exit(constants.EXIT_NOTCANDIDATE)