verify-disks: Explicitely state nothing has to be done
[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 re
31 import os
32 import errno
33
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import utils
37 from ganeti import serializer
38 from ganeti import objects
39 from ganeti import netutils
40
41
42 SSCONF_LOCK_TIMEOUT = 10
43
44 RE_VALID_SSCONF_NAME = re.compile(r"^[-_a-z0-9]+$")
45
46
47 class SimpleConfigReader(object):
48   """Simple class to read configuration file.
49
50   """
51   def __init__(self, file_name=constants.CLUSTER_CONF_FILE):
52     """Initializes this class.
53
54     @type file_name: string
55     @param file_name: Configuration file path
56
57     """
58     self._file_name = file_name
59     self._last_inode = None
60     self._last_mtime = None
61     self._last_size = None
62
63     self._config_data = None
64     self._inst_ips_by_link = None
65     self._ip_to_inst_by_link = None
66     self._instances_ips = None
67     self._mc_primary_ips = None
68     self._nodes_primary_ips = None
69
70     # we need a forced reload at class init time, to initialize _last_*
71     self._Load(force=True)
72
73   def _Load(self, force=False):
74     """Loads (or reloads) the config file.
75
76     @type force: boolean
77     @param force: whether to force the reload without checking the mtime
78     @rtype: boolean
79     @return: boolean value that says whether we reloaded the configuration or
80              not (because we decided it was already up-to-date)
81
82     """
83     try:
84       cfg_stat = os.stat(self._file_name)
85     except EnvironmentError, err:
86       raise errors.ConfigurationError("Cannot stat config file %s: %s" %
87                                       (self._file_name, err))
88     inode = cfg_stat.st_ino
89     mtime = cfg_stat.st_mtime
90     size = cfg_stat.st_size
91
92     if (force or inode != self._last_inode or
93         mtime > self._last_mtime or
94         size != self._last_size):
95       self._last_inode = inode
96       self._last_mtime = mtime
97       self._last_size = size
98     else:
99       # Don't reload
100       return False
101
102     try:
103       self._config_data = serializer.Load(utils.ReadFile(self._file_name))
104     except EnvironmentError, err:
105       raise errors.ConfigurationError("Cannot read config file %s: %s" %
106                                       (self._file_name, err))
107     except ValueError, err:
108       raise errors.ConfigurationError("Cannot load config file %s: %s" %
109                                       (self._file_name, err))
110
111     self._ip_to_inst_by_link = {}
112     self._instances_ips = []
113     self._inst_ips_by_link = {}
114     c_nparams = self._config_data["cluster"]["nicparams"][constants.PP_DEFAULT]
115     for iname in self._config_data["instances"]:
116       instance = self._config_data["instances"][iname]
117       for nic in instance["nics"]:
118         if "ip" in nic and nic["ip"]:
119           params = objects.FillDict(c_nparams, nic["nicparams"])
120           if not params["link"] in self._inst_ips_by_link:
121             self._inst_ips_by_link[params["link"]] = []
122             self._ip_to_inst_by_link[params["link"]] = {}
123           self._ip_to_inst_by_link[params["link"]][nic["ip"]] = iname
124           self._inst_ips_by_link[params["link"]].append(nic["ip"])
125
126     self._nodes_primary_ips = []
127     self._mc_primary_ips = []
128     for node_name in self._config_data["nodes"]:
129       node = self._config_data["nodes"][node_name]
130       self._nodes_primary_ips.append(node["primary_ip"])
131       if node["master_candidate"]:
132         self._mc_primary_ips.append(node["primary_ip"])
133
134     return True
135
136   # Clients can request a reload of the config file, so we export our internal
137   # _Load function as Reload.
138   Reload = _Load
139
140   def GetClusterName(self):
141     return self._config_data["cluster"]["cluster_name"]
142
143   def GetHostKey(self):
144     return self._config_data["cluster"]["rsahostkeypub"]
145
146   def GetMasterNode(self):
147     return self._config_data["cluster"]["master_node"]
148
149   def GetMasterIP(self):
150     return self._config_data["cluster"]["master_ip"]
151
152   def GetMasterNetdev(self):
153     return self._config_data["cluster"]["master_netdev"]
154
155   def GetMasterNetmask(self):
156     return self._config_data["cluster"]["master_netmask"]
157
158   def GetFileStorageDir(self):
159     return self._config_data["cluster"]["file_storage_dir"]
160
161   def GetSharedFileStorageDir(self):
162     return self._config_data["cluster"]["shared_file_storage_dir"]
163
164   def GetNodeList(self):
165     return self._config_data["nodes"].keys()
166
167   def GetConfigSerialNo(self):
168     return self._config_data["serial_no"]
169
170   def GetClusterSerialNo(self):
171     return self._config_data["cluster"]["serial_no"]
172
173   def GetDefaultNicParams(self):
174     return self._config_data["cluster"]["nicparams"][constants.PP_DEFAULT]
175
176   def GetDefaultNicLink(self):
177     return self.GetDefaultNicParams()[constants.NIC_LINK]
178
179   def GetNodeStatusFlags(self, node):
180     """Get a node's status flags
181
182     @type node: string
183     @param node: node name
184     @rtype: (bool, bool, bool)
185     @return: (master_candidate, drained, offline) (or None if no such node)
186
187     """
188     if node not in self._config_data["nodes"]:
189       return None
190
191     master_candidate = self._config_data["nodes"][node]["master_candidate"]
192     drained = self._config_data["nodes"][node]["drained"]
193     offline = self._config_data["nodes"][node]["offline"]
194     return master_candidate, drained, offline
195
196   def GetInstanceByLinkIp(self, ip, link):
197     """Get instance name from its link and ip address.
198
199     @type ip: string
200     @param ip: ip address
201     @type link: string
202     @param link: nic link
203     @rtype: string
204     @return: instance name
205
206     """
207     if not link:
208       link = self.GetDefaultNicLink()
209     if not link in self._ip_to_inst_by_link:
210       return None
211     if not ip in self._ip_to_inst_by_link[link]:
212       return None
213     return self._ip_to_inst_by_link[link][ip]
214
215   def GetNodePrimaryIp(self, node):
216     """Get a node's primary ip
217
218     @type node: string
219     @param node: node name
220     @rtype: string, or None
221     @return: node's primary ip, or None if no such node
222
223     """
224     if node not in self._config_data["nodes"]:
225       return None
226     return self._config_data["nodes"][node]["primary_ip"]
227
228   def GetInstancePrimaryNode(self, instance):
229     """Get an instance's primary node
230
231     @type instance: string
232     @param instance: instance name
233     @rtype: string, or None
234     @return: primary node, or None if no such instance
235
236     """
237     if instance not in self._config_data["instances"]:
238       return None
239     return self._config_data["instances"][instance]["primary_node"]
240
241   def GetNodesPrimaryIps(self):
242     return self._nodes_primary_ips
243
244   def GetMasterCandidatesPrimaryIps(self):
245     return self._mc_primary_ips
246
247   def GetInstancesIps(self, link):
248     """Get list of nic ips connected to a certain link.
249
250     @type link: string
251     @param link: nic link
252     @rtype: list
253     @return: list of ips connected to that link
254
255     """
256     if not link:
257       link = self.GetDefaultNicLink()
258
259     if link in self._inst_ips_by_link:
260       return self._inst_ips_by_link[link]
261     else:
262       return []
263
264
265 class SimpleStore(object):
266   """Interface to static cluster data.
267
268   This is different that the config.ConfigWriter and
269   SimpleConfigReader classes in that it holds data that will always be
270   present, even on nodes which don't have all the cluster data.
271
272   Other particularities of the datastore:
273     - keys are restricted to predefined values
274
275   """
276   _VALID_KEYS = (
277     constants.SS_CLUSTER_NAME,
278     constants.SS_CLUSTER_TAGS,
279     constants.SS_FILE_STORAGE_DIR,
280     constants.SS_SHARED_FILE_STORAGE_DIR,
281     constants.SS_MASTER_CANDIDATES,
282     constants.SS_MASTER_CANDIDATES_IPS,
283     constants.SS_MASTER_IP,
284     constants.SS_MASTER_NETDEV,
285     constants.SS_MASTER_NETMASK,
286     constants.SS_MASTER_NODE,
287     constants.SS_NODE_LIST,
288     constants.SS_NODE_PRIMARY_IPS,
289     constants.SS_NODE_SECONDARY_IPS,
290     constants.SS_OFFLINE_NODES,
291     constants.SS_ONLINE_NODES,
292     constants.SS_PRIMARY_IP_FAMILY,
293     constants.SS_INSTANCE_LIST,
294     constants.SS_RELEASE_VERSION,
295     constants.SS_HYPERVISOR_LIST,
296     constants.SS_MAINTAIN_NODE_HEALTH,
297     constants.SS_UID_POOL,
298     constants.SS_NODEGROUPS,
299     )
300   _MAX_SIZE = 131072
301
302   def __init__(self, cfg_location=None):
303     if cfg_location is None:
304       self._cfg_dir = constants.DATA_DIR
305     else:
306       self._cfg_dir = cfg_location
307
308   def KeyToFilename(self, key):
309     """Convert a given key into filename.
310
311     """
312     if key not in self._VALID_KEYS:
313       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
314                                    % str(key))
315
316     filename = self._cfg_dir + "/" + constants.SSCONF_FILEPREFIX + key
317     return filename
318
319   def _ReadFile(self, key, default=None):
320     """Generic routine to read keys.
321
322     This will read the file which holds the value requested. Errors
323     will be changed into ConfigurationErrors.
324
325     """
326     filename = self.KeyToFilename(key)
327     try:
328       data = utils.ReadFile(filename, size=self._MAX_SIZE)
329     except EnvironmentError, err:
330       if err.errno == errno.ENOENT and default is not None:
331         return default
332       raise errors.ConfigurationError("Can't read from the ssconf file:"
333                                       " '%s'" % str(err))
334     data = data.rstrip("\n")
335     return data
336
337   def WriteFiles(self, values):
338     """Writes ssconf files used by external scripts.
339
340     @type values: dict
341     @param values: Dictionary of (name, value)
342
343     """
344     ssconf_lock = utils.FileLock.Open(constants.SSCONF_LOCK_FILE)
345
346     # Get lock while writing files
347     ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
348     try:
349       for name, value in values.iteritems():
350         if value and not value.endswith("\n"):
351           value += "\n"
352         if len(value) > self._MAX_SIZE:
353           raise errors.ConfigurationError("ssconf file %s above maximum size" %
354                                           name)
355         utils.WriteFile(self.KeyToFilename(name), data=value,
356                         mode=constants.SS_FILE_PERMS)
357     finally:
358       ssconf_lock.Unlock()
359
360   def GetFileList(self):
361     """Return the list of all config files.
362
363     This is used for computing node replication data.
364
365     """
366     return [self.KeyToFilename(key) for key in self._VALID_KEYS]
367
368   def GetClusterName(self):
369     """Get the cluster name.
370
371     """
372     return self._ReadFile(constants.SS_CLUSTER_NAME)
373
374   def GetFileStorageDir(self):
375     """Get the file storage dir.
376
377     """
378     return self._ReadFile(constants.SS_FILE_STORAGE_DIR)
379
380   def GetSharedFileStorageDir(self):
381     """Get the shared file storage dir.
382
383     """
384     return self._ReadFile(constants.SS_SHARED_FILE_STORAGE_DIR)
385
386   def GetMasterCandidates(self):
387     """Return the list of master candidates.
388
389     """
390     data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
391     nl = data.splitlines(False)
392     return nl
393
394   def GetMasterCandidatesIPList(self):
395     """Return the list of master candidates' primary IP.
396
397     """
398     data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
399     nl = data.splitlines(False)
400     return nl
401
402   def GetMasterIP(self):
403     """Get the IP of the master node for this cluster.
404
405     """
406     return self._ReadFile(constants.SS_MASTER_IP)
407
408   def GetMasterNetdev(self):
409     """Get the netdev to which we'll add the master ip.
410
411     """
412     return self._ReadFile(constants.SS_MASTER_NETDEV)
413
414   def GetMasterNetmask(self):
415     """Get the master netmask.
416
417     """
418     try:
419       return self._ReadFile(constants.SS_MASTER_NETMASK)
420     except errors.ConfigurationError:
421       family = self.GetPrimaryIPFamily()
422       ipcls = netutils.IPAddress.GetClassFromIpFamily(family)
423       return ipcls.iplen
424
425   def GetMasterNode(self):
426     """Get the hostname of the master node for this cluster.
427
428     """
429     return self._ReadFile(constants.SS_MASTER_NODE)
430
431   def GetNodeList(self):
432     """Return the list of cluster nodes.
433
434     """
435     data = self._ReadFile(constants.SS_NODE_LIST)
436     nl = data.splitlines(False)
437     return nl
438
439   def GetNodePrimaryIPList(self):
440     """Return the list of cluster nodes' primary IP.
441
442     """
443     data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
444     nl = data.splitlines(False)
445     return nl
446
447   def GetNodeSecondaryIPList(self):
448     """Return the list of cluster nodes' secondary IP.
449
450     """
451     data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
452     nl = data.splitlines(False)
453     return nl
454
455   def GetNodegroupList(self):
456     """Return the list of nodegroups.
457
458     """
459     data = self._ReadFile(constants.SS_NODEGROUPS)
460     nl = data.splitlines(False)
461     return nl
462
463   def GetClusterTags(self):
464     """Return the cluster tags.
465
466     """
467     data = self._ReadFile(constants.SS_CLUSTER_TAGS)
468     nl = data.splitlines(False)
469     return nl
470
471   def GetHypervisorList(self):
472     """Return the list of enabled hypervisors.
473
474     """
475     data = self._ReadFile(constants.SS_HYPERVISOR_LIST)
476     nl = data.splitlines(False)
477     return nl
478
479   def GetMaintainNodeHealth(self):
480     """Return the value of the maintain_node_health option.
481
482     """
483     data = self._ReadFile(constants.SS_MAINTAIN_NODE_HEALTH)
484     # we rely on the bool serialization here
485     return data == "True"
486
487   def GetUidPool(self):
488     """Return the user-id pool definition string.
489
490     The separator character is a newline.
491
492     The return value can be parsed using uidpool.ParseUidPool()::
493
494       ss = ssconf.SimpleStore()
495       uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
496
497     """
498     data = self._ReadFile(constants.SS_UID_POOL)
499     return data
500
501   def GetPrimaryIPFamily(self):
502     """Return the cluster-wide primary address family.
503
504     """
505     try:
506       return int(self._ReadFile(constants.SS_PRIMARY_IP_FAMILY,
507                                 default=netutils.IP4Address.family))
508     except (ValueError, TypeError), err:
509       raise errors.ConfigurationError("Error while trying to parse primary ip"
510                                       " family: %s" % err)
511
512
513 def GetMasterAndMyself(ss=None):
514   """Get the master node and my own hostname.
515
516   This can be either used for a 'soft' check (compared to CheckMaster,
517   which exits) or just for computing both at the same time.
518
519   The function does not handle any errors, these should be handled in
520   the caller (errors.ConfigurationError, errors.ResolverError).
521
522   @param ss: either a sstore.SimpleConfigReader or a
523       sstore.SimpleStore instance
524   @rtype: tuple
525   @return: a tuple (master node name, my own name)
526
527   """
528   if ss is None:
529     ss = SimpleStore()
530   return ss.GetMasterNode(), netutils.Hostname.GetSysName()
531
532
533 def CheckMaster(debug, ss=None):
534   """Checks the node setup.
535
536   If this is the master, the function will return. Otherwise it will
537   exit with an exit code based on the node status.
538
539   """
540   try:
541     master_name, myself = GetMasterAndMyself(ss)
542   except errors.ConfigurationError, err:
543     print "Cluster configuration incomplete: '%s'" % str(err)
544     sys.exit(constants.EXIT_NODESETUP_ERROR)
545   except errors.ResolverError, err:
546     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
547     sys.exit(constants.EXIT_NODESETUP_ERROR)
548
549   if myself != master_name:
550     if debug:
551       sys.stderr.write("Not master, exiting.\n")
552     sys.exit(constants.EXIT_NOTMASTER)