Statistics
| Branch: | Tag: | Revision:

root / lib / config.py @ 032a7d71

History | View | Annotate | Download (84.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 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
"""Configuration management for Ganeti
23

24
This module provides the interface to the Ganeti cluster configuration.
25

26
The configuration data is stored on every node but is updated on the master
27
only. After each update, the master distributes the data to the other nodes.
28

29
Currently, the data storage format is JSON. YAML was slow and consuming too
30
much memory.
31

32
"""
33

    
34
# pylint: disable=R0904
35
# R0904: Too many public methods
36

    
37
import os
38
import random
39
import logging
40
import time
41
import itertools
42

    
43
from ganeti import errors
44
from ganeti import locking
45
from ganeti import utils
46
from ganeti import constants
47
from ganeti import rpc
48
from ganeti import objects
49
from ganeti import serializer
50
from ganeti import uidpool
51
from ganeti import netutils
52
from ganeti import runtime
53
from ganeti import pathutils
54
from ganeti import network
55

    
56

    
57
_config_lock = locking.SharedLock("ConfigWriter")
58

    
59
# job id used for resource management at config upgrade time
60
_UPGRADE_CONFIG_JID = "jid-cfg-upgrade"
61

    
62

    
63
def _ValidateConfig(data):
64
  """Verifies that a configuration objects looks valid.
65

66
  This only verifies the version of the configuration.
67

68
  @raise errors.ConfigurationError: if the version differs from what
69
      we expect
70

71
  """
72
  if data.version != constants.CONFIG_VERSION:
73
    raise errors.ConfigVersionMismatch(constants.CONFIG_VERSION, data.version)
74

    
75

    
76
class TemporaryReservationManager:
77
  """A temporary resource reservation manager.
78

79
  This is used to reserve resources in a job, before using them, making sure
80
  other jobs cannot get them in the meantime.
81

82
  """
83
  def __init__(self):
84
    self._ec_reserved = {}
85

    
86
  def Reserved(self, resource):
87
    for holder_reserved in self._ec_reserved.values():
88
      if resource in holder_reserved:
89
        return True
90
    return False
91

    
92
  def Reserve(self, ec_id, resource):
93
    if self.Reserved(resource):
94
      raise errors.ReservationError("Duplicate reservation for resource '%s'"
95
                                    % str(resource))
96
    if ec_id not in self._ec_reserved:
97
      self._ec_reserved[ec_id] = set([resource])
98
    else:
99
      self._ec_reserved[ec_id].add(resource)
100

    
101
  def DropECReservations(self, ec_id):
102
    if ec_id in self._ec_reserved:
103
      del self._ec_reserved[ec_id]
104

    
105
  def GetReserved(self):
106
    all_reserved = set()
107
    for holder_reserved in self._ec_reserved.values():
108
      all_reserved.update(holder_reserved)
109
    return all_reserved
110

    
111
  def GetECReserved(self, ec_id):
112
    ec_reserved = set()
113
    if ec_id in self._ec_reserved:
114
      ec_reserved.update(self._ec_reserved[ec_id])
115
    return ec_reserved
116

    
117
  def Generate(self, existing, generate_one_fn, ec_id):
118
    """Generate a new resource of this type
119

120
    """
121
    assert callable(generate_one_fn)
122

    
123
    all_elems = self.GetReserved()
124
    all_elems.update(existing)
125
    retries = 64
126
    while retries > 0:
127
      new_resource = generate_one_fn()
128
      if new_resource is not None and new_resource not in all_elems:
129
        break
130
    else:
131
      raise errors.ConfigurationError("Not able generate new resource"
132
                                      " (last tried: %s)" % new_resource)
133
    self.Reserve(ec_id, new_resource)
134
    return new_resource
135

    
136

    
137
def _MatchNameComponentIgnoreCase(short_name, names):
138
  """Wrapper around L{utils.text.MatchNameComponent}.
139

140
  """
141
  return utils.MatchNameComponent(short_name, names, case_sensitive=False)
142

    
143

    
144
def _CheckInstanceDiskIvNames(disks):
145
  """Checks if instance's disks' C{iv_name} attributes are in order.
146

147
  @type disks: list of L{objects.Disk}
148
  @param disks: List of disks
149
  @rtype: list of tuples; (int, string, string)
150
  @return: List of wrongly named disks, each tuple contains disk index,
151
    expected and actual name
152

153
  """
154
  result = []
155

    
156
  for (idx, disk) in enumerate(disks):
157
    exp_iv_name = "disk/%s" % idx
158
    if disk.iv_name != exp_iv_name:
159
      result.append((idx, exp_iv_name, disk.iv_name))
160

    
161
  return result
162

    
163

    
164
class ConfigWriter:
165
  """The interface to the cluster configuration.
166

167
  @ivar _temporary_lvs: reservation manager for temporary LVs
168
  @ivar _all_rms: a list of all temporary reservation managers
169

170
  """
171
  def __init__(self, cfg_file=None, offline=False, _getents=runtime.GetEnts,
172
               accept_foreign=False):
173
    self.write_count = 0
174
    self._lock = _config_lock
175
    self._config_data = None
176
    self._offline = offline
177
    if cfg_file is None:
178
      self._cfg_file = pathutils.CLUSTER_CONF_FILE
179
    else:
180
      self._cfg_file = cfg_file
181
    self._getents = _getents
182
    self._temporary_ids = TemporaryReservationManager()
183
    self._temporary_drbds = {}
184
    self._temporary_macs = TemporaryReservationManager()
185
    self._temporary_secrets = TemporaryReservationManager()
186
    self._temporary_lvs = TemporaryReservationManager()
187
    self._temporary_ips = TemporaryReservationManager()
188
    self._all_rms = [self._temporary_ids, self._temporary_macs,
189
                     self._temporary_secrets, self._temporary_lvs,
190
                     self._temporary_ips]
191
    # Note: in order to prevent errors when resolving our name in
192
    # _DistributeConfig, we compute it here once and reuse it; it's
193
    # better to raise an error before starting to modify the config
194
    # file than after it was modified
195
    self._my_hostname = netutils.Hostname.GetSysName()
196
    self._last_cluster_serial = -1
197
    self._cfg_id = None
198
    self._context = None
199
    self._OpenConfig(accept_foreign)
200

    
201
  def _GetRpc(self, address_list):
202
    """Returns RPC runner for configuration.
203

204
    """
205
    return rpc.ConfigRunner(self._context, address_list)
206

    
207
  def SetContext(self, context):
208
    """Sets Ganeti context.
209

210
    """
211
    self._context = context
212

    
213
  # this method needs to be static, so that we can call it on the class
214
  @staticmethod
215
  def IsCluster():
216
    """Check if the cluster is configured.
217

218
    """
219
    return os.path.exists(pathutils.CLUSTER_CONF_FILE)
220

    
221
  @locking.ssynchronized(_config_lock, shared=1)
222
  def GetNdParams(self, node):
223
    """Get the node params populated with cluster defaults.
224

225
    @type node: L{objects.Node}
226
    @param node: The node we want to know the params for
227
    @return: A dict with the filled in node params
228

229
    """
230
    nodegroup = self._UnlockedGetNodeGroup(node.group)
231
    return self._config_data.cluster.FillND(node, nodegroup)
232

    
233
  @locking.ssynchronized(_config_lock, shared=1)
234
  def GetInstanceDiskParams(self, instance):
235
    """Get the disk params populated with inherit chain.
236

237
    @type instance: L{objects.Instance}
238
    @param instance: The instance we want to know the params for
239
    @return: A dict with the filled in disk params
240

241
    """
242
    node = self._UnlockedGetNodeInfo(instance.primary_node)
243
    nodegroup = self._UnlockedGetNodeGroup(node.group)
244
    return self._UnlockedGetGroupDiskParams(nodegroup)
245

    
246
  @locking.ssynchronized(_config_lock, shared=1)
247
  def GetGroupDiskParams(self, group):
248
    """Get the disk params populated with inherit chain.
249

250
    @type group: L{objects.NodeGroup}
251
    @param group: The group we want to know the params for
252
    @return: A dict with the filled in disk params
253

254
    """
255
    return self._UnlockedGetGroupDiskParams(group)
256

    
257
  def _UnlockedGetGroupDiskParams(self, group):
258
    """Get the disk params populated with inherit chain down to node-group.
259

260
    @type group: L{objects.NodeGroup}
261
    @param group: The group we want to know the params for
262
    @return: A dict with the filled in disk params
263

264
    """
265
    return self._config_data.cluster.SimpleFillDP(group.diskparams)
266

    
267
  def _UnlockedGetNetworkMACPrefix(self, net):
268
    """Return the network mac prefix if it exists or the cluster level default.
269

270
    """
271
    prefix = None
272
    if net:
273
      net_uuid = self._UnlockedLookupNetwork(net)
274
      if net_uuid:
275
        nobj = self._UnlockedGetNetwork(net_uuid)
276
        if nobj.mac_prefix:
277
          prefix = nobj.mac_prefix
278

    
279
    return prefix
280

    
281
  def _GenerateOneMAC(self, prefix=None):
282
    """Return a function that randomly generates a MAC suffic
283
       and appends it to the given prefix. If prefix is not given get
284
       the cluster level default.
285

286
    """
287
    if not prefix:
288
      prefix = self._config_data.cluster.mac_prefix
289

    
290
    def GenMac():
291
      byte1 = random.randrange(0, 256)
292
      byte2 = random.randrange(0, 256)
293
      byte3 = random.randrange(0, 256)
294
      mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
295
      return mac
296

    
297
    return GenMac
298

    
299
  @locking.ssynchronized(_config_lock, shared=1)
300
  def GenerateMAC(self, net, ec_id):
301
    """Generate a MAC for an instance.
302

303
    This should check the current instances for duplicates.
304

305
    """
306
    existing = self._AllMACs()
307
    prefix = self._UnlockedGetNetworkMACPrefix(net)
308
    gen_mac = self._GenerateOneMAC(prefix)
309
    return self._temporary_ids.Generate(existing, gen_mac, ec_id)
310

    
311
  @locking.ssynchronized(_config_lock, shared=1)
312
  def ReserveMAC(self, mac, ec_id):
313
    """Reserve a MAC for an instance.
314

315
    This only checks instances managed by this cluster, it does not
316
    check for potential collisions elsewhere.
317

318
    """
319
    all_macs = self._AllMACs()
320
    if mac in all_macs:
321
      raise errors.ReservationError("mac already in use")
322
    else:
323
      self._temporary_macs.Reserve(ec_id, mac)
324

    
325
  def _UnlockedCommitTemporaryIps(self, ec_id):
326
    """Commit all reserved IP address to their respective pools
327

328
    """
329
    for action, address, net_uuid in self._temporary_ips.GetECReserved(ec_id):
330
      self._UnlockedCommitIp(action, net_uuid, address)
331

    
332
  def _UnlockedCommitIp(self, action, net_uuid, address):
333
    """Commit a reserved IP address to an IP pool.
334

335
    The IP address is taken from the network's IP pool and marked as reserved.
336

337
    """
338
    nobj = self._UnlockedGetNetwork(net_uuid)
339
    pool = network.AddressPool(nobj)
340
    if action == 'reserve':
341
      pool.Reserve(address)
342
    elif action == 'release':
343
      pool.Release(address)
344

    
345
  def _UnlockedReleaseIp(self, net_uuid, address, ec_id):
346
    """Give a specific IP address back to an IP pool.
347

348
    The IP address is returned to the IP pool designated by pool_id and marked
349
    as reserved.
350

351
    """
352
    self._temporary_ips.Reserve(ec_id, ('release', address, net_uuid))
353

    
354
  @locking.ssynchronized(_config_lock, shared=1)
355
  def ReleaseIp(self, net, address, ec_id):
356
    """Give a specified IP address back to an IP pool.
357

358
    This is just a wrapper around _UnlockedReleaseIp.
359

360
    """
361
    net_uuid = self._UnlockedLookupNetwork(net)
362
    if net_uuid:
363
      self._UnlockedReleaseIp(net_uuid, address, ec_id)
364

    
365
  @locking.ssynchronized(_config_lock, shared=1)
366
  def GenerateIp(self, net, ec_id):
367
    """Find a free IPv4 address for an instance.
368

369
    """
370
    net_uuid = self._UnlockedLookupNetwork(net)
371
    nobj = self._UnlockedGetNetwork(net_uuid)
372
    pool = network.AddressPool(nobj)
373
    gen_free = pool.GenerateFree()
374

    
375
    def gen_one():
376
      try:
377
        ip = gen_free()
378
      except StopIteration:
379
        raise errors.ReservationError("Cannot generate IP. Network is full")
380
      return ("reserve", ip, net_uuid)
381

    
382
    _, address, _ = self._temporary_ips.Generate([], gen_one, ec_id)
383
    return address
384

    
385
  def _UnlockedReserveIp(self, net_uuid, address, ec_id):
386
    """Reserve a given IPv4 address for use by an instance.
387

388
    """
389
    nobj = self._UnlockedGetNetwork(net_uuid)
390
    pool = network.AddressPool(nobj)
391
    try:
392
      isreserved = pool.IsReserved(address)
393
    except errors.AddressPoolError:
394
      raise errors.ReservationError("IP address not in network")
395
    if isreserved:
396
      raise errors.ReservationError("IP address already in use")
397

    
398
    return self._temporary_ips.Reserve(ec_id, ('reserve', address, net_uuid))
399

    
400
  @locking.ssynchronized(_config_lock, shared=1)
401
  def ReserveIp(self, net, address, ec_id):
402
    """Reserve a given IPv4 address for use by an instance.
403

404
    """
405
    net_uuid = self._UnlockedLookupNetwork(net)
406
    if net_uuid:
407
      return self._UnlockedReserveIp(net_uuid, address, ec_id)
408

    
409
  @locking.ssynchronized(_config_lock, shared=1)
410
  def ReserveLV(self, lv_name, ec_id):
411
    """Reserve an VG/LV pair for an instance.
412

413
    @type lv_name: string
414
    @param lv_name: the logical volume name to reserve
415

416
    """
417
    all_lvs = self._AllLVs()
418
    if lv_name in all_lvs:
419
      raise errors.ReservationError("LV already in use")
420
    else:
421
      self._temporary_lvs.Reserve(ec_id, lv_name)
422

    
423
  @locking.ssynchronized(_config_lock, shared=1)
424
  def GenerateDRBDSecret(self, ec_id):
425
    """Generate a DRBD secret.
426

427
    This checks the current disks for duplicates.
428

429
    """
430
    return self._temporary_secrets.Generate(self._AllDRBDSecrets(),
431
                                            utils.GenerateSecret,
432
                                            ec_id)
433

    
434
  def _AllLVs(self):
435
    """Compute the list of all LVs.
436

437
    """
438
    lvnames = set()
439
    for instance in self._config_data.instances.values():
440
      node_data = instance.MapLVsByNode()
441
      for lv_list in node_data.values():
442
        lvnames.update(lv_list)
443
    return lvnames
444

    
445
  def _AllIDs(self, include_temporary):
446
    """Compute the list of all UUIDs and names we have.
447

448
    @type include_temporary: boolean
449
    @param include_temporary: whether to include the _temporary_ids set
450
    @rtype: set
451
    @return: a set of IDs
452

453
    """
454
    existing = set()
455
    if include_temporary:
456
      existing.update(self._temporary_ids.GetReserved())
457
    existing.update(self._AllLVs())
458
    existing.update(self._config_data.instances.keys())
459
    existing.update(self._config_data.nodes.keys())
460
    existing.update([i.uuid for i in self._AllUUIDObjects() if i.uuid])
461
    return existing
462

    
463
  def _GenerateUniqueID(self, ec_id):
464
    """Generate an unique UUID.
465

466
    This checks the current node, instances and disk names for
467
    duplicates.
468

469
    @rtype: string
470
    @return: the unique id
471

472
    """
473
    existing = self._AllIDs(include_temporary=False)
474
    return self._temporary_ids.Generate(existing, utils.NewUUID, ec_id)
475

    
476
  @locking.ssynchronized(_config_lock, shared=1)
477
  def GenerateUniqueID(self, ec_id):
478
    """Generate an unique ID.
479

480
    This is just a wrapper over the unlocked version.
481

482
    @type ec_id: string
483
    @param ec_id: unique id for the job to reserve the id to
484

485
    """
486
    return self._GenerateUniqueID(ec_id)
487

    
488
  def _AllMACs(self):
489
    """Return all MACs present in the config.
490

491
    @rtype: list
492
    @return: the list of all MACs
493

494
    """
495
    result = []
496
    for instance in self._config_data.instances.values():
497
      for nic in instance.nics:
498
        result.append(nic.mac)
499

    
500
    return result
501

    
502
  def _AllDRBDSecrets(self):
503
    """Return all DRBD secrets present in the config.
504

505
    @rtype: list
506
    @return: the list of all DRBD secrets
507

508
    """
509
    def helper(disk, result):
510
      """Recursively gather secrets from this disk."""
511
      if disk.dev_type == constants.DT_DRBD8:
512
        result.append(disk.logical_id[5])
513
      if disk.children:
514
        for child in disk.children:
515
          helper(child, result)
516

    
517
    result = []
518
    for instance in self._config_data.instances.values():
519
      for disk in instance.disks:
520
        helper(disk, result)
521

    
522
    return result
523

    
524
  def _CheckDiskIDs(self, disk, l_ids, p_ids):
525
    """Compute duplicate disk IDs
526

527
    @type disk: L{objects.Disk}
528
    @param disk: the disk at which to start searching
529
    @type l_ids: list
530
    @param l_ids: list of current logical ids
531
    @type p_ids: list
532
    @param p_ids: list of current physical ids
533
    @rtype: list
534
    @return: a list of error messages
535

536
    """
537
    result = []
538
    if disk.logical_id is not None:
539
      if disk.logical_id in l_ids:
540
        result.append("duplicate logical id %s" % str(disk.logical_id))
541
      else:
542
        l_ids.append(disk.logical_id)
543
    if disk.physical_id is not None:
544
      if disk.physical_id in p_ids:
545
        result.append("duplicate physical id %s" % str(disk.physical_id))
546
      else:
547
        p_ids.append(disk.physical_id)
548

    
549
    if disk.children:
550
      for child in disk.children:
551
        result.extend(self._CheckDiskIDs(child, l_ids, p_ids))
552
    return result
553

    
554
  def _UnlockedVerifyConfig(self):
555
    """Verify function.
556

557
    @rtype: list
558
    @return: a list of error messages; a non-empty list signifies
559
        configuration errors
560

561
    """
562
    # pylint: disable=R0914
563
    result = []
564
    seen_macs = []
565
    ports = {}
566
    data = self._config_data
567
    cluster = data.cluster
568
    seen_lids = []
569
    seen_pids = []
570

    
571
    # global cluster checks
572
    if not cluster.enabled_hypervisors:
573
      result.append("enabled hypervisors list doesn't have any entries")
574
    invalid_hvs = set(cluster.enabled_hypervisors) - constants.HYPER_TYPES
575
    if invalid_hvs:
576
      result.append("enabled hypervisors contains invalid entries: %s" %
577
                    invalid_hvs)
578
    missing_hvp = (set(cluster.enabled_hypervisors) -
579
                   set(cluster.hvparams.keys()))
580
    if missing_hvp:
581
      result.append("hypervisor parameters missing for the enabled"
582
                    " hypervisor(s) %s" % utils.CommaJoin(missing_hvp))
583

    
584
    if cluster.master_node not in data.nodes:
585
      result.append("cluster has invalid primary node '%s'" %
586
                    cluster.master_node)
587

    
588
    def _helper(owner, attr, value, template):
589
      try:
590
        utils.ForceDictType(value, template)
591
      except errors.GenericError, err:
592
        result.append("%s has invalid %s: %s" % (owner, attr, err))
593

    
594
    def _helper_nic(owner, params):
595
      try:
596
        objects.NIC.CheckParameterSyntax(params)
597
      except errors.ConfigurationError, err:
598
        result.append("%s has invalid nicparams: %s" % (owner, err))
599

    
600
    def _helper_ipolicy(owner, params, check_std):
601
      try:
602
        objects.InstancePolicy.CheckParameterSyntax(params, check_std)
603
      except errors.ConfigurationError, err:
604
        result.append("%s has invalid instance policy: %s" % (owner, err))
605

    
606
    def _helper_ispecs(owner, params):
607
      for key, value in params.items():
608
        if key in constants.IPOLICY_ISPECS:
609
          fullkey = "ipolicy/" + key
610
          _helper(owner, fullkey, value, constants.ISPECS_PARAMETER_TYPES)
611
        else:
612
          # FIXME: assuming list type
613
          if key in constants.IPOLICY_PARAMETERS:
614
            exp_type = float
615
          else:
616
            exp_type = list
617
          if not isinstance(value, exp_type):
618
            result.append("%s has invalid instance policy: for %s,"
619
                          " expecting %s, got %s" %
620
                          (owner, key, exp_type.__name__, type(value)))
621

    
622
    # check cluster parameters
623
    _helper("cluster", "beparams", cluster.SimpleFillBE({}),
624
            constants.BES_PARAMETER_TYPES)
625
    _helper("cluster", "nicparams", cluster.SimpleFillNIC({}),
626
            constants.NICS_PARAMETER_TYPES)
627
    _helper_nic("cluster", cluster.SimpleFillNIC({}))
628
    _helper("cluster", "ndparams", cluster.SimpleFillND({}),
629
            constants.NDS_PARAMETER_TYPES)
630
    _helper_ipolicy("cluster", cluster.SimpleFillIPolicy({}), True)
631
    _helper_ispecs("cluster", cluster.SimpleFillIPolicy({}))
632

    
633
    # per-instance checks
634
    for instance_name in data.instances:
635
      instance = data.instances[instance_name]
636
      if instance.name != instance_name:
637
        result.append("instance '%s' is indexed by wrong name '%s'" %
638
                      (instance.name, instance_name))
639
      if instance.primary_node not in data.nodes:
640
        result.append("instance '%s' has invalid primary node '%s'" %
641
                      (instance_name, instance.primary_node))
642
      for snode in instance.secondary_nodes:
643
        if snode not in data.nodes:
644
          result.append("instance '%s' has invalid secondary node '%s'" %
645
                        (instance_name, snode))
646
      for idx, nic in enumerate(instance.nics):
647
        if nic.mac in seen_macs:
648
          result.append("instance '%s' has NIC %d mac %s duplicate" %
649
                        (instance_name, idx, nic.mac))
650
        else:
651
          seen_macs.append(nic.mac)
652
        if nic.nicparams:
653
          filled = cluster.SimpleFillNIC(nic.nicparams)
654
          owner = "instance %s nic %d" % (instance.name, idx)
655
          _helper(owner, "nicparams",
656
                  filled, constants.NICS_PARAMETER_TYPES)
657
          _helper_nic(owner, filled)
658

    
659
      # parameter checks
660
      if instance.beparams:
661
        _helper("instance %s" % instance.name, "beparams",
662
                cluster.FillBE(instance), constants.BES_PARAMETER_TYPES)
663

    
664
      # gather the drbd ports for duplicate checks
665
      for (idx, dsk) in enumerate(instance.disks):
666
        if dsk.dev_type in constants.LDS_DRBD:
667
          tcp_port = dsk.logical_id[2]
668
          if tcp_port not in ports:
669
            ports[tcp_port] = []
670
          ports[tcp_port].append((instance.name, "drbd disk %s" % idx))
671
      # gather network port reservation
672
      net_port = getattr(instance, "network_port", None)
673
      if net_port is not None:
674
        if net_port not in ports:
675
          ports[net_port] = []
676
        ports[net_port].append((instance.name, "network port"))
677

    
678
      # instance disk verify
679
      for idx, disk in enumerate(instance.disks):
680
        result.extend(["instance '%s' disk %d error: %s" %
681
                       (instance.name, idx, msg) for msg in disk.Verify()])
682
        result.extend(self._CheckDiskIDs(disk, seen_lids, seen_pids))
683

    
684
      wrong_names = _CheckInstanceDiskIvNames(instance.disks)
685
      if wrong_names:
686
        tmp = "; ".join(("name of disk %s should be '%s', but is '%s'" %
687
                         (idx, exp_name, actual_name))
688
                        for (idx, exp_name, actual_name) in wrong_names)
689

    
690
        result.append("Instance '%s' has wrongly named disks: %s" %
691
                      (instance.name, tmp))
692

    
693
    # cluster-wide pool of free ports
694
    for free_port in cluster.tcpudp_port_pool:
695
      if free_port not in ports:
696
        ports[free_port] = []
697
      ports[free_port].append(("cluster", "port marked as free"))
698

    
699
    # compute tcp/udp duplicate ports
700
    keys = ports.keys()
701
    keys.sort()
702
    for pnum in keys:
703
      pdata = ports[pnum]
704
      if len(pdata) > 1:
705
        txt = utils.CommaJoin(["%s/%s" % val for val in pdata])
706
        result.append("tcp/udp port %s has duplicates: %s" % (pnum, txt))
707

    
708
    # highest used tcp port check
709
    if keys:
710
      if keys[-1] > cluster.highest_used_port:
711
        result.append("Highest used port mismatch, saved %s, computed %s" %
712
                      (cluster.highest_used_port, keys[-1]))
713

    
714
    if not data.nodes[cluster.master_node].master_candidate:
715
      result.append("Master node is not a master candidate")
716

    
717
    # master candidate checks
718
    mc_now, mc_max, _ = self._UnlockedGetMasterCandidateStats()
719
    if mc_now < mc_max:
720
      result.append("Not enough master candidates: actual %d, target %d" %
721
                    (mc_now, mc_max))
722

    
723
    # node checks
724
    for node_name, node in data.nodes.items():
725
      if node.name != node_name:
726
        result.append("Node '%s' is indexed by wrong name '%s'" %
727
                      (node.name, node_name))
728
      if [node.master_candidate, node.drained, node.offline].count(True) > 1:
729
        result.append("Node %s state is invalid: master_candidate=%s,"
730
                      " drain=%s, offline=%s" %
731
                      (node.name, node.master_candidate, node.drained,
732
                       node.offline))
733
      if node.group not in data.nodegroups:
734
        result.append("Node '%s' has invalid group '%s'" %
735
                      (node.name, node.group))
736
      else:
737
        _helper("node %s" % node.name, "ndparams",
738
                cluster.FillND(node, data.nodegroups[node.group]),
739
                constants.NDS_PARAMETER_TYPES)
740

    
741
    # nodegroups checks
742
    nodegroups_names = set()
743
    for nodegroup_uuid in data.nodegroups:
744
      nodegroup = data.nodegroups[nodegroup_uuid]
745
      if nodegroup.uuid != nodegroup_uuid:
746
        result.append("node group '%s' (uuid: '%s') indexed by wrong uuid '%s'"
747
                      % (nodegroup.name, nodegroup.uuid, nodegroup_uuid))
748
      if utils.UUID_RE.match(nodegroup.name.lower()):
749
        result.append("node group '%s' (uuid: '%s') has uuid-like name" %
750
                      (nodegroup.name, nodegroup.uuid))
751
      if nodegroup.name in nodegroups_names:
752
        result.append("duplicate node group name '%s'" % nodegroup.name)
753
      else:
754
        nodegroups_names.add(nodegroup.name)
755
      group_name = "group %s" % nodegroup.name
756
      _helper_ipolicy(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy),
757
                      False)
