Barebones LXC hypervisor
[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     constants.SS_MAINTAIN_NODE_HEALTH,
287     constants.SS_UID_POOL,
288     )
289   _MAX_SIZE = 131072
290
291   def __init__(self, cfg_location=None):
292     if cfg_location is None:
293       self._cfg_dir = constants.DATA_DIR
294     else:
295       self._cfg_dir = cfg_location
296
297   def KeyToFilename(self, key):
298     """Convert a given key into filename.
299
300     """
301     if key not in self._VALID_KEYS:
302       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
303                                    % str(key))
304
305     filename = self._cfg_dir + '/' + self._SS_FILEPREFIX + key
306     return filename
307
308   def _ReadFile(self, key):
309     """Generic routine to read keys.
310
311     This will read the file which holds the value requested. Errors
312     will be changed into ConfigurationErrors.
313
314     """
315     filename = self.KeyToFilename(key)
316     try:
317       data = utils.ReadFile(filename, size=self._MAX_SIZE)
318     except EnvironmentError, err:
319       raise errors.ConfigurationError("Can't read from the ssconf file:"
320                                       " '%s'" % str(err))
321     data = data.rstrip('\n')
322     return data
323
324   def WriteFiles(self, values):
325     """Writes ssconf files used by external scripts.
326
327     @type values: dict
328     @param values: Dictionary of (name, value)
329
330     """
331     ssconf_lock = utils.FileLock.Open(constants.SSCONF_LOCK_FILE)
332
333     # Get lock while writing files
334     ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
335     try:
336       for name, value in values.iteritems():
337         if value and not value.endswith("\n"):
338           value += "\n"
339         if len(value) > self._MAX_SIZE:
340           raise errors.ConfigurationError("ssconf file %s above maximum size" %
341                                           name)
342         utils.WriteFile(self.KeyToFilename(name), data=value, mode=0444)
343     finally:
344       ssconf_lock.Unlock()
345
346   def GetFileList(self):
347     """Return the list of all config files.
348
349     This is used for computing node replication data.
350
351     """
352     return [self.KeyToFilename(key) for key in self._VALID_KEYS]
353
354   def GetClusterName(self):
355     """Get the cluster name.
356
357     """
358     return self._ReadFile(constants.SS_CLUSTER_NAME)
359
360   def GetFileStorageDir(self):
361     """Get the file storage dir.
362
363     """
364     return self._ReadFile(constants.SS_FILE_STORAGE_DIR)
365
366   def GetMasterCandidates(self):
367     """Return the list of master candidates.
368
369     """
370     data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
371     nl = data.splitlines(False)
372     return nl
373
374   def GetMasterCandidatesIPList(self):
375     """Return the list of master candidates' primary IP.
376
377     """
378     data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
379     nl = data.splitlines(False)
380     return nl
381
382   def GetMasterIP(self):
383     """Get the IP of the master node for this cluster.
384
385     """
386     return self._ReadFile(constants.SS_MASTER_IP)
387
388   def GetMasterNetdev(self):
389     """Get the netdev to which we'll add the master ip.
390
391     """
392     return self._ReadFile(constants.SS_MASTER_NETDEV)
393
394   def GetMasterNode(self):
395     """Get the hostname of the master node for this cluster.
396
397     """
398     return self._ReadFile(constants.SS_MASTER_NODE)
399
400   def GetNodeList(self):
401     """Return the list of cluster nodes.
402
403     """
404     data = self._ReadFile(constants.SS_NODE_LIST)
405     nl = data.splitlines(False)
406     return nl
407
408   def GetNodePrimaryIPList(self):
409     """Return the list of cluster nodes' primary IP.
410
411     """
412     data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
413     nl = data.splitlines(False)
414     return nl
415
416   def GetNodeSecondaryIPList(self):
417     """Return the list of cluster nodes' secondary IP.
418
419     """
420     data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
421     nl = data.splitlines(False)
422     return nl
423
424   def GetClusterTags(self):
425     """Return the cluster tags.
426
427     """
428     data = self._ReadFile(constants.SS_CLUSTER_TAGS)
429     nl = data.splitlines(False)
430     return nl
431
432   def GetHypervisorList(self):
433     """Return the list of enabled hypervisors.
434
435     """
436     data = self._ReadFile(constants.SS_HYPERVISOR_LIST)
437     nl = data.splitlines(False)
438     return nl
439
440   def GetMaintainNodeHealth(self):
441     """Return the value of the maintain_node_health option.
442
443     """
444     data = self._ReadFile(constants.SS_MAINTAIN_NODE_HEALTH)
445     # we rely on the bool serialization here
446     return data == "True"
447
448   def GetUidPool(self):
449     """Return the user-id pool definition string.
450
451     The separator character is a newline.
452
453     The return value can be parsed using uidpool.ParseUidPool()::
454
455       ss = ssconf.SimpleStore()
456       uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
457
458     """
459     data = self._ReadFile(constants.SS_UID_POOL)
460     return data
461
462
463 def GetMasterAndMyself(ss=None):
464   """Get the master node and my own hostname.
465
466   This can be either used for a 'soft' check (compared to CheckMaster,
467   which exits) or just for computing both at the same time.
468
469   The function does not handle any errors, these should be handled in
470   the caller (errors.ConfigurationError, errors.ResolverError).
471
472   @param ss: either a sstore.SimpleConfigReader or a
473       sstore.SimpleStore instance
474   @rtype: tuple
475   @return: a tuple (master node name, my own name)
476
477   """
478   if ss is None:
479     ss = SimpleStore()
480   return ss.GetMasterNode(), utils.HostInfo().name
481
482
483 def CheckMaster(debug, ss=None):
484   """Checks the node setup.
485
486   If this is the master, the function will return. Otherwise it will
487   exit with an exit code based on the node status.
488
489   """
490   try:
491     master_name, myself = GetMasterAndMyself(ss)
492   except errors.ConfigurationError, err:
493     print "Cluster configuration incomplete: '%s'" % str(err)
494     sys.exit(constants.EXIT_NODESETUP_ERROR)
495   except errors.ResolverError, err:
496     sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
497     sys.exit(constants.EXIT_NODESETUP_ERROR)
498
499   if myself != master_name:
500     if debug:
501       sys.stderr.write("Not master, exiting.\n")
502     sys.exit(constants.EXIT_NOTMASTER)