ssconf.py 15.2 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
#
Iustin Pop's avatar
Iustin Pop committed
2
3
#

4
# Copyright (C) 2006, 2007, 2008, 2010 Google Inc.
Iustin Pop's avatar
Iustin Pop committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Global Configuration data for Ganeti.

This module provides the interface to a special case of cluster
configuration data, which is mostly static and available to all nodes.

"""

29
import sys
30
import re
31
import os
32
import errno
Iustin Pop's avatar
Iustin Pop committed
33
34
35

from ganeti import errors
from ganeti import constants
36
from ganeti import utils
37
from ganeti import serializer
38
from ganeti import objects
39
from ganeti import netutils
Iustin Pop's avatar
Iustin Pop committed
40
41


42
43
SSCONF_LOCK_TIMEOUT = 10

Iustin Pop's avatar
Iustin Pop committed
44
RE_VALID_SSCONF_NAME = re.compile(r"^[-_a-z0-9]+$")
45

46

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
47
class SimpleConfigReader(object):
48
49
50
51
52
53
54
55
56
57
58
  """Simple class to read configuration file.

  """
  def __init__(self, file_name=constants.CLUSTER_CONF_FILE):
    """Initializes this class.

    @type file_name: string
    @param file_name: Configuration file path

    """
    self._file_name = file_name
59
60
61
    self._last_inode = None
    self._last_mtime = None
    self._last_size = None
62
63
64

    self._config_data = None
    self._inst_ips_by_link = None
65
    self._ip_to_inst_by_link = None
66
    self._instances_ips = None
67
68
69
    self._mc_primary_ips = None
    self._nodes_primary_ips = None

70
71
    # we need a forced reload at class init time, to initialize _last_*
    self._Load(force=True)
Guido Trotter's avatar
Guido Trotter committed
72

73
  def _Load(self, force=False):
74
    """Loads (or reloads) the config file.
Guido Trotter's avatar
Guido Trotter committed
75

76
77
78
    @type force: boolean
    @param force: whether to force the reload without checking the mtime
    @rtype: boolean
79
80
    @return: boolean value that says whether we reloaded the configuration or
             not (because we decided it was already up-to-date)
81