758
      _helper_ispecs(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy))
759
      if nodegroup.ndparams:
760
        _helper(group_name, "ndparams",
761
                cluster.SimpleFillND(nodegroup.ndparams),
762
                constants.NDS_PARAMETER_TYPES)
763

    
764
    # drbd minors check
765
    _, duplicates = self._UnlockedComputeDRBDMap()
766
    for node, minor, instance_a, instance_b in duplicates:
767
      result.append("DRBD minor %d on node %s is assigned twice to instances"
768
                    " %s and %s" % (minor, node, instance_a, instance_b))
769

    
770
    # IP checks
771
    default_nicparams = cluster.nicparams[constants.PP_DEFAULT]
772
    ips = {}
773

    
774
    def _AddIpAddress(ip, name):
775
      ips.setdefault(ip, []).append(name)
776

    
777
    _AddIpAddress(cluster.master_ip, "cluster_ip")
778

    
779
    for node in data.nodes.values():
780
      _AddIpAddress(node.primary_ip, "node:%s/primary" % node.name)
781
      if node.secondary_ip != node.primary_ip:
782
        _AddIpAddress(node.secondary_ip, "node:%s/secondary" % node.name)
783

    
784
    for instance in data.instances.values():
785
      for idx, nic in enumerate(instance.nics):
