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