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