Code and docstring style fixes
[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._instances_ips = None
63     self._inst_ips_by_link = None
64     self._ip_to_instance = 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_instance = {}
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           self._instances_ips.append(nic['ip'])
118           self._ip_to_instance[nic['ip']] = iname
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._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 GetNodeStatusFlags(self, node):
166     """Get a node's status flags
167
168     @type node: string
169     @param node: node name
170     @rtype: (bool, bool, bool)
171     @return: (master_candidate, drained, offline) (or None if no such node)
172
173     """
174     if node not in self._config_data["nodes"]:
175       return None
176
177     master_candidate = self._config_data["nodes"][node]["master_candidate"]
178     drained = self._config_data["nodes"][node]["drained"]
179     offline = self._config_data["nodes"][node]["offline"]
180     return master_candidate, drained, offline
181
182   def GetInstanceByIp(self, ip):
183     if ip not in self._ip_to_instance:
184       return None
185     return self._ip_to_instance[ip]
186
187   def GetNodePrimaryIp(self, node):
188     """Get a node's primary ip
189
190     @type node: string
191     @param node: node name
192     @rtype: string, or None
193     @return: node's primary ip, or None if no such node
194
195     """
196     if node not in self._config_data["nodes"]:
197       return None
198     return self._config_data["nodes"][node]["primary_ip"]
199
200   def GetInstancePrimaryNode(self, instance):
201     """Get an instance's primary node
202
203     @type instance: string
204     @param instance: instance name
205     @rtype: string, or None
206     @return: primary node, or None if no such instance
207
208     """
209     if instance not in self._config_data["instances"]:
210       return None
211     return self._config_data["instances"][instance]["primary_node"]
212
213   def GetNodesPrimaryIps(self):
214     return self._nodes_primary_ips
215
216   def GetMasterCandidatesPrimaryIps(self):
217     return self._mc_primary_ips
218
219   def GetInstancesIps(self, link):
220     if link is None:
221       return self._instances_ips
222     if link in self._inst_ips_by_link:
223       return self._inst_ips_by_link[link]
224     else:
225       return []
226
227
228 class SimpleStore(object):
229   """Interface to static cluster data.
230
231   This is different that the config.ConfigWriter and
232   SimpleConfigReader classes in that it holds data that will always be
233   present, even on nodes which don't have all the cluster data.
234
235   Other particularities of the datastore:
236     - keys are restricted to predefined values
237
238   """
239   _SS_FILEPREFIX = "ssconf_"
240   _VALID_KEYS = (
241     constants.SS_CLUSTER_NAME,
242     constants.SS_CLUSTER_TAGS,
243     constants.SS_FILE_STORAGE_DIR,
244     constants.SS_MASTER_CANDIDATES,
245     constants.SS_MASTER_CANDIDATES_IPS,
246     constants.SS_MASTER_IP,
247     constants.SS_MASTER_NETDEV,
248     constants.SS_MASTER_NODE,
249     constants.SS_NODE_LIST,
250     constants.SS_NODE_PRIMARY_IPS,
251     constants.SS_NODE_SECONDARY_IPS,
252     constants.SS_OFFLINE_NODES,
253     constants.SS_ONLINE_NODES,
254     constants.SS_INSTANCE_LIST,
255     constants.SS_RELEASE_VERSION,
256     )
257   _MAX_SIZE = 131072
258
259   def __init__(self, cfg_location=None):
260     if cfg_location is None:
261       self._cfg_dir = constants.DATA_DIR
262     else:
263       self._cfg_dir = cfg_location
264
265   def KeyToFilename(self, key):
266     """Convert a given key into filename.
267
268     """
269     if key not in self._VALID_KEYS:
270       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
271                                    % str(key))
272
273     filename = self._cfg_dir + '/' + self._SS_FILEPREFIX + key
274     return filename
275
276   def _ReadFile(self, key):
277     """Generic routine to read keys.
278
279     This will read the file which holds the value requested. Errors
280     will be changed into ConfigurationErrors.
281
282     """
283     filename = self.KeyToFilename(key)
284     try:
285       data = utils.ReadFile(filename, size=self._MAX_SIZE)
286     except EnvironmentError, err:
287       raise errors.ConfigurationError("Can't read from the ssconf file:"
288                                       " '%s'" % str(err))
289     data = data.rstrip('\n')
290     return data
291
292   def WriteFiles(self, values):
293     """Writes ssconf files used by external scripts.
294
295     @type values: dict
296     @param values: Dictionary of (name, value)
297
298     """
299     ssconf_lock = utils.FileLock(constants.SSCONF_LOCK_FILE)
300
301     # Get lock while writing files
302     ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
303     try:
304       for name, value in values.iteritems():
305         if value and not value.endswith("\n"):
306           value += "\n"
307         utils.WriteFile(self.KeyToFilename(name), data=value, mode=0444)
308     finally:
309       ssconf_lock.Unlock()
310
311   def GetFileList(self):
312     """Return the list of all config files.
313
314     This is used for computing node replication data.
315
316     """
317     return [self.KeyToFilename(key) for key in self._VALID_KEYS]
318
319   def GetClusterName(self):
320     """Get the cluster name.
321
322     """
323     return self._ReadFile(constants.SS_CLUSTER_NAME)
324
325   def GetFileStorageDir(self):
326     """Get the file storage dir.
327
328     """
329     return self._ReadFile(constants.SS_FILE_STORAGE_DIR)
330
331   def GetMasterCandidates(self):
332     """Return the list of master candidates.
333
334     """
335     data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
336     nl = data.splitlines(False)
337     return nl
338
339   def GetMasterCandidatesIPList(self):
340     """Return the list of master candidates' primary IP.
341
342     """
343     data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
344     nl = data.splitlines(False)
345     return nl
346
347   def GetMasterIP(self):
348     """Get the IP of the master node for this cluster.
349
350     """
351     return self._ReadFile(constants.SS_MASTER_IP)
352
353   def GetMasterNetdev(self):
354     """Get the netdev to which we'll add the master ip.
355
356     """
357     return self._ReadFile(constants.SS_MASTER_NETDEV)
358
359   def GetMasterNode(self):
360     """Get the hostname of the master node for this cluster.
361
362     """
363     return self._ReadFile(constants.SS_MASTER_NODE)
364
365   def GetNodeList(self):
366     """Return the list of cluster nodes.
367
368     """
369     data = self._ReadFile(constants.SS_NODE_LIST)
370     nl = data.splitlines(False)
371     return nl
372
373   def GetNodePrimaryIPList(self):
374     """Return the list of cluster nodes' primary IP.
375
376     """
377     data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
378     nl = data.splitlines(False)
379     return nl
380
381   def GetNodeSecondaryIPList(self):
382     """Return the list of cluster nodes' secondary IP.
383
384     """
385     data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
386     nl = data.splitlines(False)
387     return nl
388
389   def GetClusterTags(self):
390     """Return the cluster tags.
391
392     """
393     data = self._ReadFile(constants.SS_CLUSTER_TAGS)
394     nl = data.splitlines(False)
395     return nl
396
397
398 def GetMasterAndMyself(ss=None):
399   """Get the master node and my own hostname.
400
401   This can be either used for a 'soft' check (compared to CheckMaster,
402   which exits) or just for computing both at the same time.
403
404   The function does not handle any errors, these should be handled in
405   the caller (errors.ConfigurationError, errors.ResolverError).
406
407   @param ss: either a sstore.SimpleConfigReader or a
408       sstore.SimpleStore instance
409   @rtype: tuple
410   @return: a tuple (master node name, my own name)
411
412   """
413   if ss is None:
414     ss = SimpleStore()
415   return ss.GetMasterNode(), utils.HostInfo().name
416
417
418 def CheckMaster(debug, ss=None):
419   """Checks the node setup.
420
421   If this is the master, the function will return. Otherwise it will
422   exit with an exit code based on the node status.
423
424   """
425   try:
426     master_name, myself = GetMasterAndMyself(ss)
427   except errors.ConfigurationError, err:
428     print "Cluster configuration incomplete: '%s'" % str(err)
429     sys.exit(constants.EXIT_NODESETUP_ERROR)
430   except errors.ResolverError, err:
431     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
432     sys.exit(constants.EXIT_NODESETUP_ERROR)
433
434   if myself != master_name:
435     if debug:
436       sys.stderr.write("Not master, exiting.\n")
437     sys.exit(constants.EXIT_NOTMASTER)
438
439
440 def CheckMasterCandidate(debug, ss=None):
441   """Checks the node setup.
442
443   If this is a master candidate, the function will return. Otherwise it will
444   exit with an exit code based on the node status.
445
446   """
447   try:
448     if ss is None:
449       ss = SimpleStore()
450     myself = utils.HostInfo().name
451     candidates = ss.GetMasterCandidates()
452   except errors.ConfigurationError, err:
453     print "Cluster configuration incomplete: '%s'" % str(err)
454     sys.exit(constants.EXIT_NODESETUP_ERROR)
455   except errors.ResolverError, err:
456     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
457     sys.exit(constants.EXIT_NODESETUP_ERROR)
458
459   if myself not in candidates:
460     if debug:
461       sys.stderr.write("Not master candidate, exiting.\n")
462     sys.exit(constants.EXIT_NOTCANDIDATE)
463