786
        if nic.ip is None:
787
          continue
788

    
789
        nicparams = objects.FillDict(default_nicparams, nic.nicparams)
790
        nic_mode = nicparams[constants.NIC_MODE]
791
        nic_link = nicparams[constants.NIC_LINK]
792

    
793
        if nic_mode == constants.NIC_MODE_BRIDGED:
794
          link = "bridge:%s" % nic_link
795
        elif nic_mode == constants.NIC_MODE_ROUTED:
796
          link = "route:%s" % nic_link
797
        else:
798
          raise errors.ProgrammerError("NIC mode '%s' not handled" % nic_mode)
799

    
800
        _AddIpAddress("%s/%s/%s" % (link, nic.ip, nic.network),
801
                      "instance:%s/nic:%d" % (instance.name, idx))
802

    
803
    for ip, owners in ips.items():
804
      if len(owners) > 1:
805
        result.append("IP address %s is used by multiple owners: %s" %
806
                      (ip, utils.CommaJoin(owners)))
807

    
808
    return result
809

    
810
  @locking.ssynchronized(_config_lock, shared=1)
811
  def VerifyConfig(self):
812
    """Verify function.
813

814
    This is just a wrapper over L{_UnlockedVerifyConfig}.
815

816
    @rtype: list
817
    @return: a list of error messages; a non-empty list signifies
818
        configuration errors
819

820
    """
821
    return self._UnlockedVerifyConfig()
822

    
823
  def _UnlockedSetDiskID(self, disk, node_name):
824
    """Convert the unique ID to the ID needed on the target nodes.
825

826
    This is used only for drbd, which needs ip/port configuration.
827

828
    The routine descends down and updates its children also, because
829
    this helps when the only the top device is passed to the remote
830
    node.
831

832
    This function is for internal use, when the config lock is already held.
833

834
    """
835
    if disk.children:
836
      for child in disk.children:
837
        self._UnlockedSetDiskID(child, node_name)
838

    
839
    if disk.logical_id is None and disk.physical_id is not None:
840
      return
841
    if disk.dev_type == constants.LD_DRBD8:
842
      pnode, snode, port, pminor, sminor, secret = disk.logical_id
843
      if node_name not in (pnode, snode):
844
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
845
                                        node_name)
846
      pnode_info = self._UnlockedGetNodeInfo(pnode)
847
      snode_info = self._UnlockedGetNodeInfo(snode)
848
      if pnode_info is None or snode_info is None:
849
        raise errors.ConfigurationError("Can't find primary or secondary node"
850
                                        " for %s" % str(disk))
851
      p_data = (pnode_info.secondary_ip, port)
852
      s_data = (snode_info.secondary_ip, port)
853
      if pnode == node_name:
854
        disk.physical_id = p_data + s_data + (pminor, secret)
855
      else: # it must be secondary, we tested above
856
        disk.physical_id = s_data + p_data + (sminor, secret)
857
    else:
858
      disk.physical_id = disk.logical_id
859
    return
860

    
861
  @locking.ssynchronized(_config_lock)
862
  def SetDiskID(self, disk, node_name):
863
    """Convert the unique ID to the ID needed on the target nodes.
864

865
    This is used only for drbd, which needs ip/port configuration.
866

867
    The routine descends down and updates its children also, because
868
    this helps when the only the top device is passed to the remote
869
    node.
870

871
    """
872
    return self._UnlockedSetDiskID(disk, node_name)
873

    
874
  @locking.ssynchronized(_config_lock)
875
  def AddTcpUdpPort(self, port):
876
    """Adds a new port to the available port pool.
877

878
    @warning: this method does not "flush" the configuration (via
879
        L{_WriteConfig}); callers should do that themselves once the
880
        configuration is stable
881

882
    """
883
    if not isinstance(port, int):
884
      raise errors.ProgrammerError("Invalid type passed for port")
885

    
886
    self._config_data.cluster.tcpudp_port_pool.add(port)
887

    
888
  @locking.ssynchronized(_config_lock, shared=1)
889
  def GetPortList(self):
890
    """Returns a copy of the current port list.
891

892
    """
893
    return self._config_data.cluster.tcpudp_port_pool.copy()
894

    
895
  @locking.ssynchronized(_config_lock)
896
  def AllocatePort(self):
897
    """Allocate a port.
898

899
    The port will be taken from the available port pool or from the
900
    default port range (and in this case we increase
901
    highest_used_port).
902

903
    """
904
    # If there are TCP/IP ports configured, we use them first.
905
    if self._config_data.cluster.tcpudp_port_pool:
906
      port = self._config_data.cluster.tcpudp_port_pool.pop()
907
    else:
908
      port = self._config_data.cluster.highest_used_port + 1
909
      if port >= constants.LAST_DRBD_PORT:
910
        raise errors.ConfigurationError("The highest used port is greater"
911
                                        " than %s. Aborting." %
912
                                        constants.LAST_DRBD_PORT)
913
      self._config_data.cluster.highest_used_port = port
914

    
915
    self._WriteConfig()
916
    return port
917

    
918
  def _UnlockedComputeDRBDMap(self):
919
    """Compute the used DRBD minor/nodes.
920

921
    @rtype: (dict, list)
922
    @return: dictionary of node_name: dict of minor: instance_name;
923
        the returned dict will have all the nodes in it (even if with
924
        an empty list), and a list of duplicates; if the duplicates
925
        list is not empty, the configuration is corrupted and its caller
926
        should raise an exception
927

928
    """
929
    def _AppendUsedPorts(instance_name, disk, used):
930
      duplicates = []
931
      if disk.dev_type == constants.LD_DRBD8 and len(disk.logical_id) >= 5:
932
        node_a, node_b, _, minor_a, minor_b = disk.logical_id[:5]
933
        for node, port in ((node_a, minor_a), (node_b, minor_b)):
934
          assert node in used, ("Node '%s' of instance '%s' not found"
935
                                " in node list" % (node, instance_name))
936
          if port in used[node]:
937
            duplicates.append((node, port, instance_name, used[node][port]))
938
          else:
939
            used[node][port] = instance_name
940
      if disk.children:
941
        for child in disk.children:
942
          duplicates.extend(_AppendUsedPorts(instance_name, child, used))
943
      return duplicates
944

    
945
    duplicates = []
946
    my_dict = dict((node, {}) for node in self._config_data.nodes)
947
    for instance in self._config_data.instances.itervalues():
948
      for disk in instance.disks:
949
        duplicates.extend(_AppendUsedPorts(instance.name, disk, my_dict))
950
    for (node, minor), instance in self._temporary_drbds.iteritems():
951
      if minor in my_dict[node] and my_dict[node][minor] != instance:
952
        duplicates.append((node, minor, instance, my_dict[node][minor]))
953
      else:
954
        my_dict[node][minor] = instance
955
    return my_dict, duplicates
956

    
957
  @locking.ssynchronized(_config_lock)
958
  def ComputeDRBDMap(self):
959
    """Compute the used DRBD minor/nodes.
960

961
    This is just a wrapper over L{_UnlockedComputeDRBDMap}.
962

963
    @return: dictionary of node_name: dict of minor: instance_name;
964
        the returned dict will have all the nodes in it (even if with
965
        an empty list).
966

967
    """
968
    d_map, duplicates = self._UnlockedComputeDRBDMap()
969
    if duplicates:
970
      raise errors.ConfigurationError("Duplicate DRBD ports detected: %s" %
971
                                      str(duplicates))
972
    return d_map
973

    
974
  @locking.ssynchronized(_config_lock)
975
  def AllocateDRBDMinor(self, nodes, instance):
976
    """Allocate a drbd minor.
977

978
    The free minor will be automatically computed from the existing
979
    devices. A node can be given multiple times in order to allocate
980
    multiple minors. The result is the list of minors, in the same
981
    order as the passed nodes.
982

983
    @type instance: string
984
    @param instance: the instance for which we allocate minors
985

986
    """
987
    assert isinstance(instance, basestring), \
988
           "Invalid argument '%s' passed to AllocateDRBDMinor" % instance
989

    
990
    d_map, duplicates = self._UnlockedComputeDRBDMap()
991
    if duplicates:
992
      raise errors.ConfigurationError("Duplicate DRBD ports detected: %s" %
993
                                      str(duplicates))
994
    result = []
995
    for nname in nodes:
996
      ndata = d_map[nname]
997
      if not ndata:
998
        # no minors used, we can start at 0
