Update "FIXME" string in RAPI
[ganeti-local] / lib / ssconf.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012 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 errno
31 import logging
32
33 from ganeti import compat
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import utils
37 from ganeti import netutils
38 from ganeti import pathutils
39
40
41 SSCONF_LOCK_TIMEOUT = 10
42
43 #: Valid ssconf keys
44 _VALID_KEYS = compat.UniqueFrozenset([
45   constants.SS_CLUSTER_NAME,
46   constants.SS_CLUSTER_TAGS,
47   constants.SS_FILE_STORAGE_DIR,
48   constants.SS_SHARED_FILE_STORAGE_DIR,
49   constants.SS_MASTER_CANDIDATES,
50   constants.SS_MASTER_CANDIDATES_IPS,
51   constants.SS_MASTER_IP,
52   constants.SS_MASTER_NETDEV,
53   constants.SS_MASTER_NETMASK,
54   constants.SS_MASTER_NODE,
55   constants.SS_NODE_LIST,
56   constants.SS_NODE_PRIMARY_IPS,
57   constants.SS_NODE_SECONDARY_IPS,
58   constants.SS_OFFLINE_NODES,
59   constants.SS_ONLINE_NODES,
60   constants.SS_PRIMARY_IP_FAMILY,
61   constants.SS_INSTANCE_LIST,
62   constants.SS_RELEASE_VERSION,
63   constants.SS_HYPERVISOR_LIST,
64   constants.SS_MAINTAIN_NODE_HEALTH,
65   constants.SS_UID_POOL,
66   constants.SS_NODEGROUPS,
67   constants.SS_NETWORKS,
68   ])
69
70 #: Maximum size for ssconf files
71 _MAX_SIZE = 128 * 1024
72
73
74 def ReadSsconfFile(filename):
75   """Reads an ssconf file and verifies its size.
76
77   @type filename: string
78   @param filename: Path to file
79   @rtype: string
80   @return: File contents without newlines at the end
81   @raise RuntimeError: When the file size exceeds L{_MAX_SIZE}
82
83   """
84   statcb = utils.FileStatHelper()
85
86   data = utils.ReadFile(filename, size=_MAX_SIZE, preread=statcb)
87
88   if statcb.st.st_size > _MAX_SIZE:
89     msg = ("File '%s' has a size of %s bytes (up to %s allowed)" %
90            (filename, statcb.st.st_size, _MAX_SIZE))
91     raise RuntimeError(msg)
92
93   return data.rstrip("\n")
94
95
96 class SimpleStore(object):
97   """Interface to static cluster data.
98
99   This is different that the config.ConfigWriter and
100   SimpleConfigReader classes in that it holds data that will always be
101   present, even on nodes which don't have all the cluster data.
102
103   Other particularities of the datastore:
104     - keys are restricted to predefined values
105
106   """
107   def __init__(self, cfg_location=None, _lockfile=pathutils.SSCONF_LOCK_FILE):
108     if cfg_location is None:
109       self._cfg_dir = pathutils.DATA_DIR
110     else:
111       self._cfg_dir = cfg_location
112
113     self._lockfile = _lockfile
114
115   def KeyToFilename(self, key):
116     """Convert a given key into filename.
117
118     """
119     if key not in _VALID_KEYS:
120       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
121                                    % str(key))
122
123     filename = self._cfg_dir + "/" + constants.SSCONF_FILEPREFIX + key
124     return filename
125
126   def _ReadFile(self, key, default=None):
127     """Generic routine to read keys.
128
129     This will read the file which holds the value requested. Errors
130     will be changed into ConfigurationErrors.
131
132     """
133     filename = self.KeyToFilename(key)
134     try:
135       return ReadSsconfFile(filename)
136     except EnvironmentError, err:
137       if err.errno == errno.ENOENT and default is not None:
138         return default
139       raise errors.ConfigurationError("Can't read ssconf file %s: %s" %
140                                       (filename, str(err)))
141
142   def ReadAll(self):
143     """Reads all keys and returns their values.
144
145     @rtype: dict
146     @return: Dictionary, ssconf key as key, value as value
147
148     """
149     result = []
150
151     for key in _VALID_KEYS:
152       try:
153         value = self._ReadFile(key)
154       except errors.ConfigurationError:
155         # Ignore non-existing files
156         pass
157       else:
158         result.append((key, value))
159
160     return dict(result)
161
162   def WriteFiles(self, values, dry_run=False):
163     """Writes ssconf files used by external scripts.
164
165     @type values: dict
166     @param values: Dictionary of (name, value)
167     @type dry_run boolean
168     @param dry_run: Whether to perform a dry run
169
170     """
171     ssconf_lock = utils.FileLock.Open(self._lockfile)
172
173     # Get lock while writing files
174     ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
175     try:
176       for name, value in values.iteritems():
177         if value and not value.endswith("\n"):
178           value += "\n"
179
180         if len(value) > _MAX_SIZE:
181           msg = ("Value '%s' has a length of %s bytes, but only up to %s are"
182                  " allowed" % (name, len(value), _MAX_SIZE))
183           raise errors.ConfigurationError(msg)
184
185         utils.WriteFile(self.KeyToFilename(name), data=value,
186                         mode=constants.SS_FILE_PERMS,
187                         dry_run=dry_run)
188     finally:
189       ssconf_lock.Unlock()
190
191   def GetFileList(self):
192     """Return the list of all config files.
193
194     This is used for computing node replication data.
195
196     """
197     return [self.KeyToFilename(key) for key in _VALID_KEYS]
198
199   def GetClusterName(self):
200     """Get the cluster name.
201
202     """
203     return self._ReadFile(constants.SS_CLUSTER_NAME)
204
205   def GetFileStorageDir(self):
206     """Get the file storage dir.
207
208     """
209     return self._ReadFile(constants.SS_FILE_STORAGE_DIR)
210
211   def GetSharedFileStorageDir(self):
212     """Get the shared file storage dir.
213
214     """
215     return self._ReadFile(constants.SS_SHARED_FILE_STORAGE_DIR)
216
217   def GetMasterCandidates(self):
218     """Return the list of master candidates.
219
220     """
221     data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
222     nl = data.splitlines(False)
223     return nl
224
225   def GetMasterCandidatesIPList(self):
226     """Return the list of master candidates' primary IP.
227
228     """
229     data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
230     nl = data.splitlines(False)
231     return nl
232
233   def GetMasterIP(self):
234     """Get the IP of the master node for this cluster.
235
236     """
237     return self._ReadFile(constants.SS_MASTER_IP)
238
239   def GetMasterNetdev(self):
240     """Get the netdev to which we'll add the master ip.
241
242     """
243     return self._ReadFile(constants.SS_MASTER_NETDEV)
244
245   def GetMasterNetmask(self):
246     """Get the master netmask.
247
248     """
249     try:
250       return self._ReadFile(constants.SS_MASTER_NETMASK)
251     except errors.ConfigurationError:
252       family = self.GetPrimaryIPFamily()
253       ipcls = netutils.IPAddress.GetClassFromIpFamily(family)
254       return ipcls.iplen
255
256   def GetMasterNode(self):
257     """Get the hostname of the master node for this cluster.
258
259     """
260     return self._ReadFile(constants.SS_MASTER_NODE)
261
262   def GetNodeList(self):
263     """Return the list of cluster nodes.
264
265     """
266     data = self._ReadFile(constants.SS_NODE_LIST)
267     nl = data.splitlines(False)
268     return nl
269
270   def GetNodePrimaryIPList(self):
271     """Return the list of cluster nodes' primary IP.
272
273     """
274     data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
275     nl = data.splitlines(False)
276     return nl
277
278   def GetNodeSecondaryIPList(self):
279     """Return the list of cluster nodes' secondary IP.
280
281     """
282     data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
283     nl = data.splitlines(False)
284     return nl
285
286   def GetNodegroupList(self):
287     """Return the list of nodegroups.
288
289     """
290     data = self._ReadFile(constants.SS_NODEGROUPS)
291     nl = data.splitlines(False)
292     return nl
293
294   def GetNetworkList(self):
295     """Return the list of networks.
296
297     """
298     data = self._ReadFile(constants.SS_NETWORKS)
299     nl = data.splitlines(False)
300     return nl
301
302   def GetClusterTags(self):
303     """Return the cluster tags.
304
305     """
306     data = self._ReadFile(constants.SS_CLUSTER_TAGS)
307     nl = data.splitlines(False)
308     return nl
309
310   def GetHypervisorList(self):
311     """Return the list of enabled hypervisors.
312
313     """
314     data = self._ReadFile(constants.SS_HYPERVISOR_LIST)
315     nl = data.splitlines(False)
316     return nl
317
318   def GetMaintainNodeHealth(self):
319     """Return the value of the maintain_node_health option.
320
321     """
322     data = self._ReadFile(constants.SS_MAINTAIN_NODE_HEALTH)
323     # we rely on the bool serialization here
324     return data == "True"
325
326   def GetUidPool(self):
327     """Return the user-id pool definition string.
328
329     The separator character is a newline.
330
331     The return value can be parsed using uidpool.ParseUidPool()::
332
333       ss = ssconf.SimpleStore()
334       uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
335
336     """
337     data = self._ReadFile(constants.SS_UID_POOL)
338     return data
339
340   def GetPrimaryIPFamily(self):
341     """Return the cluster-wide primary address family.
342
343     """
344     try:
345       return int(self._ReadFile(constants.SS_PRIMARY_IP_FAMILY,
346                                 default=netutils.IP4Address.family))
347     except (ValueError, TypeError), err:
348       raise errors.ConfigurationError("Error while trying to parse primary IP"
349                                       " family: %s" % err)
350
351
352 def WriteSsconfFiles(values, dry_run=False):
353   """Update all ssconf files.
354
355   Wrapper around L{SimpleStore.WriteFiles}.
356
357   """
358   SimpleStore().WriteFiles(values, dry_run=dry_run)
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(), netutils.Hostname.GetSysName()
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 VerifyClusterName(name, _cfg_location=None):
404   """Verifies cluster name against a local cluster name.
405
406   @type name: string
407   @param name: Cluster name
408
409   """
410   sstore = SimpleStore(cfg_location=_cfg_location)
411
412   try:
413     local_name = sstore.GetClusterName()
414   except errors.ConfigurationError, err:
415     logging.debug("Can't get local cluster name: %s", err)
416   else:
417     if name != local_name:
418       raise errors.GenericError("Current cluster name is '%s'" % local_name)
419
420
421 def VerifyKeys(keys):
422   """Raises an exception if unknown ssconf keys are given.
423
424   @type keys: sequence
425   @param keys: Key names to verify
426   @raise errors.GenericError: When invalid keys were found
427
428   """
429   invalid = frozenset(keys) - _VALID_KEYS
430   if invalid:
431     raise errors.GenericError("Invalid ssconf keys: %s" %
432                               utils.CommaJoin(sorted(invalid)))