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