999
        result.append(0)
1000
        ndata[0] = instance
1001
        self._temporary_drbds[(nname, 0)] = instance
1002
        continue
1003
      keys = ndata.keys()
1004
      keys.sort()
1005
      ffree = utils.FirstFree(keys)
1006
      if ffree is None:
1007
        # return the next minor
1008
        # TODO: implement high-limit check
1009
        minor = keys[-1] + 1
1010
      else:
1011
        minor = ffree
1012
      # double-check minor against current instances
1013
      assert minor not in d_map[nname], \
1014
             ("Attempt to reuse allocated DRBD minor %d on node %s,"
1015
              " already allocated to instance %s" %
1016
              (minor, nname, d_map[nname][minor]))
1017
      ndata[minor] = instance
1018
      # double-check minor against reservation
1019
      r_key = (nname, minor)
1020
      assert r_key not in self._temporary_drbds, \
1021
             ("Attempt to reuse reserved DRBD minor %d on node %s,"
1022
              " reserved for instance %s" %
1023
              (minor, nname, self._temporary_drbds[r_key]))
1024
      self._temporary_drbds[r_key] = instance
1025
      result.append(minor)
1026
    logging.debug("Request to allocate drbd minors, input: %s, returning %s",
1027
                  nodes, result)
1028
    return result
1029

    
1030
  def _UnlockedReleaseDRBDMinors(self, instance):
1031
    """Release temporary drbd minors allocated for a given instance.
1032

1033
    @type instance: string
1034
    @param instance: the instance for which temporary minors should be
1035
                     released
1036

1037
    """
1038
    assert isinstance(instance, basestring), \
1039
           "Invalid argument passed to ReleaseDRBDMinors"
1040
    for key, name in self._temporary_drbds.items():
1041
      if name == instance:
1042
        del self._temporary_drbds[key]
1043

    
1044
  @locking.ssynchronized(_config_lock)
1045
  def ReleaseDRBDMinors(self, instance):
1046
    """Release temporary drbd minors allocated for a given instance.
1047

1048
    This should be called on the error paths, on the success paths
1049
    it's automatically called by the ConfigWriter add and update
1050
    functions.
1051

1052
    This function is just a wrapper over L{_UnlockedReleaseDRBDMinors}.
1053

1054
    @type instance: string
1055
    @param instance: the instance for which temporary minors should be
1056
                     released
1057

1058
    """
1059
    self._UnlockedReleaseDRBDMinors(instance)
1060

    
1061
  @locking.ssynchronized(_config_lock, shared=1)
1062
  def GetConfigVersion(self):
1063
    """Get the configuration version.
1064

1065
    @return: Config version
1066

1067
    """
1068
    return self._config_data.version
1069

    
1070
  @locking.ssynchronized(_config_lock, shared=1)
1071
  def GetClusterName(self):
1072
    """Get cluster name.
1073

1074
    @return: Cluster name
1075

1076
    """
1077
    return self._config_data.cluster.cluster_name
1078

    
1079
  @locking.ssynchronized(_config_lock, shared=1)
1080
  def GetMasterNode(self):
1081
    """Get the hostname of the master node for this cluster.
1082

1083
    @return: Master hostname
1084

1085
    """
1086
    return self._config_data.cluster.master_node
1087

    
1088
  @locking.ssynchronized(_config_lock, shared=1)
1089
  def GetMasterIP(self):
1090
    """Get the IP of the master node for this cluster.
1091

1092
    @return: Master IP
1093

1094
    """
1095
    return self._config_data.cluster.master_ip
1096

    
1097
  @locking.ssynchronized(_config_lock, shared=1)
1098
  def GetMasterNetdev(self):
1099
    """Get the master network device for this cluster.
1100

1101
    """
1102
    return self._config_data.cluster.master_netdev
1103

    
1104
  @locking.ssynchronized(_config_lock, shared=1)
1105
  def GetMasterNetmask(self):
1106
    """Get the netmask of the master node for this cluster.
1107

1108
    """
1109
    return self._config_data.cluster.master_netmask
1110

    
1111
  @locking.ssynchronized(_config_lock, shared=1)
1112
  def GetUseExternalMipScript(self):
1113
    """Get flag representing whether to use the external master IP setup script.
1114

1115
    """
1116
    return self._config_data.cluster.use_external_mip_script
1117

    
1118
  @locking.ssynchronized(_config_lock, shared=1)
1119
  def GetFileStorageDir(self):
1120
    """Get the file storage dir for this cluster.
1121

1122
    """
1123
    return self._config_data.cluster.file_storage_dir
1124

    
1125
  @locking.ssynchronized(_config_lock, shared=1)
1126
  def GetSharedFileStorageDir(self):
1127
    """Get the shared file storage dir for this cluster.
1128

1129
    """
1130
    return self._config_data.cluster.shared_file_storage_dir
1131

    
1132
  @locking.ssynchronized(_config_lock, shared=1)
1133
  def GetHypervisorType(self):
1134
    """Get the hypervisor type for this cluster.
1135

1136
    """
1137
    return self._config_data.cluster.enabled_hypervisors[0]
1138

    
1139
  @locking.ssynchronized(_config_lock, shared=1)
1140
  def GetHostKey(self):
1141
    """Return the rsa hostkey from the config.
1142

1143
    @rtype: string
1144
    @return: the rsa hostkey
1145

1146
    """
1147
    return self._config_data.cluster.rsahostkeypub
1148

    
1149
  @locking.ssynchronized(_config_lock, shared=1)
1150
  def GetDefaultIAllocator(self):
1151
    """Get the default instance allocator for this cluster.
1152

1153
    """
1154
    return self._config_data.cluster.default_iallocator
1155

    
1156
  @locking.ssynchronized(_config_lock, shared=1)
1157
  def GetPrimaryIPFamily(self):
1158
    """Get cluster primary ip family.
1159

1160
    @return: primary ip family
1161

1162
    """
1163
    return self._config_data.cluster.primary_ip_family
1164

    
1165
  @locking.ssynchronized(_config_lock, shared=1)
1166
  def GetMasterNetworkParameters(self):
1167
    """Get network parameters of the master node.
1168

1169
    @rtype: L{object.MasterNetworkParameters}
1170
    @return: network parameters of the master node
1171

1172
    """
1173
    cluster = self._config_data.cluster
1174
    result = objects.MasterNetworkParameters(
1175
      name=cluster.master_node, ip=cluster.master_ip,
1176
      netmask=cluster.master_netmask, netdev=cluster.master_netdev,
1177
      ip_family=cluster.primary_ip_family)
1178

    
1179
    return result
1180

    
1181
  @locking.ssynchronized(_config_lock)
1182
  def AddNodeGroup(self, group, ec_id, check_uuid=True):
1183
    """Add a node group to the configuration.
1184

1185
    This method calls group.UpgradeConfig() to fill any missing attributes
1186
    according to their default values.
1187

1188
    @type group: L{objects.NodeGroup}
1189
    @param group: the NodeGroup object to add
1190
    @type ec_id: string
1191
    @param ec_id: unique id for the job to use when creating a missing UUID
1192
    @type check_uuid: bool
1193
    @param check_uuid: add an UUID to the group if it doesn't have one or, if
1194
                       it does, ensure that it does not exist in the
1195
                       configuration already
1196

1197
    """
1198
    self._UnlockedAddNodeGroup(group, ec_id, check_uuid)
1199
    self._WriteConfig()
1200

    
1201
  def _UnlockedAddNodeGroup(self, group, ec_id, check_uuid):
1202
    """Add a node group to the configuration.
1203

1204
    """
1205
    logging.info("Adding node group %s to configuration", group.name)
1206

    
1207
    # Some code might need to add a node group with a pre-populated UUID
1208
    # generated with ConfigWriter.GenerateUniqueID(). We allow them to bypass
1209
    # the "does this UUID" exist already check.
1210
    if check_uuid:
1211
      self._EnsureUUID(group, ec_id)
1212

    
1213
    try:
1214
      existing_uuid = self._UnlockedLookupNodeGroup(group.name)
1215
    except errors.OpPrereqError:
1216
      pass
1217
    else:
1218
      raise errors.OpPrereqError("Desired group name '%s' already exists as a"
1219
                                 " node group (UUID: %s)" %
1220
                                 (group.name, existing_uuid),
1221
                                 errors.ECODE_EXISTS)
1222

    
1223
    group.serial_no = 1
1224
    group.ctime = group.mtime = time.time()
1225
    group.UpgradeConfig()
1226

    
1227
    self._config_data.nodegroups[group.uuid] = group
1228
    self._config_data.cluster.serial_no += 1
1229

    
1230
  @locking.ssynchronized(_config_lock)
1231
  def RemoveNodeGroup(self, group_uuid):
1232
    """Remove a node group from the configuration.
1233

1234
    @type group_uuid: string
1235
    @param group_uuid: the UUID of the node group to remove
1236

1237
    """
1238
    logging.info("Removing node group %s from configuration", group_uuid)
1239

    
1240
    if group_uuid not in self._config_data.nodegroups:
1241
      raise errors.ConfigurationError("Unknown node group '%s'" % group_uuid)
1242

    
1243
    assert len(self._config_data.nodegroups) != 1, \
1244
            "Group '%s' is the only group, cannot be removed" % group_uuid
1245

    
1246
    del self._config_data.nodegroups[group_uuid]
1247
    self._config_data.cluster.serial_no += 1
1248
    self._WriteConfig()
1249

    
1250
  def _UnlockedLookupNodeGroup(self, target):
1251
    """Lookup a node group's UUID.
1252

1253
    @type target: string or None
1254
    @param target: group name or UUID or None to look for the default
1255
    @rtype: string
1256
    @return: nodegroup UUID
1257
    @raises errors.OpPrereqError: when the target group cannot be found
1258

1259
    """
1260
    if target is None:
1261
      if len(self._config_data.nodegroups) != 1:
1262
        raise errors.OpPrereqError("More than one node group exists. Target"
1263
                                   " group must be specified explicitly.")
1264
      else:
1265
        return self._config_data.nodegroups.keys()[0]
1266
    if target in self._config_data.nodegroups:
1267
      return target
1268
    for nodegroup in self._config_data.nodegroups.values():
1269
      if nodegroup.name == target:
1270
        return nodegroup.uuid
1271
    raise errors.OpPrereqError("Node group '%s' not found" % target,
1272
                               errors.ECODE_NOENT)
1273

    
1274
  @locking.ssynchronized(_config_lock, shared=1)
1275
  def LookupNodeGroup(self, target):
1276
    """Lookup a node group's UUID.
1277

1278
    This function is just a wrapper over L{_UnlockedLookupNodeGroup}.
1279

1280
    @type target: string or None
1281
    @param target: group name or UUID or None to look for the default
1282
    @rtype: string
1283
    @return: nodegroup UUID
1284

1285
    """
1286
    return self._UnlockedLookupNodeGroup(target)
1287

    
1288
  def _UnlockedGetNodeGroup(self, uuid):
1289
    """Lookup a node group.
1290

1291
    @type uuid: string
1292
    @param uuid: group UUID
1293
    @rtype: L{objects.NodeGroup} or None
1294
    @return: nodegroup object, or None if not found
1295

1296
    """
1297
    if uuid not in self._config_data.nodegroups:
1298
      return None
1299

    
1300
    return self._config_data.nodegroups[uuid]
1301

    
1302
  @locking.ssynchronized(_config_lock, shared=1)
1303
  def GetNodeGroup(self, uuid):
1304
    """Lookup a node group.
1305

1306
    @type uuid: string
1307
    @param uuid: group UUID
1308
    @rtype: L{objects.NodeGroup} or None
1309
    @return: nodegroup object, or None if not found
1310

1311
    """
1312
    return self._UnlockedGetNodeGroup(uuid)
1313

    
1314
  @locking.ssynchronized(_config_lock, shared=1)
1315
  def GetAllNodeGroupsInfo(self):
1316
    """Get the configuration of all node groups.
1317

1318
    """
1319
    return dict(self._config_data.nodegroups)
1320

    
1321
  @locking.ssynchronized(_config_lock, shared=1)
1322
  def GetNodeGroupList(self):
1323
    """Get a list of node groups.
1324

1325
    """
1326
    return self._config_data.nodegroups.keys()
1327

    
1328
  @locking.ssynchronized(_config_lock, shared=1)
1329
  def GetNodeGroupMembersByNodes(self, nodes):
1330
    """Get nodes which are member in the same nodegroups as the given nodes.
1331

1332
    """
