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