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