1333
    ngfn = lambda node_name: self._UnlockedGetNodeInfo(node_name).group
1334
    return frozenset(member_name
1335
                     for node_name in nodes
1336
                     for member_name in
1337
                       self._UnlockedGetNodeGroup(ngfn(node_name)).members)
1338

    
1339
  @locking.ssynchronized(_config_lock, shared=1)
1340
  def GetMultiNodeGroupInfo(self, group_uuids):
1341
    """Get the configuration of multiple node groups.
1342

1343
    @param group_uuids: List of node group UUIDs
1344
    @rtype: list
1345
    @return: List of tuples of (group_uuid, group_info)
1346

1347
    """
1348
    return [(uuid, self._UnlockedGetNodeGroup(uuid)) for uuid in group_uuids]
1349

    
1350
  @locking.ssynchronized(_config_lock)
1351
  def AddInstance(self, instance, ec_id):
1352
    """Add an instance to the config.
1353

1354
    This should be used after creating a new instance.
1355

1356
    @type instance: L{objects.Instance}
1357
    @param instance: the instance object
1358

1359
    """
1360
    if not isinstance(instance, objects.Instance):
1361
      raise errors.ProgrammerError("Invalid type passed to AddInstance")
1362

    
1363
    if instance.disk_template != constants.DT_DISKLESS:
1364
      all_lvs = instance.MapLVsByNode()
1365
      logging.info("Instance '%s' DISK_LAYOUT: %s", instance.name, all_lvs)
1366

    
1367
    all_macs = self._AllMACs()
1368
    for nic in instance.nics:
1369
      if nic.mac in all_macs:
1370
        raise errors.ConfigurationError("Cannot add instance %s:"
1371
                                        " MAC address '%s' already in use." %
1372
                                        (instance.name, nic.mac))
1373

    
1374
    self._EnsureUUID(instance, ec_id)
1375

    
1376
    instance.serial_no = 1
1377
    instance.ctime = instance.mtime = time.time()
1378
    self._config_data.instances[instance.name] = instance
1379
    self._config_data.cluster.serial_no += 1
1380
    self._UnlockedReleaseDRBDMinors(instance.name)
1381
    self._UnlockedCommitTemporaryIps(ec_id)
1382
    self._WriteConfig()
1383

    
1384
  def _EnsureUUID(self, item, ec_id):
1385
    """Ensures a given object has a valid UUID.
1386

1387
    @param item: the instance or node to be checked
1388
    @param ec_id: the execution context id for the uuid reservation
1389

1390
    """
1391
    if not item.uuid:
1392
      item.uuid = self._GenerateUniqueID(ec_id)
1393
    elif item.uuid in self._AllIDs(include_temporary=True):
1394
      raise errors.ConfigurationError("Cannot add '%s': UUID %s already"
1395
                                      " in use" % (item.name, item.uuid))
1396

    
1397
  def _SetInstanceStatus(self, instance_name, status):
1398
    """Set the instance's status to a given value.
1399

1400
    """
1401
    assert status in constants.ADMINST_ALL, \
1402
           "Invalid status '%s' passed to SetInstanceStatus" % (status,)
1403

    
1404
    if instance_name not in self._config_data.instances:
1405
      raise errors.ConfigurationError("Unknown instance '%s'" %
1406
                                      instance_name)
1407
    instance = self._config_data.instances[instance_name]
1408
    if instance.admin_state != status:
1409
      instance.admin_state = status
1410
      instance.serial_no += 1
1411
      instance.mtime = time.time()
1412
      self._WriteConfig()
1413

    
1414
  @locking.ssynchronized(_config_lock)
1415
  def MarkInstanceUp(self, instance_name):
1416
    """Mark the instance status to up in the config.
1417

1418
    """
1419
    self._SetInstanceStatus(instance_name, constants.ADMINST_UP)
1420

    
1421
  @locking.ssynchronized(_config_lock)
1422
  def MarkInstanceOffline(self, instance_name):
1423
    """Mark the instance status to down in the config.
1424

1425
    """
1426
    self._SetInstanceStatus(instance_name, constants.ADMINST_OFFLINE)
1427

    
1428
  @locking.ssynchronized(_config_lock)
1429
  def RemoveInstance(self, instance_name):
1430
    """Remove the instance from the configuration.
1431

1432
    """
1433
    if instance_name not in self._config_data.instances:
1434
      raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1435

    
1436
    # If a network port has been allocated to the instance,
1437
    # return it to the pool of free ports.
1438
    inst = self._config_data.instances[instance_name]
1439
    network_port = getattr(inst, "network_port", None)
1440
    if network_port is not None:
1441
      self._config_data.cluster.tcpudp_port_pool.add(network_port)
1442

    
1443
    instance = self._UnlockedGetInstanceInfo(instance_name)
1444

    
1445
    for nic in instance.nics:
1446
      if nic.network is not None and nic.ip is not None:
1447
        net_uuid = self._UnlockedLookupNetwork(nic.network)
1448
        if net_uuid:
1449
          # Return all IP addresses to the respective address pools
1450
          self._UnlockedCommitIp('release', net_uuid, nic.ip)
1451

    
1452
    del self._config_data.instances[instance_name]
1453
    self._config_data.cluster.serial_no += 1
1454
    self._WriteConfig()
1455

    
1456
  @locking.ssynchronized(_config_lock)
1457
  def RenameInstance(self, old_name, new_name):
1458
    """Rename an instance.
1459

1460
    This needs to be done in ConfigWriter and not by RemoveInstance
1461
    combined with AddInstance as only we can guarantee an atomic
1462
    rename.
1463

1464
    """
1465
    if old_name not in self._config_data.instances:
1466
      raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
1467

    
1468
    # Operate on a copy to not loose instance object in case of a failure
1469
    inst = self._config_data.instances[old_name].Copy()
1470
    inst.name = new_name
1471

    
1472
    for (idx, disk) in enumerate(inst.disks):
1473
      if disk.dev_type == constants.LD_FILE:
1474
        # rename the file paths in logical and physical id
1475
        file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1]))
1476
        disk.logical_id = (disk.logical_id[0],
1477
                           utils.PathJoin(file_storage_dir, inst.name,
1478
                                          "disk%s" % idx))
1479
        disk.physical_id = disk.logical_id
1480

    
1481
    # Actually replace instance object
1482
    del self._config_data.instances[old_name]
1483
    self._config_data.instances[inst.name] = inst
1484

    
1485
    # Force update of ssconf files
1486
    self._config_data.cluster.serial_no += 1
1487

    
1488
    self._WriteConfig()
1489

    
1490
  @locking.ssynchronized(_config_lock)
1491
  def MarkInstanceDown(self, instance_name):
1492
    """Mark the status of an instance to down in the configuration.
1493

1494
    """
1495
    self._SetInstanceStatus(instance_name, constants.ADMINST_DOWN)
1496

    
1497
  def _UnlockedGetInstanceList(self):
1498
    """Get the list of instances.
1499

1500
    This function is for internal use, when the config lock is already held.
1501

1502
    """
1503
    return self._config_data.instances.keys()
1504

    
1505
  @locking.ssynchronized(_config_lock, shared=1)
1506
  def GetInstanceList(self):
1507
    """Get the list of instances.
1508

1509
    @return: array of instances, ex. ['instance2.example.com',
1510
        'instance1.example.com']
1511

1512
    """
1513
    return self._UnlockedGetInstanceList()
1514

    
1515
  def ExpandInstanceName(self, short_name):
1516
    """Attempt to expand an incomplete instance name.
1517

1518
    """
1519
    # Locking is done in L{ConfigWriter.GetInstanceList}
1520
    return _MatchNameComponentIgnoreCase(short_name, self.GetInstanceList())
1521

    
1522
  def _UnlockedGetInstanceInfo(self, instance_name):
1523
    """Returns information about an instance.
1524

1525
    This function is for internal use, when the config lock is already held.
1526

1527
    """
1528
    if instance_name not in self._config_data.instances:
1529
      return None
1530

    
1531
    return self._config_data.instances[instance_name]
1532

    
1533
  @locking.ssynchronized(_config_lock, shared=1)
1534
  def GetInstanceInfo(self, instance_name):
1535
    """Returns information about an instance.
1536

1537
    It takes the information from the configuration file. Other information of
1538
    an instance are taken from the live systems.
1539

1540
    @param instance_name: name of the instance, e.g.
1541
        I{instance1.example.com}
1542

1543
    @rtype: L{objects.Instance}
1544
    @return: the instance object
1545

1546
    """
1547
    return self._UnlockedGetInstanceInfo(instance_name)
1548

    
1549
  @locking.ssynchronized(_config_lock, shared=1)
1550
  def GetInstanceNodeGroups(self, instance_name, primary_only=False):
1551
    """Returns set of node group UUIDs for instance's nodes.
1552

1553
    @rtype: frozenset
1554

1555
    """
1556
    instance = self._UnlockedGetInstanceInfo(instance_name)
1557
    if not instance:
1558
      raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1559

    
1560
    if primary_only:
1561
      nodes = [instance.primary_node]
1562
    else:
1563
      nodes = instance.all_nodes
1564

    
1565
    return frozenset(self._UnlockedGetNodeInfo(node_name).group
1566
                     for node_name in nodes)
1567

    
1568
  @locking.ssynchronized(_config_lock, shared=1)
1569
  def GetMultiInstanceInfo(self, instances):
1570
    """Get the configuration of multiple instances.
1571

1572
    @param instances: list of instance names
1573
    @rtype: list
1574
    @return: list of tuples (instance, instance_info), where
1575
        instance_info is what would GetInstanceInfo return for the
1576
        node, while keeping the original order
1577

1578
    """
1579
    return [(name, self._UnlockedGetInstanceInfo(name)) for name in instances]
1580

    
1581
  @locking.ssynchronized(_config_lock, shared=1)
1582
  def GetAllInstancesInfo(self):
1583
    """Get the configuration of all instances.
1584

1585
    @rtype: dict
1586
    @return: dict of (instance, instance_info), where instance_info is what
1587
              would GetInstanceInfo return for the node
1588

1589
    """
1590
    my_dict = dict([(instance, self._UnlockedGetInstanceInfo(instance))
1591
                    for instance in self._UnlockedGetInstanceList()])
1592
    return my_dict
1593

    
1594
  @locking.ssynchronized(_config_lock, shared=1)
1595
  def GetInstancesInfoByFilter(self, filter_fn):
1596
    """Get instance configuration with a filter.
1597

1598
    @type filter_fn: callable
1599
    @param filter_fn: Filter function receiving instance object as parameter,
1600
      returning boolean. Important: this function is called while the
1601
      configuration locks is held. It must not do any complex work or call
1602
      functions potentially leading to a deadlock. Ideally it doesn't call any
1603
      other functions and just compares instance attributes.
1604

1605
    """
1606
    return dict((name, inst)
1607
                for (name, inst) in self._config_data.instances.items()
1608
                if filter_fn(inst))
1609

    
1610
  @locking.ssynchronized(_config_lock)
1611
  def AddNode(self, node, ec_id):
1612
    """Add a node to the configuration.
1613

1614
    @type node: L{objects.Node}
1615
    @param node: a Node instance
1616

1617
    """
1618
    logging.info("Adding node %s to configuration", node.name)
1619

    
1620
    self._EnsureUUID(node, ec_id)
1621

    
1622
    node.serial_no = 1
1623
    node.ctime = node.mtime = time.time()
1624
    self._UnlockedAddNodeToGroup(node.name, node.group)
1625
    self._config_data.nodes[node.name] = node
1626
    self._config_data.cluster.serial_no += 1
1627
    self._WriteConfig()
1628

    
1629
  @locking.ssynchronized(_config_lock)
1630
  def RemoveNode(self, node_name):
1631
    """Remove a node from the configuration.
1632

1633
    """
1634
    logging.info("Removing node %s from configuration", node_name)
1635

    
1636
    if node_name not in self._config_data.nodes:
1637
      raise errors.ConfigurationError("Unknown node '%s'" % node_name)
1638

    
1639
    self._UnlockedRemoveNodeFromGroup(self._config_data.nodes[node_name])
1640
    del self._config_data.nodes[node_name]
1641
    self._config_data.cluster.serial_no += 1
1642
    self._WriteConfig()
1643

    
1644
  def ExpandNodeName(self, short_name):
1645
    """Attempt to expand an incomplete node name.
1646

1647
    """
