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