Guido Trotter's avatar
Guido Trotter committed
82
    """
83
84
85
86
87
    try:
      cfg_stat = os.stat(self._file_name)
    except EnvironmentError, err:
      raise errors.ConfigurationError("Cannot stat config file %s: %s" %
                                      (self._file_name, err))
88
89
90
91
    inode = cfg_stat.st_ino
    mtime = cfg_stat.st_mtime
    size = cfg_stat.st_size

92
93
94
    if (force or inode != self._last_inode or
        mtime > self._last_mtime or
        size != self._last_size):
95
96
97
      self._last_inode = inode
      self._last_mtime = mtime
      self._last_size = size
98
99
    else:
      # Don't reload
100
101
      return False

102
    try:
Guido Trotter's avatar
Guido Trotter committed
103
      self._config_data = serializer.Load(utils.ReadFile(self._file_name))
104
    except EnvironmentError, err:
105
      raise errors.ConfigurationError("Cannot read config file %s: %s" %
Guido Trotter's avatar
Guido Trotter committed
106
                                      (self._file_name, err))
107
108
    except ValueError, err:
      raise errors.ConfigurationError("Cannot load config file %s: %s" %
Guido Trotter's avatar
Guido Trotter committed
109
                                      (self._file_name, err))
110

111
    self._ip_to_inst_by_link = {}
112
    self._instances_ips = []
113
    self._inst_ips_by_link = {}
Iustin Pop's avatar
Iustin Pop committed
114
115
116
117
118
119
120
121
122
123
124
    c_nparams = self._config_data["cluster"]["nicparams"][constants.PP_DEFAULT]
    for iname in self._config_data["instances"]:
      instance = self._config_data["instances"][iname]
      for nic in instance["nics"]:
        if "ip" in nic and nic["ip"]:
          params = objects.FillDict(c_nparams, nic["nicparams"])
          if not params["link"] in self._inst_ips_by_link:
            self._inst_ips_by_link[params["link"]] = []
            self._ip_to_inst_by_link[params["link"]] = {}
          self._ip_to_inst_by_link[params["link"]][nic["ip"]] = iname
          self._inst_ips_by_link[params["link"]].append(nic["ip"])
125

126
127
128
129
130
131
132
133
    self._nodes_primary_ips = []
    self._mc_primary_ips = []
    for node_name in self._config_data["nodes"]:
      node = self._config_data["nodes"][node_name]
      self._nodes_primary_ips.append(node["primary_ip"])
      if node["master_candidate"]:
        self._mc_primary_ips.append(node["primary_ip"])

134
135
    return True

136
137
138
139
  # Clients can request a reload of the config file, so we export our internal
  # _Load function as Reload.
  Reload = _Load

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
  def GetClusterName(self):
    return self._config_data["cluster"]["cluster_name"]

  def GetHostKey(self):
    return self._config_data["cluster"]["rsahostkeypub"]

  def GetMasterNode(self):
    return self._config_data["cluster"]["master_node"]

  def GetMasterIP(self):
    return self._config_data["cluster"]["master_ip"]

  def GetMasterNetdev(self):
    return self._config_data["cluster"]["master_netdev"]

  def GetFileStorageDir(self):
    return self._config_data["cluster"]["file_storage_dir"]

158
159
160
  def GetSharedFileStorageDir(self):
    return self._config_data["cluster"]["shared_file_storage_dir"]

161
162
163
  def GetNodeList(self):
    return self._config_data["nodes"].keys()

164
165
166
167
168
169
  def GetConfigSerialNo(self):
    return self._config_data["serial_no"]

  def GetClusterSerialNo(self):
    return self._config_data["cluster"]["serial_no"]

170
171
172
173
174
175
  def GetDefaultNicParams(self):
    return self._config_data["cluster"]["nicparams"][constants.PP_DEFAULT]

  def GetDefaultNicLink(self):
    return self.GetDefaultNicParams()[constants.NIC_LINK]

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
  def GetNodeStatusFlags(self, node):
    """Get a node's status flags

    @type node: string
    @param node: node name
    @rtype: (bool, bool, bool)
    @return: (master_candidate, drained, offline) (or None if no such node)

    """
    if node not in self._config_data["nodes"]:
      return None

    master_candidate = self._config_data["nodes"][node]["master_candidate"]
    drained = self._config_data["nodes"][node]["drained"]
    offline = self._config_data["nodes"][node]["offline"]
    return master_candidate, drained, offline

193
  def GetInstanceByLinkIp(self, ip, link):
194
195
196
197
198
199
200
201
202
203
    """Get instance name from its link and ip address.

    @type ip: string
    @param ip: ip address
    @type link: string
    @param link: nic link
    @rtype: string
    @return: instance name

    """
204
205
206
    if not link:
      link = self.GetDefaultNicLink()
    if not link in self._ip_to_inst_by_link:
207
      return None
208
209
210
    if not ip in self._ip_to_inst_by_link[link]:
      return None
    return self._ip_to_inst_by_link[link][ip]
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237

  def GetNodePrimaryIp(self, node):
    """Get a node's primary ip

    @type node: string
    @param node: node name
    @rtype: string, or None
    @return: node's primary ip, or None if no such node

    """
    if node not in self._config_data["nodes"]:
      return None
    return self._config_data["nodes"][node]["primary_ip"]

  def GetInstancePrimaryNode(self, instance):
    """Get an instance's primary node

    @type instance: string
    @param instance: instance name
    @rtype: string, or None
    @return: primary node, or None if no such instance

    """
    if instance not in self._config_data["instances"]:
      return None
    return self._config_data["instances"][instance]["primary_node"]

238
239
240
241
242
243
  def GetNodesPrimaryIps(self):
    return self._nodes_primary_ips

  def GetMasterCandidatesPrimaryIps(self):
    return self._mc_primary_ips

244
  def GetInstancesIps(self, link):
245
246
247
248
249
250
251
252
    """Get list of nic ips connected to a certain link.

    @type link: string
    @param link: nic link
    @rtype: list
    @return: list of ips connected to that link

    """
253
254
255
    if not link:
      link = self.GetDefaultNicLink()

256
257
258
259
    if link in self._inst_ips_by_link:
      return self._inst_ips_by_link[link]
    else:
      return []
260

261

Iustin Pop's avatar
Iustin Pop committed
262
263
264
265
266
267
268
269
270
271
272
273
274
275
class SimpleStore(object):
  """Interface to static cluster data.

  This is different that the config.ConfigWriter and
  SimpleConfigReader classes in that it holds data that will always be
  present, even on nodes which don't have all the cluster data.

  Other particularities of the datastore:
    - keys are restricted to predefined values

  """
  _SS_FILEPREFIX = "ssconf_"
  _VALID_KEYS = (
    constants.SS_CLUSTER_NAME,
276
    constants.SS_CLUSTER_TAGS,
Iustin Pop's avatar
Iustin Pop committed
277
    constants.SS_FILE_STORAGE_DIR,
278
    constants.SS_SHARED_FILE_STORAGE_DIR,
279
    constants.SS_MASTER_CANDIDATES,
280
    constants.SS_MASTER_CANDIDATES_IPS,
Iustin Pop's avatar
Iustin Pop committed
281
282
283
284
    constants.SS_MASTER_IP,
    constants.SS_MASTER_NETDEV,
    constants.SS_MASTER_NODE,
    constants.SS_NODE_LIST,
285
286
    constants.SS_NODE_PRIMARY_IPS,
    constants.SS_NODE_SECONDARY_IPS,
287
    constants.SS_OFFLINE_NODES,
288
    constants.SS_ONLINE_NODES,
289
    constants.SS_PRIMARY_IP_FAMILY,
290
    constants.SS_INSTANCE_LIST,
291
    constants.SS_RELEASE_VERSION,
292
    constants.SS_HYPERVISOR_LIST,
293
    constants.SS_MAINTAIN_NODE_HEALTH,
Balazs Lecz's avatar
Balazs Lecz committed
294
    constants.SS_UID_POOL,
295
    constants.SS_NODEGROUPS,
Iustin Pop's avatar
Iustin Pop committed
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
    )
  _MAX_SIZE = 131072

  def __init__(self, cfg_location=None):
    if cfg_location is None:
      self._cfg_dir = constants.DATA_DIR
    else:
      self._cfg_dir = cfg_location

  def KeyToFilename(self, key):
    """Convert a given key into filename.

    """
    if key not in self._VALID_KEYS:
      raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
                                   % str(key))

Iustin Pop's avatar
Iustin Pop committed
313
    filename = self._cfg_dir + "/" + self._SS_FILEPREFIX + key
Iustin Pop's avatar
Iustin Pop committed
314
315
    return filename

316
  def _ReadFile(self, key, default=None):
Iustin Pop's avatar
Iustin Pop committed
317
318
319
320
321
322
323
324
    """Generic routine to read keys.

    This will read the file which holds the value requested. Errors
    will be changed into ConfigurationErrors.

    """
    filename = self.KeyToFilename(key)
    try:
325
      data = utils.ReadFile(filename, size=self._MAX_SIZE)
Iustin Pop's avatar
Iustin Pop committed
326
    except EnvironmentError, err:
327
328
      if err.errno == errno.ENOENT and default is not None:
        return default
Iustin Pop's avatar
Iustin Pop committed
329
330
      raise errors.ConfigurationError("Can't read from the ssconf file:"
                                      " '%s'" % str(err))
Iustin Pop's avatar
Iustin Pop committed
331
    data = data.rstrip("\n")
Iustin Pop's avatar
Iustin Pop committed
332
333
    return data

334
335
336
337
338
339
340
  def WriteFiles(self, values):
    """Writes ssconf files used by external scripts.

    @type values: dict
    @param values: Dictionary of (name, value)

    """
341
    ssconf_lock = utils.FileLock.Open(constants.SSCONF_LOCK_FILE)
342
343
344
345
346

    # Get lock while writing files
    ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT)
    try:
      for name, value in values.iteritems():
347
        if value and not value.endswith("\n"):
348
          value += "\n"
349
350
351
        if len(value) > self._MAX_SIZE:
          raise errors.ConfigurationError("ssconf file %s above maximum size" %
                                          name)
352
353
        utils.WriteFile(self.KeyToFilename(name), data=value,
                        mode=constants.SS_FILE_PERMS)
354
355
356
    finally:
      ssconf_lock.Unlock()

Iustin Pop's avatar
Iustin Pop committed
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
  def GetFileList(self):
    """Return the list of all config files.

    This is used for computing node replication data.

    """
    return [self.KeyToFilename(key) for key in self._VALID_KEYS]

  def GetClusterName(self):
    """Get the cluster name.

    """
    return self._ReadFile(constants.SS_CLUSTER_NAME)

  def GetFileStorageDir(self):
    """Get the file storage dir.

    """
    return self._ReadFile(constants.SS_FILE_STORAGE_DIR)

377
378
379
380
381
382
  def GetSharedFileStorageDir(self):
    """Get the shared file storage dir.

    """
    return self._ReadFile(constants.SS_SHARED_FILE_STORAGE_DIR)

383
384
385
386
387
388
389
390
  def GetMasterCandidates(self):
    """Return the list of master candidates.

    """
    data = self._ReadFile(constants.SS_MASTER_CANDIDATES)
    nl = data.splitlines(False)
    return nl

391
392
393
394
395
396
397
398
  def GetMasterCandidatesIPList(self):
    """Return the list of master candidates' primary IP.

    """
    data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS)
    nl = data.splitlines(False)
    return nl

Iustin Pop's avatar
Iustin Pop committed
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
  def GetMasterIP(self):
    """Get the IP of the master node for this cluster.

    """
    return self._ReadFile(constants.SS_MASTER_IP)

  def GetMasterNetdev(self):
    """Get the netdev to which we'll add the master ip.

    """
    return self._ReadFile(constants.SS_MASTER_NETDEV)

  def GetMasterNode(self):
    """Get the hostname of the master node for this cluster.

    """
    return self._ReadFile(constants.SS_MASTER_NODE)

  def GetNodeList(self):
    """Return the list of cluster nodes.

    """
    data = self._ReadFile(constants.SS_NODE_LIST)
    nl = data.splitlines(False)
    return nl

425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
  def GetNodePrimaryIPList(self):
    """Return the list of cluster nodes' primary IP.

    """
    data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS)
    nl = data.splitlines(False)
    return nl

  def GetNodeSecondaryIPList(self):
    """Return the list of cluster nodes' secondary IP.

    """
    data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS)
    nl = data.splitlines(False)
    return nl

441
442
443
444
445
446
447
448
  def GetNodegroupList(self):
    """Return the list of nodegroups.

    """
    data = self._ReadFile(constants.SS_NODEGROUPS)
    nl = data.splitlines(False)
    return nl

449
450
451
452
453
454
455
456
  def GetClusterTags(self):
    """Return the cluster tags.

    """
    data = self._ReadFile(constants.SS_CLUSTER_TAGS)
    nl = data.splitlines(False)
    return nl

457
458
459
460
461
462
463
464
  def GetHypervisorList(self):
    """Return the list of enabled hypervisors.

    """
    data = self._ReadFile(constants.SS_HYPERVISOR_LIST)
    nl = data.splitlines(False)
    return nl

465
466
467
468
469
470
471
472
  def GetMaintainNodeHealth(self):
    """Return the value of the maintain_node_health option.

    """
    data = self._ReadFile(constants.SS_MAINTAIN_NODE_HEALTH)
    # we rely on the bool serialization here
    return data == "True"

Balazs Lecz's avatar
Balazs Lecz committed
473
474
475
476
477
  def GetUidPool(self):
    """Return the user-id pool definition string.

    The separator character is a newline.

