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