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