478
479
    The return value can be parsed using uidpool.ParseUidPool()::

Balazs Lecz's avatar
Balazs Lecz committed
480
      ss = ssconf.SimpleStore()
481
      uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
Balazs Lecz's avatar
Balazs Lecz committed
482
483
484
485
486

    """
    data = self._ReadFile(constants.SS_UID_POOL)
    return data

487
488
489
490
491
  def GetPrimaryIPFamily(self):
    """Return the cluster-wide primary address family.

    """
    try:
492
493
      return int(self._ReadFile(constants.SS_PRIMARY_IP_FAMILY,
                                default=netutils.IP4Address.family))
494
495
496
497
    except (ValueError, TypeError), err:
      raise errors.ConfigurationError("Error while trying to parse primary ip"
                                      " family: %s" % err)

Iustin Pop's avatar
Iustin Pop committed
498

499
500
501
502
503
504
505
506
507
def GetMasterAndMyself(ss=None):
  """Get the master node and my own hostname.

  This can be either used for a 'soft' check (compared to CheckMaster,
  which exits) or just for computing both at the same time.

  The function does not handle any errors, these should be handled in
  the caller (errors.ConfigurationError, errors.ResolverError).

508
509
510
511
512
  @param ss: either a sstore.SimpleConfigReader or a
      sstore.SimpleStore instance
  @rtype: tuple
  @return: a tuple (master node name, my own name)

513
514
  """
  if ss is None:
Iustin Pop's avatar
Iustin Pop committed
515
    ss = SimpleStore()
516
  return ss.GetMasterNode(), netutils.Hostname.GetSysName()
517

Iustin Pop's avatar
Iustin Pop committed
518

519
def CheckMaster(debug, ss=None):
520
521
522
523
524
525
526
  """Checks the node setup.

  If this is the master, the function will return. Otherwise it will
  exit with an exit code based on the node status.

  """
  try:
527
    master_name, myself = GetMasterAndMyself(ss)
528
529
530
531
532
533
534
  except errors.ConfigurationError, err:
    print "Cluster configuration incomplete: '%s'" % str(err)
    sys.exit(constants.EXIT_NODESETUP_ERROR)
  except errors.ResolverError, err:
    sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0])
    sys.exit(constants.EXIT_NODESETUP_ERROR)

535
  if myself != master_name:
536
537
538
    if debug:
      sys.stderr.write("Not master, exiting.\n")
    sys.exit(constants.EXIT_NOTMASTER)