1648
    # Locking is done in L{ConfigWriter.GetNodeList}
1649
    return _MatchNameComponentIgnoreCase(short_name, self.GetNodeList())
1650

    
1651
  def _UnlockedGetNodeInfo(self, node_name):
1652
    """Get the configuration of a node, as stored in the config.
1653

1654
    This function is for internal use, when the config lock is already
1655
    held.
1656

1657
    @param node_name: the node name, e.g. I{node1.example.com}
1658

1659
    @rtype: L{objects.Node}
1660
    @return: the node object
1661

1662
    """
1663
    if node_name not in self._config_data.nodes:
1664
      return None
1665

    
1666
    return self._config_data.nodes[node_name]
1667

    
1668
  @locking.ssynchronized(_config_lock, shared=1)
1669
  def GetNodeInfo(self, node_name):
1670
    """Get the configuration of a node, as stored in the config.
1671

1672
    This is just a locked wrapper over L{_UnlockedGetNodeInfo}.
1673

1674
    @param node_name: the node name, e.g. I{node1.example.com}
1675

1676
    @rtype: L{objects.Node}
1677
    @return: the node object
1678

1679
    """
1680
    return self._UnlockedGetNodeInfo(node_name)
1681

    
1682
  @locking.ssynchronized(_config_lock, shared=1)
1683
  def GetNodeInstances(self, node_name):
1684
    """Get the instances of a node, as stored in the config.
1685

1686
    @param node_name: the node name, e.g. I{node1.example.com}
1687

1688
    @rtype: (list, list)
1689
    @return: a tuple with two lists: the primary and the secondary instances
1690

1691
    """
1692
    pri = []
1693
    sec = []
1694
    for inst in self._config_data.instances.values():
1695
      if inst.primary_node == node_name:
1696
        pri.append(inst.name)
1697
      if node_name in inst.secondary_nodes:
1698
        sec.append(inst.name)
1699
    return (pri, sec)
1700

    
1701
  @locking.ssynchronized(_config_lock, shared=1)
1702
  def GetNodeGroupInstances(self, uuid, primary_only=False):
1703
    """Get the instances of a node group.
1704

1705
    @param uuid: Node group UUID
1706
    @param primary_only: Whether to only consider primary nodes
1707
    @rtype: frozenset
1708
    @return: List of instance names in node group
1709

1710
    """
1711
    if primary_only:
1712
      nodes_fn = lambda inst: [inst.primary_node]
1713
    else:
1714
      nodes_fn = lambda inst: inst.all_nodes
1715

    
1716
    return frozenset(inst.name
1717
                     for inst in self._config_data.instances.values()
1718
                     for node_name in nodes_fn(inst)
1719
                     if self._UnlockedGetNodeInfo(node_name).group == uuid)
1720

    
1721
  def _UnlockedGetNodeList(self):
1722
    """Return the list of nodes which are in the configuration.
1723

1724
    This function is for internal use, when the config lock is already
1725
    held.
1726

1727
    @rtype: list
1728

1729
    """
1730
    return self._config_data.nodes.keys()
1731

    
1732
  @locking.ssynchronized(_config_lock, shared=1)
1733
  def GetNodeList(self):
1734
    """Return the list of nodes which are in the configuration.
1735

1736
    """
1737
    return self._UnlockedGetNodeList()
1738

    
1739
  def _UnlockedGetOnlineNodeList(self):
1740
    """Return the list of nodes which are online.
1741

1742
    """
1743
    all_nodes = [self._UnlockedGetNodeInfo(node)
1744
                 for node in self._UnlockedGetNodeList()]
1745
    return [node.name for node in all_nodes if not node.offline]
1746

    
1747
  @locking.ssynchronized(_config_lock, shared=1)
1748
  def GetOnlineNodeList(self):
1749
    """Return the list of nodes which are online.
1750

1751
    """
1752
    return self._UnlockedGetOnlineNodeList()
1753

    
1754
  @locking.ssynchronized(_config_lock, shared=1)
1755
  def GetVmCapableNodeList(self):
1756
    """Return the list of nodes which are not vm capable.
1757

1758
    """
1759
    all_nodes = [self._UnlockedGetNodeInfo(node)
1760
                 for node in self._UnlockedGetNodeList()]
1761
    return [node.name for node in all_nodes if node.vm_capable]
1762

    
1763
  @locking.ssynchronized(_config_lock, shared=1)
1764
  def GetNonVmCapableNodeList(self):
1765
    """Return the list of nodes which are not vm capable.
1766

1767
    """
1768
    all_nodes = [self._UnlockedGetNodeInfo(node)
1769
                 for node in self._UnlockedGetNodeList()]
1770
    return [node.name for node in all_nodes if not node.vm_capable]
1771

    
1772
  @locking.ssynchronized(_config_lock, shared=1)
1773
  def GetMultiNodeInfo(self, nodes):
1774
    """Get the configuration of multiple nodes.
1775

1776
    @param nodes: list of node names
1777
    @rtype: list
1778
    @return: list of tuples of (node, node_info), where node_info is
1779
        what would GetNodeInfo return for the node, in the original
1780
        order
1781

1782
    """
1783
    return [(name, self._UnlockedGetNodeInfo(name)) for name in nodes]
1784

    
1785
  @locking.ssynchronized(_config_lock, shared=1)
1786
  def GetAllNodesInfo(self):
1787
    """Get the configuration of all nodes.
1788

1789
    @rtype: dict
1790
    @return: dict of (node, node_info), where node_info is what
1791
              would GetNodeInfo return for the node
1792

1793
    """
1794
    return self._UnlockedGetAllNodesInfo()
1795

    
1796
  def _UnlockedGetAllNodesInfo(self):
1797
    """Gets configuration of all nodes.
1798

1799
    @note: See L{GetAllNodesInfo}
1800

1801
    """
1802
    return dict([(node, self._UnlockedGetNodeInfo(node))
1803
                 for node in self._UnlockedGetNodeList()])
1804

    
1805
  @locking.ssynchronized(_config_lock, shared=1)
1806
  def GetNodeGroupsFromNodes(self, nodes):
1807
    """Returns groups for a list of nodes.
1808

1809
    @type nodes: list of string
1810
    @param nodes: List of node names
1811
    @rtype: frozenset
1812

1813
    """
1814
    return frozenset(self._UnlockedGetNodeInfo(name).group for name in nodes)
1815

    
1816
  def _UnlockedGetMasterCandidateStats(self, exceptions=None):
1817
    """Get the number of current and maximum desired and possible candidates.
1818

1819
    @type exceptions: list
1820
    @param exceptions: if passed, list of nodes that should be ignored
1821
    @rtype: tuple
1822
    @return: tuple of (current, desired and possible, possible)
1823

1824
    """
1825
    mc_now = mc_should = mc_max = 0
1826
    for node in self._config_data.nodes.values():
1827
      if exceptions and node.name in exceptions:
1828
        continue
1829
      if not (node.offline or node.drained) and node.master_capable:
1830
        mc_max += 1
1831
      if node.master_candidate:
1832
        mc_now += 1
1833
    mc_should = min(mc_max, self._config_data.cluster.candidate_pool_size)
1834
    return (mc_now, mc_should, mc_max)
1835

    
1836
  @locking.ssynchronized(_config_lock, shared=1)
1837
  def GetMasterCandidateStats(self, exceptions=None):
1838
    """Get the number of current and maximum possible candidates.
1839

1840
    This is just a wrapper over L{_UnlockedGetMasterCandidateStats}.
1841

1842
    @type exceptions: list
1843
    @param exceptions: if passed, list of nodes that should be ignored
1844
    @rtype: tuple
1845
    @return: tuple of (current, max)
1846

1847
    """
1848
    return self._UnlockedGetMasterCandidateStats(exceptions)
1849

    
1850
  @locking.ssynchronized(_config_lock)
1851
  def MaintainCandidatePool(self, exceptions):
1852
    """Try to grow the candidate pool to the desired size.
1853

1854
    @type exceptions: list
1855
    @param exceptions: if passed, list of nodes that should be ignored
1856
    @rtype: list
1857
    @return: list with the adjusted nodes (L{objects.Node} instances)
1858

1859
    """
1860
    mc_now, mc_max, _ = self._UnlockedGetMasterCandidateStats(exceptions)
1861
    mod_list = []
1862
    if mc_now < mc_max:
1863
      node_list = self._config_data.nodes.keys()
1864
      random.shuffle(node_list)
1865
      for name in node_list:
1866
        if mc_now >= mc_max:
1867
          break
1868
        node = self._config_data.nodes[name]
1869
        if (node.master_candidate or node.offline or node.drained or
1870
            node.name in exceptions or not node.master_capable):
1871
          continue
1872
        mod_list.append(node)
1873
        node.master_candidate = True
1874
        node.serial_no += 1
1875
        mc_now += 1
1876
      if mc_now != mc_max:
1877
        # this should not happen
1878
        logging.warning("Warning: MaintainCandidatePool didn't manage to"
1879
                        " fill the candidate pool (%d/%d)", mc_now, mc_max)
1880
      if mod_list:
1881
        self._config_data.cluster.serial_no += 1
1882
        self._WriteConfig()
1883

    
1884
    return mod_list
1885

    
1886
  def _UnlockedAddNodeToGroup(self, node_name, nodegroup_uuid):
1887
    """Add a given node to the specified group.
1888

1889
    """
1890
    if nodegroup_uuid not in self._config_data.nodegroups:
1891
      # This can happen if a node group gets deleted between its lookup and
1892
      # when we're adding the first node to it, since we don't keep a lock in
1893
      # the meantime. It's ok though, as we'll fail cleanly if the node group
1894
      # is not found anymore.
1895
      raise errors.OpExecError("Unknown node group: %s" % nodegroup_uuid)
1896
    if node_name not in self._config_data.nodegroups[nodegroup_uuid].members:
1897
      self._config_data.nodegroups[nodegroup_uuid].members.append(node_name)
1898

    
1899
  def _UnlockedRemoveNodeFromGroup(self, node):
1900
    """Remove a given node from its group.
1901

1902
    """
1903
    nodegroup = node.group
1904
    if nodegroup not in self._config_data.nodegroups:
1905
      logging.warning("Warning: node '%s' has unknown node group '%s'"
1906
                      " (while being removed from it)", node.name, nodegroup)
1907
    nodegroup_obj = self._config_data.nodegroups[nodegroup]
1908
    if node.name not in nodegroup_obj.members:
1909
      logging.warning("Warning: node '%s' not a member of its node group '%s'"
1910
                      " (while being removed from it)", node.name, nodegroup)
1911
    else:
1912
      nodegroup_obj.members.remove(node.name)
1913

    
1914
  @locking.ssynchronized(_config_lock)
1915
  def AssignGroupNodes(self, mods):
1916
    """Changes the group of a number of nodes.
1917

1918
    @type mods: list of tuples; (node name, new group UUID)
1919
    @param mods: Node membership modifications
1920

1921
    """
1922
    groups = self._config_data.nodegroups
1923
    nodes = self._config_data.nodes
1924

    
1925
    resmod = []
1926

    
1927
    # Try to resolve names/UUIDs first
1928
    for (node_name, new_group_uuid) in mods:
1929
      try:
1930
        node = nodes[node_name]
1931
      except KeyError:
1932
        raise errors.ConfigurationError("Unable to find node '%s'" % node_name)
1933

    
1934
      if node.group == new_group_uuid:
1935
        # Node is being assigned to its current group
1936
        logging.debug("Node '%s' was assigned to its current group (%s)",
1937
                      node_name, node.group)
1938
        continue
1939

    
1940
      # Try to find current group of node
1941
      try:
1942
        old_group = groups[node.group]
1943
      except KeyError:
1944
        raise errors.ConfigurationError("Unable to find old group '%s'" %
1945
                                        node.group)
1946

    
1947
      # Try to find new group for node
1948
      try:
1949
        new_group = groups[new_group_uuid]
1950
      except KeyError:
1951
        raise errors.ConfigurationError("Unable to find new group '%s'" %
1952
                                        new_group_uuid)
1953

    
1954
      assert node.name in old_group.members, \
1955
        ("Inconsistent configuration: node '%s' not listed in members for its"
1956
         " old group '%s'" % (node.name, old_group.uuid))
1957
      assert node.name not in new_group.members, \
1958
        ("Inconsistent configuration: node '%s' already listed in members for"
1959
         " its new group '%s'" % (node.name, new_group.uuid))
