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