Statistics
| Branch: | Tag: | Revision:

root / lib / config.py @ 3697def0

History | View | Annotate | Download (85.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 copy
38
import os
39
import random
40
import logging
41
import time
42
import itertools
43

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

    
57

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

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

    
63

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

67
  This only verifies the version of the configuration.
68

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

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

    
76

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

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

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

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

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

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

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

    
112
  def GetECReserved(self, ec_id):
113
    """ Used when you want to retrieve all reservations for a specific
114
        execution context. E.g when commiting reserved IPs for a specific
115
        network.
116

117
    """
118
    ec_reserved = set()
119
    if ec_id in self._ec_reserved:
120
      ec_reserved.update(self._ec_reserved[ec_id])
121
    return ec_reserved
122

    
123
  def Generate(self, existing, generate_one_fn, ec_id):
124
    """Generate a new resource of this type
125

126
    """
127
    assert callable(generate_one_fn)
128

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

    
142

    
143
def _MatchNameComponentIgnoreCase(short_name, names):
144
  """Wrapper around L{utils.text.MatchNameComponent}.
145

146
  """
147
  return utils.MatchNameComponent(short_name, names, case_sensitive=False)
148

    
149

    
150
def _CheckInstanceDiskIvNames(disks):
151
  """Checks if instance's disks' C{iv_name} attributes are in order.
152

153
  @type disks: list of L{objects.Disk}
154
  @param disks: List of disks
155
  @rtype: list of tuples; (int, string, string)
156
  @return: List of wrongly named disks, each tuple contains disk index,
157
    expected and actual name
158

159
  """
160
  result = []
161

    
162
  for (idx, disk) in enumerate(disks):
163
    exp_iv_name = "disk/%s" % idx
164
    if disk.iv_name != exp_iv_name:
165
      result.append((idx, exp_iv_name, disk.iv_name))
166

    
167
  return result
168

    
169

    
170
class ConfigWriter:
171
  """The interface to the cluster configuration.
172

173
  @ivar _temporary_lvs: reservation manager for temporary LVs
174
  @ivar _all_rms: a list of all temporary reservation managers
175

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

    
207
  def _GetRpc(self, address_list):
208
    """Returns RPC runner for configuration.
209

210
    """
211
    return rpc.ConfigRunner(self._context, address_list)
212

    
213
  def SetContext(self, context):
214
    """Sets Ganeti context.
215

216
    """
217
    self._context = context
218

    
219
  # this method needs to be static, so that we can call it on the class
220
  @staticmethod
221
  def IsCluster():
222
    """Check if the cluster is configured.
223

224
    """
225
    return os.path.exists(pathutils.CLUSTER_CONF_FILE)
226

    
227
  @locking.ssynchronized(_config_lock, shared=1)
228
  def GetNdParams(self, node):
229
    """Get the node params populated with cluster defaults.
230

231
    @type node: L{objects.Node}
232
    @param node: The node we want to know the params for
233
    @return: A dict with the filled in node params
234

235
    """
236
    nodegroup = self._UnlockedGetNodeGroup(node.group)
237
    return self._config_data.cluster.FillND(node, nodegroup)
238

    
239
  @locking.ssynchronized(_config_lock, shared=1)
240
  def GetInstanceDiskParams(self, instance):
241
    """Get the disk params populated with inherit chain.
242

243
    @type instance: L{objects.Instance}
244
    @param instance: The instance we want to know the params for
245
    @return: A dict with the filled in disk params
246

247
    """
248
    node = self._UnlockedGetNodeInfo(instance.primary_node)
249
    nodegroup = self._UnlockedGetNodeGroup(node.group)
250
    return self._UnlockedGetGroupDiskParams(nodegroup)
251

    
252
  @locking.ssynchronized(_config_lock, shared=1)
253
  def GetGroupDiskParams(self, group):
254
    """Get the disk params populated with inherit chain.
255

256
    @type group: L{objects.NodeGroup}
257
    @param group: The group we want to know the params for
258
    @return: A dict with the filled in disk params
259

260
    """
261
    return self._UnlockedGetGroupDiskParams(group)
262

    
263
  def _UnlockedGetGroupDiskParams(self, group):
264
    """Get the disk params populated with inherit chain down to node-group.
265

266
    @type group: L{objects.NodeGroup}
267
    @param group: The group we want to know the params for
268
    @return: A dict with the filled in disk params
269

270
    """
271
    return self._config_data.cluster.SimpleFillDP(group.diskparams)
272

    
273
  def _UnlockedGetNetworkMACPrefix(self, net):
274
    """Return the network mac prefix if it exists or the cluster level default.
275

276
    """
277
    prefix = None
278
    if net:
279
      net_uuid = self._UnlockedLookupNetwork(net)
280
      if net_uuid:
281
        nobj = self._UnlockedGetNetwork(net_uuid)
282
        if nobj.mac_prefix:
283
          prefix = nobj.mac_prefix
284

    
285
    return prefix
286

    
287
  def _GenerateOneMAC(self, prefix=None):
288
    """Return a function that randomly generates a MAC suffic
289
       and appends it to the given prefix. If prefix is not given get
290
       the cluster level default.
291

292
    """
293
    if not prefix:
294
      prefix = self._config_data.cluster.mac_prefix
295

    
296
    def GenMac():
297
      byte1 = random.randrange(0, 256)
298
      byte2 = random.randrange(0, 256)
299
      byte3 = random.randrange(0, 256)
300
      mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
301
      return mac
302

    
303
    return GenMac
304

    
305
  @locking.ssynchronized(_config_lock, shared=1)
306
  def GenerateMAC(self, net, ec_id):
307
    """Generate a MAC for an instance.
308

309
    This should check the current instances for duplicates.
310

311
    """
312
    existing = self._AllMACs()
313
    prefix = self._UnlockedGetNetworkMACPrefix(net)
314
    gen_mac = self._GenerateOneMAC(prefix)
315
    return self._temporary_ids.Generate(existing, gen_mac, ec_id)
316

    
317
  @locking.ssynchronized(_config_lock, shared=1)
318
  def ReserveMAC(self, mac, ec_id):
319
    """Reserve a MAC for an instance.
320

321
    This only checks instances managed by this cluster, it does not
322
    check for potential collisions elsewhere.
323

324
    """
325
    all_macs = self._AllMACs()
326
    if mac in all_macs:
327
      raise errors.ReservationError("mac already in use")
328
    else:
329
      self._temporary_macs.Reserve(ec_id, mac)
330

    
331
  def _UnlockedCommitTemporaryIps(self, ec_id):
332
    """Commit all reserved IP address to their respective pools
333

334
    """
335
    for action, address, net_uuid in self._temporary_ips.GetECReserved(ec_id):
336
      self._UnlockedCommitIp(action, net_uuid, address)
337

    
338
  def _UnlockedCommitIp(self, action, net_uuid, address):
339
    """Commit a reserved IP address to an IP pool.
340

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

343
    """
344
    nobj = self._UnlockedGetNetwork(net_uuid)
345
    pool = network.AddressPool(nobj)
346
    if action == constants.RESERVE_ACTION:
347
      pool.Reserve(address)
348
    elif action == constants.RELEASE_ACTION:
349
      pool.Release(address)
350

    
351
  def _UnlockedReleaseIp(self, net_uuid, address, ec_id):
352
    """Give a specific IP address back to an IP pool.
353

354
    The IP address is returned to the IP pool designated by pool_id and marked
355
    as reserved.
356

357
    """
358
    self._temporary_ips.Reserve(ec_id,
359
                                (constants.RELEASE_ACTION, address, net_uuid))
360

    
361
  @locking.ssynchronized(_config_lock, shared=1)
362
  def ReleaseIp(self, net, address, ec_id):
363
    """Give a specified IP address back to an IP pool.
364

365
    This is just a wrapper around _UnlockedReleaseIp.
366

367
    """
368
    net_uuid = self._UnlockedLookupNetwork(net)
369
    if net_uuid:
370
      self._UnlockedReleaseIp(net_uuid, address, ec_id)
371

    
372
  @locking.ssynchronized(_config_lock, shared=1)
373
  def GenerateIp(self, net, ec_id):
374
    """Find a free IPv4 address for an instance.
375

376
    """
377
    net_uuid = self._UnlockedLookupNetwork(net)
378
    nobj = self._UnlockedGetNetwork(net_uuid)
379
    pool = network.AddressPool(nobj)
380

    
381
    def gen_one():
382
      try:
383
        ip = pool.GenerateFree()
384
      except errors.AddressPoolError:
385
        raise errors.ReservationError("Cannot generate IP. Network is full")
386
      return (constants.RESERVE_ACTION, ip, net_uuid)
387

    
388
    _, address, _ = self._temporary_ips.Generate([], gen_one, ec_id)
389
    return address
390

    
391
  def _UnlockedReserveIp(self, net_uuid, address, ec_id):
392
    """Reserve a given IPv4 address for use by an instance.
393

394
    """
395
    nobj = self._UnlockedGetNetwork(net_uuid)
396
    pool = network.AddressPool(nobj)
397
    try:
398
      isreserved = pool.IsReserved(address)
399
    except errors.AddressPoolError:
400
      raise errors.ReservationError("IP address not in network")
401
    if isreserved:
402
      raise errors.ReservationError("IP address already in use")
403

    
404
    return self._temporary_ips.Reserve(ec_id,
405
                                       (constants.RESERVE_ACTION,
406
                                        address, net_uuid))
407

    
408
  @locking.ssynchronized(_config_lock, shared=1)
409
  def ReserveIp(self, net, address, ec_id):
410
    """Reserve a given IPv4 address for use by an instance.
411

412
    """
413
    net_uuid = self._UnlockedLookupNetwork(net)
414
    if net_uuid:
415
      return self._UnlockedReserveIp(net_uuid, address, ec_id)
416

    
417
  @locking.ssynchronized(_config_lock, shared=1)
418
  def ReserveLV(self, lv_name, ec_id):
419
    """Reserve an VG/LV pair for an instance.
420

421
    @type lv_name: string
422
    @param lv_name: the logical volume name to reserve
423

424
    """
425
    all_lvs = self._AllLVs()
426
    if lv_name in all_lvs:
427
      raise errors.ReservationError("LV already in use")
428
    else:
429
      self._temporary_lvs.Reserve(ec_id, lv_name)
430

    
431
  @locking.ssynchronized(_config_lock, shared=1)
432
  def GenerateDRBDSecret(self, ec_id):
433
    """Generate a DRBD secret.
434

435
    This checks the current disks for duplicates.
436

437
    """
438
    return self._temporary_secrets.Generate(self._AllDRBDSecrets(),
439
                                            utils.GenerateSecret,
440
                                            ec_id)
441

    
442
  def _AllLVs(self):
443
    """Compute the list of all LVs.
444

445
    """
446
    lvnames = set()
447
    for instance in self._config_data.instances.values():
448
      node_data = instance.MapLVsByNode()
449
      for lv_list in node_data.values():
450
        lvnames.update(lv_list)
451
    return lvnames
452

    
453
  def _AllIDs(self, include_temporary):
454
    """Compute the list of all UUIDs and names we have.
455

456
    @type include_temporary: boolean
457
    @param include_temporary: whether to include the _temporary_ids set
458
    @rtype: set
459
    @return: a set of IDs
460

461
    """
462
    existing = set()
463
    if include_temporary:
464
      existing.update(self._temporary_ids.GetReserved())
465
    existing.update(self._AllLVs())
466
    existing.update(self._config_data.instances.keys())
467
    existing.update(self._config_data.nodes.keys())
468
    existing.update([i.uuid for i in self._AllUUIDObjects() if i.uuid])
469
    return existing
470

    
471
  def _GenerateUniqueID(self, ec_id):
472
    """Generate an unique UUID.
473

474
    This checks the current node, instances and disk names for
475
    duplicates.
476

477
    @rtype: string
478
    @return: the unique id
479

480
    """
481
    existing = self._AllIDs(include_temporary=False)
482
    return self._temporary_ids.Generate(existing, utils.NewUUID, ec_id)
483

    
484
  @locking.ssynchronized(_config_lock, shared=1)
485
  def GenerateUniqueID(self, ec_id):
486
    """Generate an unique ID.
487

488
    This is just a wrapper over the unlocked version.
489

490
    @type ec_id: string
491
    @param ec_id: unique id for the job to reserve the id to
492

493
    """
494
    return self._GenerateUniqueID(ec_id)
495

    
496
  def _AllMACs(self):
497
    """Return all MACs present in the config.
498

499
    @rtype: list
500
    @return: the list of all MACs
501

502
    """
503
    result = []
504
    for instance in self._config_data.instances.values():
505
      for nic in instance.nics:
506
        result.append(nic.mac)
507

    
508
    return result
509

    
510
  def _AllDRBDSecrets(self):
511
    """Return all DRBD secrets present in the config.
512

513
    @rtype: list
514
    @return: the list of all DRBD secrets
515

516
    """
517
    def helper(disk, result):
518
      """Recursively gather secrets from this disk."""
519
      if disk.dev_type == constants.DT_DRBD8:
520
        result.append(disk.logical_id[5])
521
      if disk.children:
522
        for child in disk.children:
523
          helper(child, result)
524

    
525
    result = []
526
    for instance in self._config_data.instances.values():
527
      for disk in instance.disks:
528
        helper(disk, result)
529

    
530
    return result
531

    
532
  def _CheckDiskIDs(self, disk, l_ids, p_ids):
533
    """Compute duplicate disk IDs
534

535
    @type disk: L{objects.Disk}
536
    @param disk: the disk at which to start searching
537
    @type l_ids: list
538
    @param l_ids: list of current logical ids
539
    @type p_ids: list
540
    @param p_ids: list of current physical ids
541
    @rtype: list
542
    @return: a list of error messages
543

544
    """
545
    result = []
546
    if disk.logical_id is not None:
547
      if disk.logical_id in l_ids:
548
        result.append("duplicate logical id %s" % str(disk.logical_id))
549
      else:
550
        l_ids.append(disk.logical_id)
551
    if disk.physical_id is not None:
552
      if disk.physical_id in p_ids:
553
        result.append("duplicate physical id %s" % str(disk.physical_id))
554
      else:
555
        p_ids.append(disk.physical_id)
556

    
557
    if disk.children:
558
      for child in disk.children:
559
        result.extend(self._CheckDiskIDs(child, l_ids, p_ids))
560
    return result
561

    
562
  def _UnlockedVerifyConfig(self):
563
    """Verify function.
564

565
    @rtype: list
566
    @return: a list of error messages; a non-empty list signifies
567
        configuration errors
568

569
    """
570
    # pylint: disable=R0914
571
    result = []
572
    seen_macs = []
573
    ports = {}
574
    data = self._config_data
575
    cluster = data.cluster
576
    seen_lids = []
577
    seen_pids = []
578

    
579
    # global cluster checks
580
    if not cluster.enabled_hypervisors:
581
      result.append("enabled hypervisors list doesn't have any entries")
582
    invalid_hvs = set(cluster.enabled_hypervisors) - constants.HYPER_TYPES
583
    if invalid_hvs:
584
      result.append("enabled hypervisors contains invalid entries: %s" %
585
                    invalid_hvs)
586
    missing_hvp = (set(cluster.enabled_hypervisors) -
587
                   set(cluster.hvparams.keys()))
588
    if missing_hvp:
589
      result.append("hypervisor parameters missing for the enabled"
590
                    " hypervisor(s) %s" % utils.CommaJoin(missing_hvp))
591

    
592
    if cluster.master_node not in data.nodes:
593
      result.append("cluster has invalid primary node '%s'" %
594
                    cluster.master_node)
595

    
596
    def _helper(owner, attr, value, template):
597
      try:
598
        utils.ForceDictType(value, template)
599
      except errors.GenericError, err:
600
        result.append("%s has invalid %s: %s" % (owner, attr, err))
601

    
602
    def _helper_nic(owner, params):
603
      try:
604
        objects.NIC.CheckParameterSyntax(params)
605
      except errors.ConfigurationError, err:
606
        result.append("%s has invalid nicparams: %s" % (owner, err))
607

    
608
    def _helper_ipolicy(owner, params, check_std):
609
      try:
610
        objects.InstancePolicy.CheckParameterSyntax(params, check_std)
611
      except errors.ConfigurationError, err:
612
        result.append("%s has invalid instance policy: %s" % (owner, err))
613

    
614
    def _helper_ispecs(owner, params):
615
      for key, value in params.items():
616
        if key in constants.IPOLICY_ISPECS:
617
          fullkey = "ipolicy/" + key
618
          _helper(owner, fullkey, value, constants.ISPECS_PARAMETER_TYPES)
619
        else:
620
          # FIXME: assuming list type
621
          if key in constants.IPOLICY_PARAMETERS:
622
            exp_type = float
623
          else:
624
            exp_type = list
625
          if not isinstance(value, exp_type):
626
            result.append("%s has invalid instance policy: for %s,"
627
                          " expecting %s, got %s" %
628
                          (owner, key, exp_type.__name__, type(value)))
629

    
630
    # check cluster parameters
631
    _helper("cluster", "beparams", cluster.SimpleFillBE({}),
632
            constants.BES_PARAMETER_TYPES)
633
    _helper("cluster", "nicparams", cluster.SimpleFillNIC({}),
634
            constants.NICS_PARAMETER_TYPES)
635
    _helper_nic("cluster", cluster.SimpleFillNIC({}))
636
    _helper("cluster", "ndparams", cluster.SimpleFillND({}),
637
            constants.NDS_PARAMETER_TYPES)
638
    _helper_ipolicy("cluster", cluster.SimpleFillIPolicy({}), True)
639
    _helper_ispecs("cluster", cluster.SimpleFillIPolicy({}))
640

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

    
667
      # parameter checks
668
      if instance.beparams:
669
        _helper("instance %s" % instance.name, "beparams",
670
                cluster.FillBE(instance), constants.BES_PARAMETER_TYPES)
671

    
672
      # gather the drbd ports for duplicate checks
673
      for (idx, dsk) in enumerate(instance.disks):
674
        if dsk.dev_type in constants.LDS_DRBD:
675
          tcp_port = dsk.logical_id[2]
676
          if tcp_port not in ports:
677
            ports[tcp_port] = []
678
          ports[tcp_port].append((instance.name, "drbd disk %s" % idx))
679
      # gather network port reservation
680
      net_port = getattr(instance, "network_port", None)
681
      if net_port is not None:
682
        if net_port not in ports:
683
          ports[net_port] = []
684
        ports[net_port].append((instance.name, "network port"))
685

    
686
      # instance disk verify
687
      for idx, disk in enumerate(instance.disks):
688
        result.extend(["instance '%s' disk %d error: %s" %
689
                       (instance.name, idx, msg) for msg in disk.Verify()])
690
        result.extend(self._CheckDiskIDs(disk, seen_lids, seen_pids))
691

    
692
      wrong_names = _CheckInstanceDiskIvNames(instance.disks)
693
      if wrong_names:
694
        tmp = "; ".join(("name of disk %s should be '%s', but is '%s'" %
695
                         (idx, exp_name, actual_name))
696
                        for (idx, exp_name, actual_name) in wrong_names)
697

    
698
        result.append("Instance '%s' has wrongly named disks: %s" %
699
                      (instance.name, tmp))
700

    
701
    # cluster-wide pool of free ports
702
    for free_port in cluster.tcpudp_port_pool:
703
      if free_port not in ports:
704
        ports[free_port] = []
705
      ports[free_port].append(("cluster", "port marked as free"))
706

    
707
    # compute tcp/udp duplicate ports
708
    keys = ports.keys()
709
    keys.sort()
710
    for pnum in keys:
711
      pdata = ports[pnum]
712
      if len(pdata) > 1:
713
        txt = utils.CommaJoin(["%s/%s" % val for val in pdata])
714
        result.append("tcp/udp port %s has duplicates: %s" % (pnum, txt))
715

    
716
    # highest used tcp port check
717
    if keys:
718
      if keys[-1] > cluster.highest_used_port:
719
        result.append("Highest used port mismatch, saved %s, computed %s" %
720
                      (cluster.highest_used_port, keys[-1]))
721

    
722
    if not data.nodes[cluster.master_node].master_candidate:
723
      result.append("Master node is not a master candidate")
724

    
725
    # master candidate checks
726
    mc_now, mc_max, _ = self._UnlockedGetMasterCandidateStats()
727
    if mc_now < mc_max:
728
      result.append("Not enough master candidates: actual %d, target %d" %
729
                    (mc_now, mc_max))
730

    
731
    # node checks
732
    for node_name, node in data.nodes.items():
733
      if node.name != node_name:
734
        result.append("Node '%s' is indexed by wrong name '%s'" %
735
                      (node.name, node_name))
736
      if [node.master_candidate, node.drained, node.offline].count(True) > 1:
737
        result.append("Node %s state is invalid: master_candidate=%s,"
738
                      " drain=%s, offline=%s" %
739
                      (node.name, node.master_candidate, node.drained,
740
                       node.offline))
741
      if node.group not in data.nodegroups:
742
        result.append("Node '%s' has invalid group '%s'" %
743
                      (node.name, node.group))
744
      else:
745
        _helper("node %s" % node.name, "ndparams",
746
                cluster.FillND(node, data.nodegroups[node.group]),
747
                constants.NDS_PARAMETER_TYPES)
748
      used_globals = constants.NDC_GLOBALS.intersection(node.ndparams)
749
      if used_globals:
750
        result.append("Node '%s' has some global parameters set: %s" %
751
                      (node.name, utils.CommaJoin(used_globals)))
752

    
753
    # nodegroups checks
754
    nodegroups_names = set()
755
    for nodegroup_uuid in data.nodegroups:
756
      nodegroup = data.nodegroups[nodegroup_uuid]
757
      if nodegroup.uuid != nodegroup_uuid:
758
        result.append("node group '%s' (uuid: '%s') indexed by wrong uuid '%s'"
759
                      % (nodegroup.name, nodegroup.uuid, nodegroup_uuid))
760
      if utils.UUID_RE.match(nodegroup.name.lower()):
761
        result.append("node group '%s' (uuid: '%s') has uuid-like name" %
762
                      (nodegroup.name, nodegroup.uuid))
763
      if nodegroup.name in nodegroups_names:
764
        result.append("duplicate node group name '%s'" % nodegroup.name)
765
      else:
766
        nodegroups_names.add(nodegroup.name)
767
      group_name = "group %s" % nodegroup.name
768
      _helper_ipolicy(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy),
769
                      False)
770
      _helper_ispecs(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy))
771
      if nodegroup.ndparams:
772
        _helper(group_name, "ndparams",
773
                cluster.SimpleFillND(nodegroup.ndparams),
774
                constants.NDS_PARAMETER_TYPES)
775

    
776
    # drbd minors check
777
    _, duplicates = self._UnlockedComputeDRBDMap()
778
    for node, minor, instance_a, instance_b in duplicates:
779
      result.append("DRBD minor %d on node %s is assigned twice to instances"
780
                    " %s and %s" % (minor, node, instance_a, instance_b))
781

    
782
    # IP checks
783
    default_nicparams = cluster.nicparams[constants.PP_DEFAULT]
784
    ips = {}
785

    
786
    def _AddIpAddress(ip, name):
787
      ips.setdefault(ip, []).append(name)
788

    
789
    _AddIpAddress(cluster.master_ip, "cluster_ip")
790

    
791
    for node in data.nodes.values():
792
      _AddIpAddress(node.primary_ip, "node:%s/primary" % node.name)
793
      if node.secondary_ip != node.primary_ip:
794
        _AddIpAddress(node.secondary_ip, "node:%s/secondary" % node.name)
795

    
796
    for instance in data.instances.values():
797
      for idx, nic in enumerate(instance.nics):
798
        if nic.ip is None:
799
          continue
800

    
801
        nicparams = objects.FillDict(default_nicparams, nic.nicparams)
802
        nic_mode = nicparams[constants.NIC_MODE]
803
        nic_link = nicparams[constants.NIC_LINK]
804

    
805
        if nic_mode == constants.NIC_MODE_BRIDGED:
806
          link = "bridge:%s" % nic_link
807
        elif nic_mode == constants.NIC_MODE_ROUTED:
808
          link = "route:%s" % nic_link
809
        else:
810
          raise errors.ProgrammerError("NIC mode '%s' not handled" % nic_mode)
811

    
812
        _AddIpAddress("%s/%s/%s" % (link, nic.ip, nic.network),
813
                      "instance:%s/nic:%d" % (instance.name, idx))
814

    
815
    for ip, owners in ips.items():
816
      if len(owners) > 1:
817
        result.append("IP address %s is used by multiple owners: %s" %
818
                      (ip, utils.CommaJoin(owners)))
819

    
820
    return result
821

    
822
  @locking.ssynchronized(_config_lock, shared=1)
823
  def VerifyConfig(self):
824
    """Verify function.
825

826
    This is just a wrapper over L{_UnlockedVerifyConfig}.
827

828
    @rtype: list
829
    @return: a list of error messages; a non-empty list signifies
830
        configuration errors
831

832
    """
833
    return self._UnlockedVerifyConfig()
834

    
835
  def _UnlockedSetDiskID(self, disk, node_name):
836
    """Convert the unique ID to the ID needed on the target nodes.
837

838
    This is used only for drbd, which needs ip/port configuration.
839

840
    The routine descends down and updates its children also, because
841
    this helps when the only the top device is passed to the remote
842
    node.
843

844
    This function is for internal use, when the config lock is already held.
845

846
    """
847
    if disk.children:
848
      for child in disk.children:
849
        self._UnlockedSetDiskID(child, node_name)
850

    
851
    if disk.logical_id is None and disk.physical_id is not None:
852
      return
853
    if disk.dev_type == constants.LD_DRBD8:
854
      pnode, snode, port, pminor, sminor, secret = disk.logical_id
855
      if node_name not in (pnode, snode):
856
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
857
                                        node_name)
858
      pnode_info = self._UnlockedGetNodeInfo(pnode)
859
      snode_info = self._UnlockedGetNodeInfo(snode)
860
      if pnode_info is None or snode_info is None:
861
        raise errors.ConfigurationError("Can't find primary or secondary node"
862
                                        " for %s" % str(disk))
863
      p_data = (pnode_info.secondary_ip, port)
864
      s_data = (snode_info.secondary_ip, port)
865
      if pnode == node_name:
866
        disk.physical_id = p_data + s_data + (pminor, secret)
867
      else: # it must be secondary, we tested above
868
        disk.physical_id = s_data + p_data + (sminor, secret)
869
    else:
870
      disk.physical_id = disk.logical_id
871
    return
872

    
873
  @locking.ssynchronized(_config_lock)
874
  def SetDiskID(self, disk, node_name):
875
    """Convert the unique ID to the ID needed on the target nodes.
876

877
    This is used only for drbd, which needs ip/port configuration.
878

879
    The routine descends down and updates its children also, because
880
    this helps when the only the top device is passed to the remote
881
    node.
882

883
    """
884
    return self._UnlockedSetDiskID(disk, node_name)
885

    
886
  @locking.ssynchronized(_config_lock)
887
  def AddTcpUdpPort(self, port):
888
    """Adds a new port to the available port pool.
889

890
    @warning: this method does not "flush" the configuration (via
891
        L{_WriteConfig}); callers should do that themselves once the
892
        configuration is stable
893

894
    """
895
    if not isinstance(port, int):
896
      raise errors.ProgrammerError("Invalid type passed for port")
897

    
898
    self._config_data.cluster.tcpudp_port_pool.add(port)
899

    
900
  @locking.ssynchronized(_config_lock, shared=1)
901
  def GetPortList(self):
902
    """Returns a copy of the current port list.
903

904
    """
905
    return self._config_data.cluster.tcpudp_port_pool.copy()
906

    
907
  @locking.ssynchronized(_config_lock)
908
  def AllocatePort(self):
909
    """Allocate a port.
910

911
    The port will be taken from the available port pool or from the
912
    default port range (and in this case we increase
913
    highest_used_port).
914

915
    """
916
    # If there are TCP/IP ports configured, we use them first.
917
    if self._config_data.cluster.tcpudp_port_pool:
918
      port = self._config_data.cluster.tcpudp_port_pool.pop()
919
    else:
920
      port = self._config_data.cluster.highest_used_port + 1
921
      if port >= constants.LAST_DRBD_PORT:
922
        raise errors.ConfigurationError("The highest used port is greater"
923
                                        " than %s. Aborting." %
924
                                        constants.LAST_DRBD_PORT)
925
      self._config_data.cluster.highest_used_port = port
926

    
927
    self._WriteConfig()
928
    return port
929

    
930
  def _UnlockedComputeDRBDMap(self):
931
    """Compute the used DRBD minor/nodes.
932

933
    @rtype: (dict, list)
934
    @return: dictionary of node_name: dict of minor: instance_name;
935
        the returned dict will have all the nodes in it (even if with
936
        an empty list), and a list of duplicates; if the duplicates
937
        list is not empty, the configuration is corrupted and its caller
938
        should raise an exception
939

940
    """
941
    def _AppendUsedPorts(instance_name, disk, used):
942
      duplicates = []
943
      if disk.dev_type == constants.LD_DRBD8 and len(disk.logical_id) >= 5:
944
        node_a, node_b, _, minor_a, minor_b = disk.logical_id[:5]
945
        for node, port in ((node_a, minor_a), (node_b, minor_b)):
946
          assert node in used, ("Node '%s' of instance '%s' not found"
947
                                " in node list" % (node, instance_name))
948
          if port in used[node]:
949
            duplicates.append((node, port, instance_name, used[node][port]))
950
          else:
951
            used[node][port] = instance_name
952
      if disk.children:
953
        for child in disk.children:
954
          duplicates.extend(_AppendUsedPorts(instance_name, child, used))
955
      return duplicates
956

    
957
    duplicates = []
958
    my_dict = dict((node, {}) for node in self._config_data.nodes)
959
    for instance in self._config_data.instances.itervalues():
960
      for disk in instance.disks:
961
        duplicates.extend(_AppendUsedPorts(instance.name, disk, my_dict))
962
    for (node, minor), instance in self._temporary_drbds.iteritems():
963
      if minor in my_dict[node] and my_dict[node][minor] != instance:
964
        duplicates.append((node, minor, instance, my_dict[node][minor]))
965
      else:
966
        my_dict[node][minor] = instance
967
    return my_dict, duplicates
968

    
969
  @locking.ssynchronized(_config_lock)
970
  def ComputeDRBDMap(self):
971
    """Compute the used DRBD minor/nodes.
972

973
    This is just a wrapper over L{_UnlockedComputeDRBDMap}.
974

975
    @return: dictionary of node_name: dict of minor: instance_name;
976
        the returned dict will have all the nodes in it (even if with
977
        an empty list).
978

979
    """
980
    d_map, duplicates = self._UnlockedComputeDRBDMap()
981
    if duplicates:
982
      raise errors.ConfigurationError("Duplicate DRBD ports detected: %s" %
983
                                      str(duplicates))
984
    return d_map
985

    
986
  @locking.ssynchronized(_config_lock)
987
  def AllocateDRBDMinor(self, nodes, instance):
988
    """Allocate a drbd minor.
989

990
    The free minor will be automatically computed from the existing
991
    devices. A node can be given multiple times in order to allocate
992
    multiple minors. The result is the list of minors, in the same
993
    order as the passed nodes.
994

995
    @type instance: string
996
    @param instance: the instance for which we allocate minors
997

998
    """
999
    assert isinstance(instance, basestring), \
1000
           "Invalid argument '%s' passed to AllocateDRBDMinor" % instance
1001

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

    
1042
  def _UnlockedReleaseDRBDMinors(self, instance):
1043
    """Release temporary drbd minors allocated for a given instance.
1044

1045
    @type instance: string
1046
    @param instance: the instance for which temporary minors should be
1047
                     released
1048

1049
    """
1050
    assert isinstance(instance, basestring), \
1051
           "Invalid argument passed to ReleaseDRBDMinors"
1052
    for key, name in self._temporary_drbds.items():
1053
      if name == instance:
1054
        del self._temporary_drbds[key]
1055

    
1056
  @locking.ssynchronized(_config_lock)
1057
  def ReleaseDRBDMinors(self, instance):
1058
    """Release temporary drbd minors allocated for a given instance.
1059

1060
    This should be called on the error paths, on the success paths
1061
    it's automatically called by the ConfigWriter add and update
1062
    functions.
1063

1064
    This function is just a wrapper over L{_UnlockedReleaseDRBDMinors}.
1065

1066
    @type instance: string
1067
    @param instance: the instance for which temporary minors should be
1068
                     released
1069

1070
    """
1071
    self._UnlockedReleaseDRBDMinors(instance)
1072

    
1073
  @locking.ssynchronized(_config_lock, shared=1)
1074
  def GetConfigVersion(self):
1075
    """Get the configuration version.
1076

1077
    @return: Config version
1078

1079
    """
1080
    return self._config_data.version
1081

    
1082
  @locking.ssynchronized(_config_lock, shared=1)
1083
  def GetClusterName(self):
1084
    """Get cluster name.
1085

1086
    @return: Cluster name
1087

1088
    """
1089
    return self._config_data.cluster.cluster_name
1090

    
1091
  @locking.ssynchronized(_config_lock, shared=1)
1092
  def GetMasterNode(self):
1093
    """Get the hostname of the master node for this cluster.
1094

1095
    @return: Master hostname
1096

1097
    """
1098
    return self._config_data.cluster.master_node
1099

    
1100
  @locking.ssynchronized(_config_lock, shared=1)
1101
  def GetMasterIP(self):
1102
    """Get the IP of the master node for this cluster.
1103

1104
    @return: Master IP
1105

1106
    """
1107
    return self._config_data.cluster.master_ip
1108

    
1109
  @locking.ssynchronized(_config_lock, shared=1)
1110
  def GetMasterNetdev(self):
1111
    """Get the master network device for this cluster.
1112

1113
    """
1114
    return self._config_data.cluster.master_netdev
1115

    
1116
  @locking.ssynchronized(_config_lock, shared=1)
1117
  def GetMasterNetmask(self):
1118
    """Get the netmask of the master node for this cluster.
1119

1120
    """
1121
    return self._config_data.cluster.master_netmask
1122

    
1123
  @locking.ssynchronized(_config_lock, shared=1)
1124
  def GetUseExternalMipScript(self):
1125
    """Get flag representing whether to use the external master IP setup script.
1126

1127
    """
1128
    return self._config_data.cluster.use_external_mip_script
1129

    
1130
  @locking.ssynchronized(_config_lock, shared=1)
1131
  def GetFileStorageDir(self):
1132
    """Get the file storage dir for this cluster.
1133

1134
    """
1135
    return self._config_data.cluster.file_storage_dir
1136

    
1137
  @locking.ssynchronized(_config_lock, shared=1)
1138
  def GetSharedFileStorageDir(self):
1139
    """Get the shared file storage dir for this cluster.
1140

1141
    """
1142
    return self._config_data.cluster.shared_file_storage_dir
1143

    
1144
  @locking.ssynchronized(_config_lock, shared=1)
1145
  def GetHypervisorType(self):
1146
    """Get the hypervisor type for this cluster.
1147

1148
    """
1149
    return self._config_data.cluster.enabled_hypervisors[0]
1150

    
1151
  @locking.ssynchronized(_config_lock, shared=1)
1152
  def GetHostKey(self):
1153
    """Return the rsa hostkey from the config.
1154

1155
    @rtype: string
1156
    @return: the rsa hostkey
1157

1158
    """
1159
    return self._config_data.cluster.rsahostkeypub
1160

    
1161
  @locking.ssynchronized(_config_lock, shared=1)
1162
  def GetDefaultIAllocator(self):
1163
    """Get the default instance allocator for this cluster.
1164

1165
    """
1166
    return self._config_data.cluster.default_iallocator
1167

    
1168
  @locking.ssynchronized(_config_lock, shared=1)
1169
  def GetPrimaryIPFamily(self):
1170
    """Get cluster primary ip family.
1171

1172
    @return: primary ip family
1173

1174
    """
1175
    return self._config_data.cluster.primary_ip_family
1176

    
1177
  @locking.ssynchronized(_config_lock, shared=1)
1178
  def GetMasterNetworkParameters(self):
1179
    """Get network parameters of the master node.
1180

1181
    @rtype: L{object.MasterNetworkParameters}
1182
    @return: network parameters of the master node
1183

1184
    """
1185
    cluster = self._config_data.cluster
1186
    result = objects.MasterNetworkParameters(
1187
      name=cluster.master_node, ip=cluster.master_ip,
1188
      netmask=cluster.master_netmask, netdev=cluster.master_netdev,
1189
      ip_family=cluster.primary_ip_family)
1190

    
1191
    return result
1192

    
1193
  @locking.ssynchronized(_config_lock)
1194
  def AddNodeGroup(self, group, ec_id, check_uuid=True):
1195
    """Add a node group to the configuration.
1196

1197
    This method calls group.UpgradeConfig() to fill any missing attributes
1198
    according to their default values.
1199

1200
    @type group: L{objects.NodeGroup}
1201
    @param group: the NodeGroup object to add
1202
    @type ec_id: string
1203
    @param ec_id: unique id for the job to use when creating a missing UUID
1204
    @type check_uuid: bool
1205
    @param check_uuid: add an UUID to the group if it doesn't have one or, if
1206
                       it does, ensure that it does not exist in the
1207
                       configuration already
1208

1209
    """
1210
    self._UnlockedAddNodeGroup(group, ec_id, check_uuid)
1211
    self._WriteConfig()
1212

    
1213
  def _UnlockedAddNodeGroup(self, group, ec_id, check_uuid):
1214
    """Add a node group to the configuration.
1215

1216
    """
1217
    logging.info("Adding node group %s to configuration", group.name)
1218

    
1219
    # Some code might need to add a node group with a pre-populated UUID
1220
    # generated with ConfigWriter.GenerateUniqueID(). We allow them to bypass
1221
    # the "does this UUID" exist already check.
1222
    if check_uuid:
1223
      self._EnsureUUID(group, ec_id)
1224

    
1225
    try:
1226
      existing_uuid = self._UnlockedLookupNodeGroup(group.name)
1227
    except errors.OpPrereqError:
1228
      pass
1229
    else:
1230
      raise errors.OpPrereqError("Desired group name '%s' already exists as a"
1231
                                 " node group (UUID: %s)" %
1232
                                 (group.name, existing_uuid),
1233
                                 errors.ECODE_EXISTS)
1234

    
1235
    group.serial_no = 1
1236
    group.ctime = group.mtime = time.time()
1237
    group.UpgradeConfig()
1238

    
1239
    self._config_data.nodegroups[group.uuid] = group
1240
    self._config_data.cluster.serial_no += 1
1241

    
1242
  @locking.ssynchronized(_config_lock)
1243
  def RemoveNodeGroup(self, group_uuid):
1244
    """Remove a node group from the configuration.
1245

1246
    @type group_uuid: string
1247
    @param group_uuid: the UUID of the node group to remove
1248

1249
    """
1250
    logging.info("Removing node group %s from configuration", group_uuid)
1251

    
1252
    if group_uuid not in self._config_data.nodegroups:
1253
      raise errors.ConfigurationError("Unknown node group '%s'" % group_uuid)
1254

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

    
1258
    del self._config_data.nodegroups[group_uuid]
1259
    self._config_data.cluster.serial_no += 1
1260
    self._WriteConfig()
1261

    
1262
  def _UnlockedLookupNodeGroup(self, target):
1263
    """Lookup a node group's UUID.
1264

1265
    @type target: string or None
1266
    @param target: group name or UUID or None to look for the default
1267
    @rtype: string
1268
    @return: nodegroup UUID
1269
    @raises errors.OpPrereqError: when the target group cannot be found
1270

1271
    """
1272
    if target is None:
1273
      if len(self._config_data.nodegroups) != 1:
1274
        raise errors.OpPrereqError("More than one node group exists. Target"
1275
                                   " group must be specified explicitly.")
1276
      else:
1277
        return self._config_data.nodegroups.keys()[0]
1278
    if target in self._config_data.nodegroups:
1279
      return target
1280
    for nodegroup in self._config_data.nodegroups.values():
1281
      if nodegroup.name == target:
1282
        return nodegroup.uuid
1283
    raise errors.OpPrereqError("Node group '%s' not found" % target,
1284
                               errors.ECODE_NOENT)
1285

    
1286
  @locking.ssynchronized(_config_lock, shared=1)
1287
  def LookupNodeGroup(self, target):
1288
    """Lookup a node group's UUID.
1289

1290
    This function is just a wrapper over L{_UnlockedLookupNodeGroup}.
1291

1292
    @type target: string or None
1293
    @param target: group name or UUID or None to look for the default
1294
    @rtype: string
1295
    @return: nodegroup UUID
1296

1297
    """
1298
    return self._UnlockedLookupNodeGroup(target)
1299

    
1300
  def _UnlockedGetNodeGroup(self, uuid):
1301
    """Lookup a node group.
1302

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

1308
    """
1309
    if uuid not in self._config_data.nodegroups:
1310
      return None
1311

    
1312
    return self._config_data.nodegroups[uuid]
1313

    
1314
  @locking.ssynchronized(_config_lock, shared=1)
1315
  def GetNodeGroup(self, uuid):
1316
    """Lookup a node group.
1317

1318
    @type uuid: string
1319
    @param uuid: group UUID
1320
    @rtype: L{objects.NodeGroup} or None
1321
    @return: nodegroup object, or None if not found
1322

1323
    """
1324
    return self._UnlockedGetNodeGroup(uuid)
1325

    
1326
  @locking.ssynchronized(_config_lock, shared=1)
1327
  def GetAllNodeGroupsInfo(self):
1328
    """Get the configuration of all node groups.
1329

1330
    """
1331
    return dict(self._config_data.nodegroups)
1332

    
1333
  @locking.ssynchronized(_config_lock, shared=1)
1334
  def GetNodeGroupList(self):
1335
    """Get a list of node groups.
1336

1337
    """
1338
    return self._config_data.nodegroups.keys()
1339

    
1340
  @locking.ssynchronized(_config_lock, shared=1)
1341
  def GetNodeGroupMembersByNodes(self, nodes):
1342
    """Get nodes which are member in the same nodegroups as the given nodes.
1343

1344
    """
1345
    ngfn = lambda node_name: self._UnlockedGetNodeInfo(node_name).group
1346
    return frozenset(member_name
1347
                     for node_name in nodes
1348
                     for member_name in
1349
                       self._UnlockedGetNodeGroup(ngfn(node_name)).members)
1350

    
1351
  @locking.ssynchronized(_config_lock, shared=1)
1352
  def GetMultiNodeGroupInfo(self, group_uuids):
1353
    """Get the configuration of multiple node groups.
1354

1355
    @param group_uuids: List of node group UUIDs
1356
    @rtype: list
1357
    @return: List of tuples of (group_uuid, group_info)
1358

1359
    """
1360
    return [(uuid, self._UnlockedGetNodeGroup(uuid)) for uuid in group_uuids]
1361

    
1362
  @locking.ssynchronized(_config_lock)
1363
  def AddInstance(self, instance, ec_id):
1364
    """Add an instance to the config.
1365

1366
    This should be used after creating a new instance.
1367

1368
    @type instance: L{objects.Instance}
1369
    @param instance: the instance object
1370

1371
    """
1372
    if not isinstance(instance, objects.Instance):
1373
      raise errors.ProgrammerError("Invalid type passed to AddInstance")
1374

    
1375
    if instance.disk_template != constants.DT_DISKLESS:
1376
      all_lvs = instance.MapLVsByNode()
1377
      logging.info("Instance '%s' DISK_LAYOUT: %s", instance.name, all_lvs)
1378

    
1379
    all_macs = self._AllMACs()
1380
    for nic in instance.nics:
1381
      if nic.mac in all_macs:
1382
        raise errors.ConfigurationError("Cannot add instance %s:"
1383
                                        " MAC address '%s' already in use." %
1384
                                        (instance.name, nic.mac))
1385

    
1386
    self._EnsureUUID(instance, ec_id)
1387

    
1388
    instance.serial_no = 1
1389
    instance.ctime = instance.mtime = time.time()
1390
    self._config_data.instances[instance.name] = instance
1391
    self._config_data.cluster.serial_no += 1
1392
    self._UnlockedReleaseDRBDMinors(instance.name)
1393
    self._UnlockedCommitTemporaryIps(ec_id)
1394
    self._WriteConfig()
1395

    
1396
  def _EnsureUUID(self, item, ec_id):
1397
    """Ensures a given object has a valid UUID.
1398

1399
    @param item: the instance or node to be checked
1400
    @param ec_id: the execution context id for the uuid reservation
1401

1402
    """
1403
    if not item.uuid:
1404
      item.uuid = self._GenerateUniqueID(ec_id)
1405
    elif item.uuid in self._AllIDs(include_temporary=True):
1406
      raise errors.ConfigurationError("Cannot add '%s': UUID %s already"
1407
                                      " in use" % (item.name, item.uuid))
1408

    
1409
  def _SetInstanceStatus(self, instance_name, status):
1410
    """Set the instance's status to a given value.
1411

1412
    """
1413
    assert status in constants.ADMINST_ALL, \
1414
           "Invalid status '%s' passed to SetInstanceStatus" % (status,)
1415

    
1416
    if instance_name not in self._config_data.instances:
1417
      raise errors.ConfigurationError("Unknown instance '%s'" %
1418
                                      instance_name)
1419
    instance = self._config_data.instances[instance_name]
1420
    if instance.admin_state != status:
1421
      instance.admin_state = status
1422
      instance.serial_no += 1
1423
      instance.mtime = time.time()
1424
      self._WriteConfig()
1425

    
1426
  @locking.ssynchronized(_config_lock)
1427
  def MarkInstanceUp(self, instance_name):
1428
    """Mark the instance status to up in the config.
1429

1430
    """
1431
    self._SetInstanceStatus(instance_name, constants.ADMINST_UP)
1432

    
1433
  @locking.ssynchronized(_config_lock)
1434
  def MarkInstanceOffline(self, instance_name):
1435
    """Mark the instance status to down in the config.
1436

1437
    """
1438
    self._SetInstanceStatus(instance_name, constants.ADMINST_OFFLINE)
1439

    
1440
  @locking.ssynchronized(_config_lock)
1441
  def RemoveInstance(self, instance_name):
1442
    """Remove the instance from the configuration.
1443

1444
    """
1445
    if instance_name not in self._config_data.instances:
1446
      raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1447

    
1448
    # If a network port has been allocated to the instance,
1449
    # return it to the pool of free ports.
1450
    inst = self._config_data.instances[instance_name]
1451
    network_port = getattr(inst, "network_port", None)
1452
    if network_port is not None:
1453
      self._config_data.cluster.tcpudp_port_pool.add(network_port)
1454

    
1455
    instance = self._UnlockedGetInstanceInfo(instance_name)
1456

    
1457
    for nic in instance.nics:
1458
      if nic.network is not None and nic.ip is not None:
1459
        net_uuid = self._UnlockedLookupNetwork(nic.network)
1460
        if net_uuid:
1461
          # Return all IP addresses to the respective address pools
1462
          self._UnlockedCommitIp(constants.RELEASE_ACTION, net_uuid, nic.ip)
1463

    
1464
    del self._config_data.instances[instance_name]
1465
    self._config_data.cluster.serial_no += 1
1466
    self._WriteConfig()
1467

    
1468
  @locking.ssynchronized(_config_lock)
1469
  def RenameInstance(self, old_name, new_name):
1470
    """Rename an instance.
1471

1472
    This needs to be done in ConfigWriter and not by RemoveInstance
1473
    combined with AddInstance as only we can guarantee an atomic
1474
    rename.
1475

1476
    """
1477
    if old_name not in self._config_data.instances:
1478
      raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
1479

    
1480
    # Operate on a copy to not loose instance object in case of a failure
1481
    inst = self._config_data.instances[old_name].Copy()
1482
    inst.name = new_name
1483

    
1484
    for (idx, disk) in enumerate(inst.disks):
1485
      if disk.dev_type == constants.LD_FILE:
1486
        # rename the file paths in logical and physical id
1487
        file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1]))
1488
        disk.logical_id = (disk.logical_id[0],
1489
                           utils.PathJoin(file_storage_dir, inst.name,
1490
                                          "disk%s" % idx))
1491
        disk.physical_id = disk.logical_id
1492

    
1493
    # Actually replace instance object
1494
    del self._config_data.instances[old_name]
1495
    self._config_data.instances[inst.name] = inst
1496

    
1497
    # Force update of ssconf files
1498
    self._config_data.cluster.serial_no += 1
1499

    
1500
    self._WriteConfig()
1501

    
1502
  @locking.ssynchronized(_config_lock)
1503
  def MarkInstanceDown(self, instance_name):
1504
    """Mark the status of an instance to down in the configuration.
1505

1506
    """
1507
    self._SetInstanceStatus(instance_name, constants.ADMINST_DOWN)
1508

    
1509
  def _UnlockedGetInstanceList(self):
1510
    """Get the list of instances.
1511

1512
    This function is for internal use, when the config lock is already held.
1513

1514
    """
1515
    return self._config_data.instances.keys()
1516

    
1517
  @locking.ssynchronized(_config_lock, shared=1)
1518
  def GetInstanceList(self):
1519
    """Get the list of instances.
1520

1521
    @return: array of instances, ex. ['instance2.example.com',
1522
        'instance1.example.com']
1523

1524
    """
1525
    return self._UnlockedGetInstanceList()
1526

    
1527
  def ExpandInstanceName(self, short_name):
1528
    """Attempt to expand an incomplete instance name.
1529

1530
    """
1531
    # Locking is done in L{ConfigWriter.GetInstanceList}
1532
    return _MatchNameComponentIgnoreCase(short_name, self.GetInstanceList())
1533

    
1534
  def _UnlockedGetInstanceInfo(self, instance_name):
1535
    """Returns information about an instance.
1536

1537
    This function is for internal use, when the config lock is already held.
1538

1539
    """
1540
    if instance_name not in self._config_data.instances:
1541
      return None
1542

    
1543
    return self._config_data.instances[instance_name]
1544

    
1545
  @locking.ssynchronized(_config_lock, shared=1)
1546
  def GetInstanceInfo(self, instance_name):
1547
    """Returns information about an instance.
1548

1549
    It takes the information from the configuration file. Other information of
1550
    an instance are taken from the live systems.
1551

1552
    @param instance_name: name of the instance, e.g.
1553
        I{instance1.example.com}
1554

1555
    @rtype: L{objects.Instance}
1556
    @return: the instance object
1557

1558
    """
1559
    return self._UnlockedGetInstanceInfo(instance_name)
1560

    
1561
  @locking.ssynchronized(_config_lock, shared=1)
1562
  def GetInstanceNodeGroups(self, instance_name, primary_only=False):
1563
    """Returns set of node group UUIDs for instance's nodes.
1564

1565
    @rtype: frozenset
1566

1567
    """
1568
    instance = self._UnlockedGetInstanceInfo(instance_name)
1569
    if not instance:
1570
      raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1571

    
1572
    if primary_only:
1573
      nodes = [instance.primary_node]
1574
    else:
1575
      nodes = instance.all_nodes
1576

    
1577
    return frozenset(self._UnlockedGetNodeInfo(node_name).group
1578
                     for node_name in nodes)
1579

    
1580
  @locking.ssynchronized(_config_lock, shared=1)
1581
  def GetMultiInstanceInfo(self, instances):
1582
    """Get the configuration of multiple instances.
1583

1584
    @param instances: list of instance names
1585
    @rtype: list
1586
    @return: list of tuples (instance, instance_info), where
1587
        instance_info is what would GetInstanceInfo return for the
1588
        node, while keeping the original order
1589

1590
    """
1591
    return [(name, self._UnlockedGetInstanceInfo(name)) for name in instances]
1592

    
1593
  @locking.ssynchronized(_config_lock, shared=1)
1594
  def GetAllInstancesInfo(self):
1595
    """Get the configuration of all instances.
1596

1597
    @rtype: dict
1598
    @return: dict of (instance, instance_info), where instance_info is what
1599
              would GetInstanceInfo return for the node
1600

1601
    """
1602
    my_dict = dict([(instance, self._UnlockedGetInstanceInfo(instance))
1603
                    for instance in self._UnlockedGetInstanceList()])
1604
    return my_dict
1605

    
1606
  @locking.ssynchronized(_config_lock, shared=1)
1607
  def GetInstancesInfoByFilter(self, filter_fn):
1608
    """Get instance configuration with a filter.
1609

1610
    @type filter_fn: callable
1611
    @param filter_fn: Filter function receiving instance object as parameter,
1612
      returning boolean. Important: this function is called while the
1613
      configuration locks is held. It must not do any complex work or call
1614
      functions potentially leading to a deadlock. Ideally it doesn't call any
1615
      other functions and just compares instance attributes.
1616

1617
    """
1618
    return dict((name, inst)
1619
                for (name, inst) in self._config_data.instances.items()
1620
                if filter_fn(inst))
1621

    
1622
  @locking.ssynchronized(_config_lock)
1623
  def AddNode(self, node, ec_id):
1624
    """Add a node to the configuration.
1625

1626
    @type node: L{objects.Node}
1627
    @param node: a Node instance
1628

1629
    """
1630
    logging.info("Adding node %s to configuration", node.name)
1631

    
1632
    self._EnsureUUID(node, ec_id)
1633

    
1634
    node.serial_no = 1
1635
    node.ctime = node.mtime = time.time()
1636
    self._UnlockedAddNodeToGroup(node.name, node.group)
1637
    self._config_data.nodes[node.name] = node
1638
    self._config_data.cluster.serial_no += 1
1639
    self._WriteConfig()
1640

    
1641
  @locking.ssynchronized(_config_lock)
1642
  def RemoveNode(self, node_name):
1643
    """Remove a node from the configuration.
1644

1645
    """
1646
    logging.info("Removing node %s from configuration", node_name)
1647

    
1648
    if node_name not in self._config_data.nodes:
1649
      raise errors.ConfigurationError("Unknown node '%s'" % node_name)
1650

    
1651
    self._UnlockedRemoveNodeFromGroup(self._config_data.nodes[node_name])
1652
    del self._config_data.nodes[node_name]
1653
    self._config_data.cluster.serial_no += 1
1654
    self._WriteConfig()
1655

    
1656
  def ExpandNodeName(self, short_name):
1657
    """Attempt to expand an incomplete node name.
1658

1659
    """
1660
    # Locking is done in L{ConfigWriter.GetNodeList}
1661
    return _MatchNameComponentIgnoreCase(short_name, self.GetNodeList())
1662

    
1663
  def _UnlockedGetNodeInfo(self, node_name):
1664
    """Get the configuration of a node, as stored in the config.
1665

1666
    This function is for internal use, when the config lock is already
1667
    held.
1668

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

1671
    @rtype: L{objects.Node}
1672
    @return: the node object
1673

1674
    """
1675
    if node_name not in self._config_data.nodes:
1676
      return None
1677

    
1678
    return self._config_data.nodes[node_name]
1679

    
1680
  @locking.ssynchronized(_config_lock, shared=1)
1681
  def GetNodeInfo(self, node_name):
1682
    """Get the configuration of a node, as stored in the config.
1683

1684
    This is just a locked wrapper over L{_UnlockedGetNodeInfo}.
1685

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

1688
    @rtype: L{objects.Node}
1689
    @return: the node object
1690

1691
    """
1692
    return self._UnlockedGetNodeInfo(node_name)
1693

    
1694
  @locking.ssynchronized(_config_lock, shared=1)
1695
  def GetNodeInstances(self, node_name):
1696
    """Get the instances of a node, as stored in the config.
1697

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

1700
    @rtype: (list, list)
1701
    @return: a tuple with two lists: the primary and the secondary instances
1702

1703
    """
1704
    pri = []
1705
    sec = []
1706
    for inst in self._config_data.instances.values():
1707
      if inst.primary_node == node_name:
1708
        pri.append(inst.name)
1709
      if node_name in inst.secondary_nodes:
1710
        sec.append(inst.name)
1711
    return (pri, sec)
1712

    
1713
  @locking.ssynchronized(_config_lock, shared=1)
1714
  def GetNodeGroupInstances(self, uuid, primary_only=False):
1715
    """Get the instances of a node group.
1716

1717
    @param uuid: Node group UUID
1718
    @param primary_only: Whether to only consider primary nodes
1719
    @rtype: frozenset
1720
    @return: List of instance names in node group
1721

1722
    """
1723
    if primary_only:
1724
      nodes_fn = lambda inst: [inst.primary_node]
1725
    else:
1726
      nodes_fn = lambda inst: inst.all_nodes
1727

    
1728
    return frozenset(inst.name
1729
                     for inst in self._config_data.instances.values()
1730
                     for node_name in nodes_fn(inst)
1731
                     if self._UnlockedGetNodeInfo(node_name).group == uuid)
1732

    
1733
  def _UnlockedGetNodeList(self):
1734
    """Return the list of nodes which are in the configuration.
1735

1736
    This function is for internal use, when the config lock is already
1737
    held.
1738

1739
    @rtype: list
1740

1741
    """
1742
    return self._config_data.nodes.keys()
1743

    
1744
  @locking.ssynchronized(_config_lock, shared=1)
1745
  def GetNodeList(self):
1746
    """Return the list of nodes which are in the configuration.
1747

1748
    """
1749
    return self._UnlockedGetNodeList()
1750

    
1751
  def _UnlockedGetOnlineNodeList(self):
1752
    """Return the list of nodes which are online.
1753

1754
    """
1755
    all_nodes = [self._UnlockedGetNodeInfo(node)
1756
                 for node in self._UnlockedGetNodeList()]
1757
    return [node.name for node in all_nodes if not node.offline]
1758

    
1759
  @locking.ssynchronized(_config_lock, shared=1)
1760
  def GetOnlineNodeList(self):
1761
    """Return the list of nodes which are online.
1762

1763
    """
1764
    return self._UnlockedGetOnlineNodeList()
1765

    
1766
  @locking.ssynchronized(_config_lock, shared=1)
1767
  def GetVmCapableNodeList(self):
1768
    """Return the list of nodes which are not vm capable.
1769

1770
    """
1771
    all_nodes = [self._UnlockedGetNodeInfo(node)
1772
                 for node in self._UnlockedGetNodeList()]
1773
    return [node.name for node in all_nodes if node.vm_capable]
1774

    
1775
  @locking.ssynchronized(_config_lock, shared=1)
1776
  def GetNonVmCapableNodeList(self):
1777
    """Return the list of nodes which are not vm capable.
1778

1779
    """
1780
    all_nodes = [self._UnlockedGetNodeInfo(node)
1781
                 for node in self._UnlockedGetNodeList()]
1782
    return [node.name for node in all_nodes if not node.vm_capable]
1783

    
1784
  @locking.ssynchronized(_config_lock, shared=1)
1785
  def GetMultiNodeInfo(self, nodes):
1786
    """Get the configuration of multiple nodes.
1787

1788
    @param nodes: list of node names
1789
    @rtype: list
1790
    @return: list of tuples of (node, node_info), where node_info is
1791
        what would GetNodeInfo return for the node, in the original
1792
        order
1793

1794
    """
1795
    return [(name, self._UnlockedGetNodeInfo(name)) for name in nodes]
1796

    
1797
  @locking.ssynchronized(_config_lock, shared=1)
1798
  def GetAllNodesInfo(self):
1799
    """Get the configuration of all nodes.
1800

1801
    @rtype: dict
1802
    @return: dict of (node, node_info), where node_info is what
1803
              would GetNodeInfo return for the node
1804

1805
    """
1806
    return self._UnlockedGetAllNodesInfo()
1807

    
1808
  def _UnlockedGetAllNodesInfo(self):
1809
    """Gets configuration of all nodes.
1810

1811
    @note: See L{GetAllNodesInfo}
1812

1813
    """
1814
    return dict([(node, self._UnlockedGetNodeInfo(node))
1815
                 for node in self._UnlockedGetNodeList()])
1816

    
1817
  @locking.ssynchronized(_config_lock, shared=1)
1818
  def GetNodeGroupsFromNodes(self, nodes):
1819
    """Returns groups for a list of nodes.
1820

1821
    @type nodes: list of string
1822
    @param nodes: List of node names
1823
    @rtype: frozenset
1824

1825
    """
1826
    return frozenset(self._UnlockedGetNodeInfo(name).group for name in nodes)
1827

    
1828
  def _UnlockedGetMasterCandidateStats(self, exceptions=None):
1829
    """Get the number of current and maximum desired and possible candidates.
1830

1831
    @type exceptions: list
1832
    @param exceptions: if passed, list of nodes that should be ignored
1833
    @rtype: tuple
1834
    @return: tuple of (current, desired and possible, possible)
1835

1836
    """
1837
    mc_now = mc_should = mc_max = 0
1838
    for node in self._config_data.nodes.values():
1839
      if exceptions and node.name in exceptions:
1840
        continue
1841
      if not (node.offline or node.drained) and node.master_capable:
1842
        mc_max += 1
1843
      if node.master_candidate:
1844
        mc_now += 1
1845
    mc_should = min(mc_max, self._config_data.cluster.candidate_pool_size)
1846
    return (mc_now, mc_should, mc_max)
1847

    
1848
  @locking.ssynchronized(_config_lock, shared=1)
1849
  def GetMasterCandidateStats(self, exceptions=None):
1850
    """Get the number of current and maximum possible candidates.
1851

1852
    This is just a wrapper over L{_UnlockedGetMasterCandidateStats}.
1853

1854
    @type exceptions: list
1855
    @param exceptions: if passed, list of nodes that should be ignored
1856
    @rtype: tuple
1857
    @return: tuple of (current, max)
1858

1859
    """
1860
    return self._UnlockedGetMasterCandidateStats(exceptions)
1861

    
1862
  @locking.ssynchronized(_config_lock)
1863
  def MaintainCandidatePool(self, exceptions):
1864
    """Try to grow the candidate pool to the desired size.
1865

1866
    @type exceptions: list
1867
    @param exceptions: if passed, list of nodes that should be ignored
1868
    @rtype: list
1869
    @return: list with the adjusted nodes (L{objects.Node} instances)
1870

1871
    """
1872
    mc_now, mc_max, _ = self._UnlockedGetMasterCandidateStats(exceptions)
1873
    mod_list = []
1874
    if mc_now < mc_max:
1875
      node_list = self._config_data.nodes.keys()
1876
      random.shuffle(node_list)
1877
      for name in node_list:
1878
        if mc_now >= mc_max:
1879
          break
1880
        node = self._config_data.nodes[name]
1881
        if (node.master_candidate or node.offline or node.drained or
1882
            node.name in exceptions or not node.master_capable):
1883
          continue
1884
        mod_list.append(node)
1885
        node.master_candidate = True
1886
        node.serial_no += 1
1887
        mc_now += 1
1888
      if mc_now != mc_max:
1889
        # this should not happen
1890
        logging.warning("Warning: MaintainCandidatePool didn't manage to"
1891
                        " fill the candidate pool (%d/%d)", mc_now, mc_max)
1892
      if mod_list:
1893
        self._config_data.cluster.serial_no += 1
1894
        self._WriteConfig()
1895

    
1896
    return mod_list
1897

    
1898
  def _UnlockedAddNodeToGroup(self, node_name, nodegroup_uuid):
1899
    """Add a given node to the specified group.
1900

1901
    """
1902
    if nodegroup_uuid not in self._config_data.nodegroups:
1903
      # This can happen if a node group gets deleted between its lookup and
1904
      # when we're adding the first node to it, since we don't keep a lock in
1905
      # the meantime. It's ok though, as we'll fail cleanly if the node group
1906
      # is not found anymore.
1907
      raise errors.OpExecError("Unknown node group: %s" % nodegroup_uuid)
1908
    if node_name not in self._config_data.nodegroups[nodegroup_uuid].members:
1909
      self._config_data.nodegroups[nodegroup_uuid].members.append(node_name)
1910

    
1911
  def _UnlockedRemoveNodeFromGroup(self, node):
1912
    """Remove a given node from its group.
1913

1914
    """
1915
    nodegroup = node.group
1916
    if nodegroup not in self._config_data.nodegroups:
1917
      logging.warning("Warning: node '%s' has unknown node group '%s'"
1918
                      " (while being removed from it)", node.name, nodegroup)
1919
    nodegroup_obj = self._config_data.nodegroups[nodegroup]
1920
    if node.name not in nodegroup_obj.members:
1921
      logging.warning("Warning: node '%s' not a member of its node group '%s'"
1922
                      " (while being removed from it)", node.name, nodegroup)
1923
    else:
1924
      nodegroup_obj.members.remove(node.name)
1925

    
1926
  @locking.ssynchronized(_config_lock)
1927
  def AssignGroupNodes(self, mods):
1928
    """Changes the group of a number of nodes.
1929

1930
    @type mods: list of tuples; (node name, new group UUID)
1931
    @param mods: Node membership modifications
1932

1933
    """
1934
    groups = self._config_data.nodegroups
1935
    nodes = self._config_data.nodes
1936

    
1937
    resmod = []
1938

    
1939
    # Try to resolve names/UUIDs first
1940
    for (node_name, new_group_uuid) in mods:
1941
      try:
1942
        node = nodes[node_name]
1943
      except KeyError:
1944
        raise errors.ConfigurationError("Unable to find node '%s'" % node_name)
1945

    
1946
      if node.group == new_group_uuid:
1947
        # Node is being assigned to its current group
1948
        logging.debug("Node '%s' was assigned to its current group (%s)",
1949
                      node_name, node.group)
1950
        continue
1951

    
1952
      # Try to find current group of node
1953
      try:
1954
        old_group = groups[node.group]
1955
      except KeyError:
1956
        raise errors.ConfigurationError("Unable to find old group '%s'" %
1957
                                        node.group)
1958

    
1959
      # Try to find new group for node
1960
      try:
1961
        new_group = groups[new_group_uuid]
1962
      except KeyError:
1963
        raise errors.ConfigurationError("Unable to find new group '%s'" %
1964
                                        new_group_uuid)
1965

    
1966
      assert node.name in old_group.members, \
1967
        ("Inconsistent configuration: node '%s' not listed in members for its"
1968
         " old group '%s'" % (node.name, old_group.uuid))
1969
      assert node.name not in new_group.members, \
1970
        ("Inconsistent configuration: node '%s' already listed in members for"
1971
         " its new group '%s'" % (node.name, new_group.uuid))
1972

    
1973
      resmod.append((node, old_group, new_group))
1974

    
1975
    # Apply changes
1976
    for (node, old_group, new_group) in resmod:
1977
      assert node.uuid != new_group.uuid and old_group.uuid != new_group.uuid, \
1978
        "Assigning to current group is not possible"
1979

    
1980
      node.group = new_group.uuid
1981

    
1982
      # Update members of involved groups
1983
      if node.name in old_group.members:
1984
        old_group.members.remove(node.name)
1985
      if node.name not in new_group.members:
1986
        new_group.members.append(node.name)
1987

    
1988
    # Update timestamps and serials (only once per node/group object)
1989
    now = time.time()
1990
    for obj in frozenset(itertools.chain(*resmod)): # pylint: disable=W0142
1991
      obj.serial_no += 1
1992
      obj.mtime = now
1993

    
1994
    # Force ssconf update
1995
    self._config_data.cluster.serial_no += 1
1996

    
1997
    self._WriteConfig()
1998

    
1999
  def _BumpSerialNo(self):
2000
    """Bump up the serial number of the config.
2001

2002
    """
2003
    self._config_data.serial_no += 1
2004
    self._config_data.mtime = time.time()
2005

    
2006
  def _AllUUIDObjects(self):
2007
    """Returns all objects with uuid attributes.
2008

2009
    """
2010
    return (self._config_data.instances.values() +
2011
            self._config_data.nodes.values() +
2012
            self._config_data.nodegroups.values() +
2013
            [self._config_data.cluster])
2014

    
2015
  def _OpenConfig(self, accept_foreign):
2016
    """Read the config data from disk.
2017

2018
    """
2019
    raw_data = utils.ReadFile(self._cfg_file)
2020

    
2021
    try:
2022
      data = objects.ConfigData.FromDict(serializer.Load(raw_data))
2023
    except Exception, err:
2024
      raise errors.ConfigurationError(err)
2025

    
2026
    # Make sure the configuration has the right version
2027
    _ValidateConfig(data)
2028

    
2029
    if (not hasattr(data, "cluster") or
2030
        not hasattr(data.cluster, "rsahostkeypub")):
2031
      raise errors.ConfigurationError("Incomplete configuration"
2032
                                      " (missing cluster.rsahostkeypub)")
2033

    
2034
    if data.cluster.master_node != self._my_hostname and not accept_foreign:
2035
      msg = ("The configuration denotes node %s as master, while my"
2036
             " hostname is %s; opening a foreign configuration is only"
2037
             " possible in accept_foreign mode" %
2038
             (data.cluster.master_node, self._my_hostname))
2039
      raise errors.ConfigurationError(msg)
2040

    
2041
    self._config_data = data
2042
    # reset the last serial as -1 so that the next write will cause
2043
    # ssconf update
2044
    self._last_cluster_serial = -1
2045

    
2046
    # Upgrade configuration if needed
2047
    self._UpgradeConfig()
2048

    
2049
    self._cfg_id = utils.GetFileID(path=self._cfg_file)
2050

    
2051
  def _UpgradeConfig(self):
2052
    """Run any upgrade steps.
2053

2054
    This method performs both in-object upgrades and also update some data
2055
    elements that need uniqueness across the whole configuration or interact
2056
    with other objects.
2057

2058
    @warning: this function will call L{_WriteConfig()}, but also
2059
        L{DropECReservations} so it needs to be called only from a
2060
        "safe" place (the constructor). If one wanted to call it with
2061
        the lock held, a DropECReservationUnlocked would need to be
2062
        created first, to avoid causing deadlock.
2063

2064
    """
2065
    # Keep a copy of the persistent part of _config_data to check for changes
2066
    # Serialization doesn't guarantee order in dictionaries
2067
    oldconf = copy.deepcopy(self._config_data.ToDict())
2068

    
2069
    # In-object upgrades
2070
    self._config_data.UpgradeConfig()
2071

    
2072
    for item in self._AllUUIDObjects():
2073
      if item.uuid is None:
2074
        item.uuid = self._GenerateUniqueID(_UPGRADE_CONFIG_JID)
2075
    if not self._config_data.nodegroups:
2076
      default_nodegroup_name = constants.INITIAL_NODE_GROUP_NAME
2077
      default_nodegroup = objects.NodeGroup(name=default_nodegroup_name,
2078
                                            members=[])
2079
      self._UnlockedAddNodeGroup(default_nodegroup, _UPGRADE_CONFIG_JID, True)
2080
    for node in self._config_data.nodes.values():
2081
      if not node.group:
2082
        node.group = self.LookupNodeGroup(None)
2083
      # This is technically *not* an upgrade, but needs to be done both when
2084
      # nodegroups are being added, and upon normally loading the config,
2085
      # because the members list of a node group is discarded upon
2086
      # serializing/deserializing the object.
2087
      self._UnlockedAddNodeToGroup(node.name, node.group)
2088

    
2089
    modified = (oldconf != self._config_data.ToDict())
2090
    if modified:
2091
      self._WriteConfig()
2092
      # This is ok even if it acquires the internal lock, as _UpgradeConfig is
2093
      # only called at config init time, without the lock held
2094
      self.DropECReservations(_UPGRADE_CONFIG_JID)
2095

    
2096
  def _DistributeConfig(self, feedback_fn):
2097
    """Distribute the configuration to the other nodes.
2098

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

2102
    """
2103
    if self._offline:
2104
      return True
2105

    
2106
    bad = False
2107

    
2108
    node_list = []
2109
    addr_list = []
2110
    myhostname = self._my_hostname
2111
    # we can skip checking whether _UnlockedGetNodeInfo returns None
2112
    # since the node list comes from _UnlocketGetNodeList, and we are
2113
    # called with the lock held, so no modifications should take place
2114
    # in between
2115
    for node_name in self._UnlockedGetNodeList():
2116
      if node_name == myhostname:
2117
        continue
2118
      node_info = self._UnlockedGetNodeInfo(node_name)
2119
      if not node_info.master_candidate:
2120
        continue
2121
      node_list.append(node_info.name)
2122
      addr_list.append(node_info.primary_ip)
2123

    
2124
    # TODO: Use dedicated resolver talking to config writer for name resolution
2125
    result = \
2126
      self._GetRpc(addr_list).call_upload_file(node_list, self._cfg_file)
2127
    for to_node, to_result in result.items():
2128
      msg = to_result.fail_msg
2129
      if msg:
2130
        msg = ("Copy of file %s to node %s failed: %s" %
2131
               (self._cfg_file, to_node, msg))
2132
        logging.error(msg)
2133

    
2134
        if feedback_fn:
2135
          feedback_fn(msg)
2136

    
2137
        bad = True
2138

    
2139
    return not bad
2140

    
2141
  def _WriteConfig(self, destination=None, feedback_fn=None):
2142
    """Write the configuration data to persistent storage.
2143

2144
    """
2145
    assert feedback_fn is None or callable(feedback_fn)
2146

    
2147
    # Warn on config errors, but don't abort the save - the
2148
    # configuration has already been modified, and we can't revert;
2149
    # the best we can do is to warn the user and save as is, leaving
2150
    # recovery to the user
2151
    config_errors = self._UnlockedVerifyConfig()
2152
    if config_errors:
2153
      errmsg = ("Configuration data is not consistent: %s" %
2154
                (utils.CommaJoin(config_errors)))
2155
      logging.critical(errmsg)
2156
      if feedback_fn:
2157
        feedback_fn(errmsg)
2158

    
2159
    if destination is None:
2160
      destination = self._cfg_file
2161
    self._BumpSerialNo()
2162
    txt = serializer.Dump(self._config_data.ToDict())
2163

    
2164
    getents = self._getents()
2165
    try:
2166
      fd = utils.SafeWriteFile(destination, self._cfg_id, data=txt,
2167
                               close=False, gid=getents.confd_gid, mode=0640)
2168
    except errors.LockError:
2169
      raise errors.ConfigurationError("The configuration file has been"
2170
                                      " modified since the last write, cannot"
2171
                                      " update")
2172
    try:
2173
      self._cfg_id = utils.GetFileID(fd=fd)
2174
    finally:
2175
      os.close(fd)
2176

    
2177
    self.write_count += 1
2178

    
2179
    # and redistribute the config file to master candidates
2180
    self._DistributeConfig(feedback_fn)
2181

    
2182
    # Write ssconf files on all nodes (including locally)
2183
    if self._last_cluster_serial < self._config_data.cluster.serial_no:
2184
      if not self._offline:
2185
        result = self._GetRpc(None).call_write_ssconf_files(
2186
          self._UnlockedGetOnlineNodeList(),
2187
          self._UnlockedGetSsconfValues())
2188

    
2189
        for nname, nresu in result.items():
2190
          msg = nresu.fail_msg
2191
          if msg:
2192
            errmsg = ("Error while uploading ssconf files to"
2193
                      " node %s: %s" % (nname, msg))
2194
            logging.warning(errmsg)
2195

    
2196
            if feedback_fn:
2197
              feedback_fn(errmsg)
2198

    
2199
      self._last_cluster_serial = self._config_data.cluster.serial_no
2200

    
2201
  def _UnlockedGetSsconfValues(self):
2202
    """Return the values needed by ssconf.
2203

2204
    @rtype: dict
2205
    @return: a dictionary with keys the ssconf names and values their
2206
        associated value
2207

2208
    """
2209
    fn = "\n".join
2210
    instance_names = utils.NiceSort(self._UnlockedGetInstanceList())
2211
    node_names = utils.NiceSort(self._UnlockedGetNodeList())
2212
    node_info = [self._UnlockedGetNodeInfo(name) for name in node_names]
2213
    node_pri_ips = ["%s %s" % (ninfo.name, ninfo.primary_ip)
2214
                    for ninfo in node_info]
2215
    node_snd_ips = ["%s %s" % (ninfo.name, ninfo.secondary_ip)
2216
                    for ninfo in node_info]
2217

    
2218
    instance_data = fn(instance_names)
2219
    off_data = fn(node.name for node in node_info if node.offline)
2220
    on_data = fn(node.name for node in node_info if not node.offline)
2221
    mc_data = fn(node.name for node in node_info if node.master_candidate)
2222
    mc_ips_data = fn(node.primary_ip for node in node_info
2223
                     if node.master_candidate)
2224
    node_data = fn(node_names)
2225
    node_pri_ips_data = fn(node_pri_ips)
2226
    node_snd_ips_data = fn(node_snd_ips)
2227

    
2228
    cluster = self._config_data.cluster
2229
    cluster_tags = fn(cluster.GetTags())
2230

    
2231
    hypervisor_list = fn(cluster.enabled_hypervisors)
2232

    
2233
    uid_pool = uidpool.FormatUidPool(cluster.uid_pool, separator="\n")
2234

    
2235
    nodegroups = ["%s %s" % (nodegroup.uuid, nodegroup.name) for nodegroup in
2236
                  self._config_data.nodegroups.values()]
2237
    nodegroups_data = fn(utils.NiceSort(nodegroups))
2238
    networks = ["%s %s" % (net.uuid, net.name) for net in
2239
                self._config_data.networks.values()]
2240
    networks_data = fn(utils.NiceSort(networks))
2241

    
2242
    ssconf_values = {
2243
      constants.SS_CLUSTER_NAME: cluster.cluster_name,
2244
      constants.SS_CLUSTER_TAGS: cluster_tags,
2245
      constants.SS_FILE_STORAGE_DIR: cluster.file_storage_dir,
2246
      constants.SS_SHARED_FILE_STORAGE_DIR: cluster.shared_file_storage_dir,
2247
      constants.SS_MASTER_CANDIDATES: mc_data,
2248
      constants.SS_MASTER_CANDIDATES_IPS: mc_ips_data,
2249
      constants.SS_MASTER_IP: cluster.master_ip,
2250
      constants.SS_MASTER_NETDEV: cluster.master_netdev,
2251
      constants.SS_MASTER_NETMASK: str(cluster.master_netmask),
2252
      constants.SS_MASTER_NODE: cluster.master_node,
2253
      constants.SS_NODE_LIST: node_data,
2254
      constants.SS_NODE_PRIMARY_IPS: node_pri_ips_data,
2255
      constants.SS_NODE_SECONDARY_IPS: node_snd_ips_data,
2256
      constants.SS_OFFLINE_NODES: off_data,
2257
      constants.SS_ONLINE_NODES: on_data,
2258
      constants.SS_PRIMARY_IP_FAMILY: str(cluster.primary_ip_family),
2259
      constants.SS_INSTANCE_LIST: instance_data,
2260
      constants.SS_RELEASE_VERSION: constants.RELEASE_VERSION,
2261
      constants.SS_HYPERVISOR_LIST: hypervisor_list,
2262
      constants.SS_MAINTAIN_NODE_HEALTH: str(cluster.maintain_node_health),
2263
      constants.SS_UID_POOL: uid_pool,
2264
      constants.SS_NODEGROUPS: nodegroups_data,
2265
      constants.SS_NETWORKS: networks_data,
2266
      }
2267
    bad_values = [(k, v) for k, v in ssconf_values.items()
2268
                  if not isinstance(v, (str, basestring))]
2269
    if bad_values:
2270
      err = utils.CommaJoin("%s=%s" % (k, v) for k, v in bad_values)
2271
      raise errors.ConfigurationError("Some ssconf key(s) have non-string"
2272
                                      " values: %s" % err)
2273
    return ssconf_values
2274

    
2275
  @locking.ssynchronized(_config_lock, shared=1)
2276
  def GetSsconfValues(self):
2277
    """Wrapper using lock around _UnlockedGetSsconf().
2278

2279
    """
2280
    return self._UnlockedGetSsconfValues()
2281

    
2282
  @locking.ssynchronized(_config_lock, shared=1)
2283
  def GetVGName(self):
2284
    """Return the volume group name.
2285

2286
    """
2287
    return self._config_data.cluster.volume_group_name
2288

    
2289
  @locking.ssynchronized(_config_lock)
2290
  def SetVGName(self, vg_name):
2291
    """Set the volume group name.
2292

2293
    """
2294
    self._config_data.cluster.volume_group_name = vg_name
2295
    self._config_data.cluster.serial_no += 1
2296
    self._WriteConfig()
2297

    
2298
  @locking.ssynchronized(_config_lock, shared=1)
2299
  def GetDRBDHelper(self):
2300
    """Return DRBD usermode helper.
2301

2302
    """
2303
    return self._config_data.cluster.drbd_usermode_helper
2304

    
2305
  @locking.ssynchronized(_config_lock)
2306
  def SetDRBDHelper(self, drbd_helper):
2307
    """Set DRBD usermode helper.
2308

2309
    """
2310
    self._config_data.cluster.drbd_usermode_helper = drbd_helper
2311
    self._config_data.cluster.serial_no += 1
2312
    self._WriteConfig()
2313

    
2314
  @locking.ssynchronized(_config_lock, shared=1)
2315
  def GetMACPrefix(self):
2316
    """Return the mac prefix.
2317

2318
    """
2319
    return self._config_data.cluster.mac_prefix
2320

    
2321
  @locking.ssynchronized(_config_lock, shared=1)
2322
  def GetClusterInfo(self):
2323
    """Returns information about the cluster
2324

2325
    @rtype: L{objects.Cluster}
2326
    @return: the cluster object
2327

2328
    """
2329
    return self._config_data.cluster
2330

    
2331
  @locking.ssynchronized(_config_lock, shared=1)
2332
  def HasAnyDiskOfType(self, dev_type):
2333
    """Check if in there is at disk of the given type in the configuration.
2334

2335
    """
2336
    return self._config_data.HasAnyDiskOfType(dev_type)
2337

    
2338
  @locking.ssynchronized(_config_lock)
2339
  def Update(self, target, feedback_fn, ec_id=None):
2340
    """Notify function to be called after updates.
2341

2342
    This function must be called when an object (as returned by
2343
    GetInstanceInfo, GetNodeInfo, GetCluster) has been updated and the
2344
    caller wants the modifications saved to the backing store. Note
2345
    that all modified objects will be saved, but the target argument
2346
    is the one the caller wants to ensure that it's saved.
2347

2348
    @param target: an instance of either L{objects.Cluster},
2349
        L{objects.Node} or L{objects.Instance} which is existing in
2350
        the cluster
2351
    @param feedback_fn: Callable feedback function
2352

2353
    """
2354
    if self._config_data is None:
2355
      raise errors.ProgrammerError("Configuration file not read,"
2356
                                   " cannot save.")
2357
    update_serial = False
2358
    if isinstance(target, objects.Cluster):
2359
      test = target == self._config_data.cluster
2360
    elif isinstance(target, objects.Node):
2361
      test = target in self._config_data.nodes.values()
2362
      update_serial = True
2363
    elif isinstance(target, objects.Instance):
2364
      test = target in self._config_data.instances.values()
2365
    elif isinstance(target, objects.NodeGroup):
2366
      test = target in self._config_data.nodegroups.values()
2367
    elif isinstance(target, objects.Network):
2368
      test = target in self._config_data.networks.values()
2369
    else:
2370
      raise errors.ProgrammerError("Invalid object type (%s) passed to"
2371
                                   " ConfigWriter.Update" % type(target))
2372
    if not test:
2373
      raise errors.ConfigurationError("Configuration updated since object"
2374
                                      " has been read or unknown object")
2375
    target.serial_no += 1
2376
    target.mtime = now = time.time()
2377

    
2378
    if update_serial:
2379
      # for node updates, we need to increase the cluster serial too
2380
      self._config_data.cluster.serial_no += 1
2381
      self._config_data.cluster.mtime = now
2382

    
2383
    if isinstance(target, objects.Instance):
2384
      self._UnlockedReleaseDRBDMinors(target.name)
2385

    
2386
    if ec_id is not None:
2387
      # Commit all ips reserved by OpInstanceSetParams and OpGroupSetParams
2388
      self._UnlockedCommitTemporaryIps(ec_id)
2389

    
2390
    self._WriteConfig(feedback_fn=feedback_fn)
2391

    
2392
  @locking.ssynchronized(_config_lock)
2393
  def DropECReservations(self, ec_id):
2394
    """Drop per-execution-context reservations
2395

2396
    """
2397
    for rm in self._all_rms:
2398
      rm.DropECReservations(ec_id)
2399

    
2400
  @locking.ssynchronized(_config_lock, shared=1)
2401
  def GetAllNetworksInfo(self):
2402
    """Get configuration info of all the networks.
2403

2404
    """
2405
    return dict(self._config_data.networks)
2406

    
2407
  def _UnlockedGetNetworkList(self):
2408
    """Get the list of networks.
2409

2410
    This function is for internal use, when the config lock is already held.
2411

2412
    """
2413
    return self._config_data.networks.keys()
2414

    
2415
  @locking.ssynchronized(_config_lock, shared=1)
2416
  def GetNetworkList(self):
2417
    """Get the list of networks.
2418

2419
    @return: array of networks, ex. ["main", "vlan100", "200]
2420

2421
    """
2422
    return self._UnlockedGetNetworkList()
2423

    
2424
  @locking.ssynchronized(_config_lock, shared=1)
2425
  def GetNetworkNames(self):
2426
    """Get a list of network names
2427

2428
    """
2429
    names = [net.name
2430
             for net in self._config_data.networks.values()]
2431
    return names
2432

    
2433
  def _UnlockedGetNetwork(self, uuid):
2434
    """Returns information about a network.
2435

2436
    This function is for internal use, when the config lock is already held.
2437

2438
    """
2439
    if uuid not in self._config_data.networks:
2440
      return None
2441

    
2442
    return self._config_data.networks[uuid]
2443

    
2444
  @locking.ssynchronized(_config_lock, shared=1)
2445
  def GetNetwork(self, uuid):
2446
    """Returns information about a network.
2447

2448
    It takes the information from the configuration file.
2449

2450
    @param uuid: UUID of the network
2451

2452
    @rtype: L{objects.Network}
2453
    @return: the network object
2454

2455
    """
2456
    return self._UnlockedGetNetwork(uuid)
2457

    
2458
  @locking.ssynchronized(_config_lock)
2459
  def AddNetwork(self, net, ec_id, check_uuid=True):
2460
    """Add a network to the configuration.
2461

2462
    @type net: L{objects.Network}
2463
    @param net: the Network object to add
2464
    @type ec_id: string
2465
    @param ec_id: unique id for the job to use when creating a missing UUID
2466

2467
    """
2468
    self._UnlockedAddNetwork(net, ec_id, check_uuid)
2469
    self._WriteConfig()
2470

    
2471
  def _UnlockedAddNetwork(self, net, ec_id, check_uuid):
2472
    """Add a network to the configuration.
2473

2474
    """
2475
    logging.info("Adding network %s to configuration", net.name)
2476

    
2477
    if check_uuid:
2478
      self._EnsureUUID(net, ec_id)
2479

    
2480
    existing_uuid = self._UnlockedLookupNetwork(net.name)
2481
    if existing_uuid:
2482
      raise errors.OpPrereqError("Desired network name '%s' already"
2483
                                 " exists as a network (UUID: %s)" %
2484
                                 (net.name, existing_uuid),
2485
                                 errors.ECODE_EXISTS)
2486
    net.serial_no = 1
2487
    self._config_data.networks[net.uuid] = net
2488
    self._config_data.cluster.serial_no += 1
2489

    
2490
  def _UnlockedLookupNetwork(self, target):
2491
    """Lookup a network's UUID.
2492

2493
    @type target: string
2494
    @param target: network name or UUID
2495
    @rtype: string
2496
    @return: network UUID
2497
    @raises errors.OpPrereqError: when the target network cannot be found
2498

2499
    """
2500
    if target in self._config_data.networks:
2501
      return target
2502
    for net in self._config_data.networks.values():
2503
      if net.name == target:
2504
        return net.uuid
2505
    return None
2506

    
2507
  @locking.ssynchronized(_config_lock, shared=1)
2508
  def LookupNetwork(self, target):
2509
    """Lookup a network's UUID.
2510

2511
    This function is just a wrapper over L{_UnlockedLookupNetwork}.
2512

2513
    @type target: string
2514
    @param target: network name or UUID
2515
    @rtype: string
2516
    @return: network UUID
2517

2518
    """
2519
    return self._UnlockedLookupNetwork(target)
2520

    
2521
  @locking.ssynchronized(_config_lock)
2522
  def RemoveNetwork(self, network_uuid):
2523
    """Remove a network from the configuration.
2524

2525
    @type network_uuid: string
2526
    @param network_uuid: the UUID of the network to remove
2527

2528
    """
2529
    logging.info("Removing network %s from configuration", network_uuid)
2530

    
2531
    if network_uuid not in self._config_data.networks:
2532
      raise errors.ConfigurationError("Unknown network '%s'" % network_uuid)
2533

    
2534
    del self._config_data.networks[network_uuid]
2535
    self._config_data.cluster.serial_no += 1
2536
    self._WriteConfig()
2537

    
2538
  def _UnlockedGetGroupNetParams(self, net, node):
2539
    """Get the netparams (mode, link) of a network.
2540

2541
    Get a network's netparams for a given node.
2542

2543
    @type net: string
2544
    @param net: network name
2545
    @type node: string
2546
    @param node: node name
2547
    @rtype: dict or None
2548
    @return: netparams
2549

2550
    """
2551
    net_uuid = self._UnlockedLookupNetwork(net)
2552
    if net_uuid is None:
2553
      return None
2554

    
2555
    node_info = self._UnlockedGetNodeInfo(node)
2556
    nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2557
    netparams = nodegroup_info.networks.get(net_uuid, None)
2558

    
2559
    return netparams
2560

    
2561
  @locking.ssynchronized(_config_lock, shared=1)
2562
  def GetGroupNetParams(self, net, node):
2563
    """Locking wrapper of _UnlockedGetGroupNetParams()
2564

2565
    """
2566
    return self._UnlockedGetGroupNetParams(net, node)
2567

    
2568
  @locking.ssynchronized(_config_lock, shared=1)
2569
  def CheckIPInNodeGroup(self, ip, node):
2570
    """Check IP uniqueness in nodegroup.
2571

2572
    Check networks that are connected in the node's node group
2573
    if ip is contained in any of them. Used when creating/adding
2574
    a NIC to ensure uniqueness among nodegroups.
2575

2576
    @type ip: string
2577
    @param ip: ip address
2578
    @type node: string
2579
    @param node: node name
2580
    @rtype: (string, dict) or (None, None)
2581
    @return: (network name, netparams)
2582

2583
    """
2584
    if ip is None:
2585
      return (None, None)
2586
    node_info = self._UnlockedGetNodeInfo(node)
2587
    nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2588
    for net_uuid in nodegroup_info.networks.keys():
2589
      net_info = self._UnlockedGetNetwork(net_uuid)
2590
      pool = network.AddressPool(net_info)
2591
      if pool.Contains(ip):
2592
        return (net_info.name, nodegroup_info.networks[net_uuid])
2593

    
2594
    return (None, None)