1960

    
1961
      resmod.append((node, old_group, new_group))
1962

    
1963
    # Apply changes
1964
    for (node, old_group, new_group) in resmod:
1965
      assert node.uuid != new_group.uuid and old_group.uuid != new_group.uuid, \
1966
        "Assigning to current group is not possible"
1967

    
1968
      node.group = new_group.uuid
1969

    
1970
      # Update members of involved groups
1971
      if node.name in old_group.members:
1972
        old_group.members.remove(node.name)
1973
      if node.name not in new_group.members:
1974
        new_group.members.append(node.name)
1975

    
1976
    # Update timestamps and serials (only once per node/group object)
1977
    now = time.time()
1978
    for obj in frozenset(itertools.chain(*resmod)): # pylint: disable=W0142
1979
      obj.serial_no += 1
1980
      obj.mtime = now
1981

    
1982
    # Force ssconf update
1983
    self._config_data.cluster.serial_no += 1
1984

    
1985
    self._WriteConfig()
1986

    
1987
  def _BumpSerialNo(self):
1988
    """Bump up the serial number of the config.
1989

1990
    """
1991
    self._config_data.serial_no += 1
1992
    self._config_data.mtime = time.time()
1993

    
1994
  def _AllUUIDObjects(self):
1995
    """Returns all objects with uuid attributes.
1996

1997
    """
1998
    return (self._config_data.instances.values() +
1999
            self._config_data.nodes.values() +
2000
            self._config_data.nodegroups.values() +
2001
            [self._config_data.cluster])
2002

    
2003
  def _OpenConfig(self, accept_foreign):
2004
    """Read the config data from disk.
2005

2006
    """
2007
    raw_data = utils.ReadFile(self._cfg_file)
2008

    
2009
    try:
2010
      data = objects.ConfigData.FromDict(serializer.Load(raw_data))
2011
    except Exception, err:
2012
      raise errors.ConfigurationError(err)
2013

    
2014
    # Make sure the configuration has the right version
2015
    _ValidateConfig(data)
2016

    
2017
    if (not hasattr(data, "cluster") or
2018
        not hasattr(data.cluster, "rsahostkeypub")):
2019
      raise errors.ConfigurationError("Incomplete configuration"
2020
                                      " (missing cluster.rsahostkeypub)")
2021

    
2022
    if data.cluster.master_node != self._my_hostname and not accept_foreign:
2023
      msg = ("The configuration denotes node %s as master, while my"
2024
             " hostname is %s; opening a foreign configuration is only"
2025
             " possible in accept_foreign mode" %
2026
             (data.cluster.master_node, self._my_hostname))
2027
      raise errors.ConfigurationError(msg)
2028

    
2029
    # Upgrade configuration if needed
2030
    data.UpgradeConfig()
2031

    
2032
    self._config_data = data
2033
    # reset the last serial as -1 so that the next write will cause
2034
    # ssconf update
2035
    self._last_cluster_serial = -1
2036

    
2037
    # And finally run our (custom) config upgrade sequence
2038
    self._UpgradeConfig()
2039

    
2040
    self._cfg_id = utils.GetFileID(path=self._cfg_file)
2041

    
2042
  def _UpgradeConfig(self):
2043
    """Run upgrade steps that cannot be done purely in the objects.
2044

2045
    This is because some data elements need uniqueness across the
2046
    whole configuration, etc.
2047

2048
    @warning: this function will call L{_WriteConfig()}, but also
2049
        L{DropECReservations} so it needs to be called only from a
2050
        "safe" place (the constructor). If one wanted to call it with
2051
        the lock held, a DropECReservationUnlocked would need to be
2052
        created first, to avoid causing deadlock.
2053

2054
    """
2055
    modified = False
2056
    for item in self._AllUUIDObjects():
2057
      if item.uuid is None:
2058
        item.uuid = self._GenerateUniqueID(_UPGRADE_CONFIG_JID)
2059
        modified = True
2060
    if not self._config_data.nodegroups:
2061
      default_nodegroup_name = constants.INITIAL_NODE_GROUP_NAME
2062
      default_nodegroup = objects.NodeGroup(name=default_nodegroup_name,
2063
                                            members=[])
2064
      self._UnlockedAddNodeGroup(default_nodegroup, _UPGRADE_CONFIG_JID, True)
2065
      modified = True
2066
    for node in self._config_data.nodes.values():
2067
      if not node.group:
2068
        node.group = self.LookupNodeGroup(None)
2069
        modified = True
2070
      # This is technically *not* an upgrade, but needs to be done both when
2071
      # nodegroups are being added, and upon normally loading the config,
2072
      # because the members list of a node group is discarded upon
2073
      # serializing/deserializing the object.
2074
      self._UnlockedAddNodeToGroup(node.name, node.group)
2075
    if modified:
2076
      self._WriteConfig()
2077
      # This is ok even if it acquires the internal lock, as _UpgradeConfig is
2078
      # only called at config init time, without the lock held
2079
      self.DropECReservations(_UPGRADE_CONFIG_JID)
2080

    
2081
  def _DistributeConfig(self, feedback_fn):
2082
    """Distribute the configuration to the other nodes.
2083

2084
    Currently, this only copies the configuration file. In the future,
2085
    it could be used to encapsulate the 2/3-phase update mechanism.
2086

2087
    """
2088
    if self._offline:
2089
      return True
2090

    
2091
    bad = False
2092

    
2093
    node_list = []
2094
    addr_list = []
2095
    myhostname = self._my_hostname
2096
    # we can skip checking whether _UnlockedGetNodeInfo returns None
2097
    # since the node list comes from _UnlocketGetNodeList, and we are
2098
    # called with the lock held, so no modifications should take place
2099
    # in between
2100
    for node_name in self._UnlockedGetNodeList():
2101
      if node_name == myhostname:
2102
        continue
2103
      node_info = self._UnlockedGetNodeInfo(node_name)
2104
      if not node_info.master_candidate:
2105
        continue
2106
      node_list.append(node_info.name)
2107
      addr_list.append(node_info.primary_ip)
2108

    
2109
    # TODO: Use dedicated resolver talking to config writer for name resolution
2110
    result = \
2111
      self._GetRpc(addr_list).call_upload_file(node_list, self._cfg_file)
2112
    for to_node, to_result in result.items():
2113
      msg = to_result.fail_msg
2114
      if msg:
2115
        msg = ("Copy of file %s to node %s failed: %s" %
2116
               (self._cfg_file, to_node, msg))
2117
        logging.error(msg)
2118

    
2119
        if feedback_fn:
2120
          feedback_fn(msg)
2121

    
2122
        bad = True
2123

    
2124
    return not bad
2125

    
2126
  def _WriteConfig(self, destination=None, feedback_fn=None):
2127
    """Write the configuration data to persistent storage.
2128

2129
    """
2130
    assert feedback_fn is None or callable(feedback_fn)
2131

    
2132
    # Warn on config errors, but don't abort the save - the
2133
    # configuration has already been modified, and we can't revert;
2134
    # the best we can do is to warn the user and save as is, leaving
2135
    # recovery to the user
2136
    config_errors = self._UnlockedVerifyConfig()
2137
    if config_errors:
2138
      errmsg = ("Configuration data is not consistent: %s" %
2139
                (utils.CommaJoin(config_errors)))
2140
      logging.critical(errmsg)
2141
      if feedback_fn:
2142
        feedback_fn(errmsg)
2143

    
2144
    if destination is None:
2145
      destination = self._cfg_file
2146
    self._BumpSerialNo()
2147
    txt = serializer.Dump(self._config_data.ToDict())
2148

    
2149
    getents = self._getents()
2150
    try:
2151
      fd = utils.SafeWriteFile(destination, self._cfg_id, data=txt,
2152
                               close=False, gid=getents.confd_gid, mode=0640)
2153
    except errors.LockError:
2154
      raise errors.ConfigurationError("The configuration file has been"
2155
                                      " modified since the last write, cannot"
2156
                                      " update")
2157
    try:
2158
      self._cfg_id = utils.GetFileID(fd=fd)
2159
    finally:
2160
      os.close(fd)
2161

    
2162
    self.write_count += 1
2163

    
2164
    # and redistribute the config file to master candidates
2165
    self._DistributeConfig(feedback_fn)
2166

    
2167
    # Write ssconf files on all nodes (including locally)
2168
    if self._last_cluster_serial < self._config_data.cluster.serial_no:
2169
      if not self._offline:
2170
        result = self._GetRpc(None).call_write_ssconf_files(
2171
          self._UnlockedGetOnlineNodeList(),
2172
          self._UnlockedGetSsconfValues())
2173

    
2174
        for nname, nresu in result.items():
2175
          msg = nresu.fail_msg
2176
          if msg:
2177
            errmsg = ("Error while uploading ssconf files to"
2178
                      " node %s: %s" % (nname, msg))
2179
            logging.warning(errmsg)
2180

    
2181
            if feedback_fn:
2182
              feedback_fn(errmsg)
2183

    
2184
      self._last_cluster_serial = self._config_data.cluster.serial_no
2185

    
2186
  def _UnlockedGetSsconfValues(self):
2187
    """Return the values needed by ssconf.
2188

2189
    @rtype: dict
2190
    @return: a dictionary with keys the ssconf names and values their
2191
        associated value
2192

2193
    """
2194
    fn = "\n".join
2195
    instance_names = utils.NiceSort(self._UnlockedGetInstanceList())
2196
    node_names = utils.NiceSort(self._UnlockedGetNodeList())
2197
    node_info = [self._UnlockedGetNodeInfo(name) for name in node_names]
2198
    node_pri_ips = ["%s %s" % (ninfo.name, ninfo.primary_ip)
2199
                    for ninfo in node_info]
2200
    node_snd_ips = ["%s %s" % (ninfo.name, ninfo.secondary_ip)
2201
                    for ninfo in node_info]
2202

    
2203
    instance_data = fn(instance_names)
2204
    off_data = fn(node.name for node in node_info if node.offline)
2205
    on_data = fn(node.name for node in node_info if not node.offline)
2206
    mc_data = fn(node.name for node in node_info if node.master_candidate)
2207
    mc_ips_data = fn(node.primary_ip for node in node_info
2208
                     if node.master_candidate)
2209
    node_data = fn(node_names)
2210
    node_pri_ips_data = fn(node_pri_ips)
2211
    node_snd_ips_data = fn(node_snd_ips)
2212

    
2213
    cluster = self._config_data.cluster
2214
    cluster_tags = fn(cluster.GetTags())
2215

    
2216
    hypervisor_list = fn(cluster.enabled_hypervisors)
2217

    
2218
    uid_pool = uidpool.FormatUidPool(cluster.uid_pool, separator="\n")
2219

    
2220
    nodegroups = ["%s %s" % (nodegroup.uuid, nodegroup.name) for nodegroup in
2221
                  self._config_data.nodegroups.values()]
2222
    nodegroups_data = fn(utils.NiceSort(nodegroups))
2223
    networks = ["%s %s" % (net.uuid, net.name) for net in
2224
                self._config_data.networks.values()]
2225
    networks_data = fn(utils.NiceSort(networks))
2226

    
2227
    ssconf_values = {
2228
      constants.SS_CLUSTER_NAME: cluster.cluster_name,
2229
      constants.SS_CLUSTER_TAGS: cluster_tags,
2230
      constants.SS_FILE_STORAGE_DIR: cluster.file_storage_dir,
2231
      constants.SS_SHARED_FILE_STORAGE_DIR: cluster.shared_file_storage_dir,
2232
      constants.SS_MASTER_CANDIDATES: mc_data,
2233
      constants.SS_MASTER_CANDIDATES_IPS: mc_ips_data,
2234
      constants.SS_MASTER_IP: cluster.master_ip,
2235
      constants.SS_MASTER_NETDEV: cluster.master_netdev,
2236
      constants.SS_MASTER_NETMASK: str(cluster.master_netmask),
2237
      constants.SS_MASTER_NODE: cluster.master_node,
2238
      constants.SS_NODE_LIST: node_data,
2239
      constants.SS_NODE_PRIMARY_IPS: node_pri_ips_data,
2240
      constants.SS_NODE_SECONDARY_IPS: node_snd_ips_data,
2241
      constants.SS_OFFLINE_NODES: off_data,
2242
      constants.SS_ONLINE_NODES: on_data,
2243
      constants.SS_PRIMARY_IP_FAMILY: str(cluster.primary_ip_family),
2244
      constants.SS_INSTANCE_LIST: instance_data,
2245
      constants.SS_RELEASE_VERSION: constants.RELEASE_VERSION,
2246
      constants.SS_HYPERVISOR_LIST: hypervisor_list,
2247
      constants.SS_MAINTAIN_NODE_HEALTH: str(cluster.maintain_node_health),
2248
      constants.SS_UID_POOL: uid_pool,
2249
      constants.SS_NODEGROUPS: nodegroups_data,
2250
      constants.SS_NETWORKS: networks_data,
2251
      }
2252
    bad_values = [(k, v) for k, v in ssconf_values.items()
2253
                  if not isinstance(v, (str, basestring))]
2254
    if bad_values:
2255
      err = utils.CommaJoin("%s=%s" % (k, v) for k, v in bad_values)
2256
      raise errors.ConfigurationError("Some ssconf key(s) have non-string"
2257
                                      " values: %s" % err)
2258
    return ssconf_values
2259

    
2260
  @locking.ssynchronized(_config_lock, shared=1)
2261
  def GetSsconfValues(self):
2262
    """Wrapper using lock around _UnlockedGetSsconf().
2263

2264
    """
2265
    return self._UnlockedGetSsconfValues()
2266

    
2267
  @locking.ssynchronized(_config_lock, shared=1)
2268
  def GetVGName(self):
2269
    """Return the volume group name.
2270

2271
    """
2272
    return self._config_data.cluster.volume_group_name
2273

    
2274
  @locking.ssynchronized(_config_lock)
2275
  def SetVGName(self, vg_name):
2276
    """Set the volume group name.
2277

2278
    """
2279
    self._config_data.cluster.volume_group_name = vg_name
2280
    self._config_data.cluster.serial_no += 1
2281
    self._WriteConfig()
2282

    
2283
  @locking.ssynchronized(_config_lock, shared=1)
2284
  def GetDRBDHelper(self):
2285
    """Return DRBD usermode helper.
2286

2287
    """
2288
    return self._config_data.cluster.drbd_usermode_helper
2289

    
2290
  @locking.ssynchronized(_config_lock)
2291
  def SetDRBDHelper(self, drbd_helper):
2292
    """Set DRBD usermode helper.
2293

2294
    """
2295
    self._config_data.cluster.drbd_usermode_helper = drbd_helper
2296
    self._config_data.cluster.serial_no += 1
2297
    self._WriteConfig()
2298

    
2299
  @locking.ssynchronized(_config_lock, shared=1)
2300
  def GetMACPrefix(self):
2301
    """Return the mac prefix.
2302

2303
    """
2304
    return self._config_data.cluster.mac_prefix
2305

    
2306
  @locking.ssynchronized(_config_lock, shared=1)
2307
  def GetClusterInfo(self):
2308
    """Returns information about the cluster
2309

2310
    @rtype: L{objects.Cluster}
2311
    @return: the cluster object
2312

2313
    """
2314
    return self._config_data.cluster
2315

    
2316
  @locking.ssynchronized(_config_lock, shared=1)
2317
  def HasAnyDiskOfType(self, dev_type):
2318
    """Check if in there is at disk of the given type in the configuration.
2319

2320
    """
2321
    return self._config_data.HasAnyDiskOfType(dev_type)
2322

    
2323
  @locking.ssynchronized(_config_lock)
2324
  def Update(self, target, feedback_fn, ec_id=None):
2325
    """Notify function to be called after updates.
2326

2327
    This function must be called when an object (as returned by
2328
    GetInstanceInfo, GetNodeInfo, GetCluster) has been updated and the
2329
    caller wants the modifications saved to the backing store. Note
2330
    that all modified objects will be saved, but the target argument
2331
    is the one the caller wants to ensure that it's saved.
2332

2333
    @param target: an instance of either L{objects.Cluster},
2334
        L{objects.Node} or L{objects.Instance} which is existing in
2335
        the cluster
2336
    @param feedback_fn: Callable feedback function
2337

2338
    """
2339
    if self._config_data is None:
2340
      raise errors.ProgrammerError("Configuration file not read,"
2341
                                   " cannot save.")
2342
    update_serial = False
2343
    if isinstance(target, objects.Cluster):
2344
      test = target == self._config_data.cluster
2345
    elif isinstance(target, objects.Node):
2346
      test = target in self._config_data.nodes.values()
2347
      update_serial = True
2348
    elif isinstance(target, objects.Instance):
2349
      test = target in self._config_data.instances.values()
2350
    elif isinstance(target, objects.NodeGroup):
2351
      test = target in self._config_data.nodegroups.values()
2352
    elif isinstance(target, objects.Network):
2353
      test = target in self._config_data.networks.values()
2354
    else:
2355
      raise errors.ProgrammerError("Invalid object type (%s) passed to"
2356
                                   " ConfigWriter.Update" % type(target))
2357
    if not test:
2358
      raise errors.ConfigurationError("Configuration updated since object"
2359
                                      " has been read or unknown object")
2360
    target.serial_no += 1
2361
    target.mtime = now = time.time()
2362

    
2363
    if update_serial:
2364
      # for node updates, we need to increase the cluster serial too
2365
      self._config_data.cluster.serial_no += 1
2366
      self._config_data.cluster.mtime = now
2367

    
2368
    if isinstance(target, objects.Instance):
2369
      self._UnlockedReleaseDRBDMinors(target.name)
2370

    
2371
    if ec_id is not None:
2372
      # Commit all ips reserved by OpInstanceSetParams and OpGroupSetParams
2373
      self._UnlockedCommitTemporaryIps(ec_id)
2374

    
2375
    self._WriteConfig(feedback_fn=feedback_fn)
2376

    
2377
  @locking.ssynchronized(_config_lock)
2378
  def DropECReservations(self, ec_id):
2379
    """Drop per-execution-context reservations
2380

2381
    """
2382
    for rm in self._all_rms:
2383
      rm.DropECReservations(ec_id)
2384

    
2385
  @locking.ssynchronized(_config_lock, shared=1)
2386
  def GetAllNetworksInfo(self):
2387
    """Get the configuration of all networks
2388

2389
    """
2390
    return dict(self._config_data.networks)
2391

    
2392
  def _UnlockedGetNetworkList(self):
2393
    """Get the list of networks.
2394

2395
    This function is for internal use, when the config lock is already held.
2396

2397
    """
2398
    return self._config_data.networks.keys()
2399

    
2400
  @locking.ssynchronized(_config_lock, shared=1)
2401
  def GetNetworkList(self):
2402
    """Get the list of networks.
2403

2404
    @return: array of networks, ex. ["main", "vlan100", "200]
2405

2406
    """
2407
    return self._UnlockedGetNetworkList()
2408

    
2409
  @locking.ssynchronized(_config_lock, shared=1)
2410
  def GetNetworkNames(self):
2411
    """Get a list of network names
2412

2413
    """
2414
    names = [net.name
2415
             for net in self._config_data.networks.values()]
2416
    return names
2417

    
2418
  def _UnlockedGetNetwork(self, uuid):
2419
    """Returns information about a network.
2420

2421
    This function is for internal use, when the config lock is already held.
2422

2423
    """
2424
    if uuid not in self._config_data.networks:
2425
      return None
2426

    
2427
    return self._config_data.networks[uuid]
2428

    
2429
  @locking.ssynchronized(_config_lock, shared=1)
2430
  def GetNetwork(self, uuid):
2431
    """Returns information about a network.
2432

2433
    It takes the information from the configuration file.
2434

2435
    @param uuid: UUID of the network
2436

2437
    @rtype: L{objects.Network}
2438
    @return: the network object
2439

2440
    """
2441
    return self._UnlockedGetNetwork(uuid)
2442

    
2443
  @locking.ssynchronized(_config_lock)
2444
  def AddNetwork(self, net, ec_id, check_uuid=True):
2445
    """Add a network to the configuration.
2446

2447
    @type net: L{objects.Network}
2448
    @param net: the Network object to add
2449
    @type ec_id: string
2450
    @param ec_id: unique id for the job to use when creating a missing UUID
2451

2452
    """
2453
    self._UnlockedAddNetwork(net, ec_id, check_uuid)
2454
    self._WriteConfig()
2455

    
2456
  def _UnlockedAddNetwork(self, net, ec_id, check_uuid):
2457
    """Add a network to the configuration.
2458

2459
    """
2460
    logging.info("Adding network %s to configuration", net.name)
2461

    
2462
    if check_uuid:
2463
      self._EnsureUUID(net, ec_id)
2464

    
2465
    existing_uuid = self._UnlockedLookupNetwork(net.name)
2466
    if existing_uuid:
2467
      raise errors.OpPrereqError("Desired network name '%s' already"
2468
                                 " exists as a network (UUID: %s)" %
2469
                                 (net.name, existing_uuid),
2470
                                 errors.ECODE_EXISTS)
2471
    net.serial_no = 1
2472
    self._config_data.networks[net.uuid] = net
2473
    self._config_data.cluster.serial_no += 1
2474

    
2475
  def _UnlockedLookupNetwork(self, target):
2476
    """Lookup a network's UUID.
2477

2478
    @type target: string
2479
    @param target: network name or UUID
2480
    @rtype: string
2481
    @return: network UUID
2482
    @raises errors.OpPrereqError: when the target network cannot be found
2483

2484
    """
2485
    if target in self._config_data.networks:
2486
      return target
2487
    for net in self._config_data.networks.values():
2488
      if net.name == target:
2489
        return net.uuid
2490
    return None
2491

    
2492
  @locking.ssynchronized(_config_lock, shared=1)
2493
  def LookupNetwork(self, target):
2494
    """Lookup a network's UUID.
2495

2496
    This function is just a wrapper over L{_UnlockedLookupNetwork}.
2497

2498
    @type target: string
2499
    @param target: network name or UUID
2500
    @rtype: string
2501
    @return: network UUID
2502

2503
    """
2504
    return self._UnlockedLookupNetwork(target)
2505

    
2506
  @locking.ssynchronized(_config_lock)
2507
  def RemoveNetwork(self, network_uuid):
2508
    """Remove a network from the configuration.
2509

2510
    @type network_uuid: string
2511
    @param network_uuid: the UUID of the network to remove
2512

2513
    """
2514
    logging.info("Removing network %s from configuration", network_uuid)
2515

    
2516
    if network_uuid not in self._config_data.networks:
2517
      raise errors.ConfigurationError("Unknown network '%s'" % network_uuid)
2518

    
2519
    del self._config_data.networks[network_uuid]
2520
    self._config_data.cluster.serial_no += 1
2521
    self._WriteConfig()
2522

    
2523
  def _UnlockedGetGroupNetParams(self, net, node):
2524
    """Get the netparams (mode, link) of a network.
2525

2526
    Get a network's netparams for a given node.
2527

2528
    @type net: string
2529
    @param net: network name
2530
    @type node: string
2531
    @param node: node name
2532
    @rtype: dict or None
2533
    @return: netparams
2534

2535
    """
2536
    net_uuid = self._UnlockedLookupNetwork(net)
2537
    if net_uuid is None:
2538
      return None
2539

    
2540
    node_info = self._UnlockedGetNodeInfo(node)
2541
    nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2542
    netparams = nodegroup_info.networks.get(net_uuid, None)
2543

    
2544
    return netparams
2545

    
2546
  @locking.ssynchronized(_config_lock, shared=1)
2547
  def GetGroupNetParams(self, net, node):
2548
    """Locking wrapper of _UnlockedGetGroupNetParams()
2549

2550
    """
2551
    return self._UnlockedGetGroupNetParams(net, node)
2552

    
2553
  @locking.ssynchronized(_config_lock, shared=1)
2554
  def CheckIPInNodeGroup(self, ip, node):
2555
    """Check for conflictig IP.
2556

2557
    @type ip: string
2558
    @param ip: ip address
2559
    @type node: string
2560
    @param node: node name
2561
    @rtype: (string, dict) or (None, None)
2562
    @return: (network name, netparams)
2563

2564
    """
2565
    if ip is None:
2566
      return (None, None)
2567
    node_info = self._UnlockedGetNodeInfo(node)
2568
    nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2569
    for net_uuid in nodegroup_info.networks.keys():
2570
      net_info = self._UnlockedGetNetwork(net_uuid)
2571
      pool = network.AddressPool(net_info)
2572
      if pool.Contains(ip):
2573
        return (net_info.name, nodegroup_info.networks[net_uuid])
2574

    
2575
    return (None, None)