Statistics
| Branch: | Tag: | Revision:

root / lib / config.py @ e3fd65a4

History | View | Annotate | Download (84.5 kB)

1
#
2
#
3

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

    
21

    
22
"""Configuration management for Ganeti
23

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

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

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

32
"""
33

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

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

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

    
55

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

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

    
61

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

65
  This only verifies the version of the configuration.
66

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

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

    
74

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

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

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

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

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

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

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

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

    
116

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

120
    """
121
    assert callable(generate_one_fn)
122

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

    
136

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

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

    
143

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

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

153
  """
154
  result = []
155

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

    
161
  return result
162

    
163

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

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

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

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

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

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

210
    """
211
    self._context = context
212

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
279
    return prefix
280

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

286
    """
287
    if not prefix:
288
      prefix = self._config_data.cluster.mac_prefix
289
    def GenMac():
290
      byte1 = random.randrange(0, 256)
291
      byte2 = random.randrange(0, 256)
292
      byte3 = random.randrange(0, 256)
293
      mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
294
      return mac
295
    return GenMac
296

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

301
    This should check the current instances for duplicates.
302

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

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

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

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

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

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

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

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

335
    """
336
    nobj = self._UnlockedGetNetwork(net_uuid)
337
    pool = network.AddressPool(nobj)
338
    if action == constants.RESERVE_ACTION:
339
      pool.Reserve(address)
340
    elif action == constants.RELEASE_ACTION:
341
      pool.Release(address)
342

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

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

349
    """
350
    self._temporary_ips.Reserve(ec_id,
351
                                (constants.RELEASE_ACTION, address, net_uuid))
352

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

357
    This is just a wrapper around _UnlockedReleaseIp.
358

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

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

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

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

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

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

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

    
397
    return self._temporary_ips.Reserve(ec_id,
398
                                       (constants.RESERVE_ACTION,
399
                                        address, net_uuid))
400

    
401

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

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

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

415
    @type lv_name: string
416
    @param lv_name: the logical volume name to reserve
417

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

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

429
    This checks the current disks for duplicates.
430

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

    
436
  def _AllLVs(self):
437
    """Compute the list of all LVs.
438

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

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

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

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

    
465
  def _GenerateUniqueID(self, ec_id):
466
    """Generate an unique UUID.
467

468
    This checks the current node, instances and disk names for
469
    duplicates.
470

471
    @rtype: string
472
    @return: the unique id
473

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

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

482
    This is just a wrapper over the unlocked version.
483

484
    @type ec_id: string
485
    @param ec_id: unique id for the job to reserve the id to
486

487
    """
488
    return self._GenerateUniqueID(ec_id)
489

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

493
    @rtype: list
494
    @return: the list of all MACs
495

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

    
502
    return result
503

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

507
    @rtype: list
508
    @return: the list of all DRBD secrets
509

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

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

    
524
    return result
525

    
526
  def _CheckDiskIDs(self, disk, l_ids, p_ids):
527
    """Compute duplicate disk IDs
528

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

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

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

    
556
  def _UnlockedVerifyConfig(self):
557
    """Verify function.
558

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
776
    def _AddIpAddress(ip, name):
777
      ips.setdefault(ip, []).append(name)
778

    
779
    _AddIpAddress(cluster.master_ip, "cluster_ip")
780

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

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

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

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

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

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

    
810
    return result
811

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

816
    This is just a wrapper over L{_UnlockedVerifyConfig}.
817

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

822
    """
823
    return self._UnlockedVerifyConfig()
824

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

828
    This is used only for drbd, which needs ip/port configuration.
829

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

834
    This function is for internal use, when the config lock is already held.
835

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

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

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

867
    This is used only for drbd, which needs ip/port configuration.
868

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

873
    """
874
    return self._UnlockedSetDiskID(disk, node_name)
875

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

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

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

    
888
    self._config_data.cluster.tcpudp_port_pool.add(port)
889

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

894
    """
895
    return self._config_data.cluster.tcpudp_port_pool.copy()
896

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

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

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

    
917
    self._WriteConfig()
918
    return port
919

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

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

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

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

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

963
    This is just a wrapper over L{_UnlockedComputeDRBDMap}.
964

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

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

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

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

985
    @type instance: string
986
    @param instance: the instance for which we allocate minors
987

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

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

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

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

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

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

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

1054
    This function is just a wrapper over L{_UnlockedReleaseDRBDMinors}.
1055

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

1060
    """
1061
    self._UnlockedReleaseDRBDMinors(instance)
1062

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

1067
    @return: Config version
1068

1069
    """
1070
    return self._config_data.version
1071

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

1076
    @return: Cluster name
1077

1078
    """
1079
    return self._config_data.cluster.cluster_name
1080

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

1085
    @return: Master hostname
1086

1087
    """
1088
    return self._config_data.cluster.master_node
1089

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

1094
    @return: Master IP
1095

1096
    """
1097
    return self._config_data.cluster.master_ip
1098

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

1103
    """
1104
    return self._config_data.cluster.master_netdev
1105

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

1110
    """
1111
    return self._config_data.cluster.master_netmask
1112

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

1117
    """
1118
    return self._config_data.cluster.use_external_mip_script
1119

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

1124
    """
1125
    return self._config_data.cluster.file_storage_dir
1126

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

1131
    """
1132
    return self._config_data.cluster.shared_file_storage_dir
1133

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

1138
    """
1139
    return self._config_data.cluster.enabled_hypervisors[0]
1140

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

1145
    @rtype: string
1146
    @return: the rsa hostkey
1147

1148
    """
1149
    return self._config_data.cluster.rsahostkeypub
1150

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

1155
    """
1156
    return self._config_data.cluster.default_iallocator
1157

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

1162
    @return: primary ip family
1163

1164
    """
1165
    return self._config_data.cluster.primary_ip_family
1166

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

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

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

    
1182
    return result
1183

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

1188
    This method calls group.UpgradeConfig() to fill any missing attributes
1189
    according to their default values.
1190

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

1200
    """
1201
    self._UnlockedAddNodeGroup(group, ec_id, check_uuid)
1202
    self._WriteConfig()
1203

    
1204
  def _UnlockedAddNodeGroup(self, group, ec_id, check_uuid):
1205
    """Add a node group to the configuration.
1206

1207
    """
1208
    logging.info("Adding node group %s to configuration", group.name)
1209

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

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

    
1226
    group.serial_no = 1
1227
    group.ctime = group.mtime = time.time()
1228
    group.UpgradeConfig()
1229

    
1230
    self._config_data.nodegroups[group.uuid] = group
1231
    self._config_data.cluster.serial_no += 1
1232

    
1233
  @locking.ssynchronized(_config_lock)
1234
  def RemoveNodeGroup(self, group_uuid):
1235
    """Remove a node group from the configuration.
1236

1237
    @type group_uuid: string
1238
    @param group_uuid: the UUID of the node group to remove
1239

1240
    """
1241
    logging.info("Removing node group %s from configuration", group_uuid)
1242

    
1243
    if group_uuid not in self._config_data.nodegroups:
1244
      raise errors.ConfigurationError("Unknown node group '%s'" % group_uuid)
1245

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

    
1249
    del self._config_data.nodegroups[group_uuid]
1250
    self._config_data.cluster.serial_no += 1
1251
    self._WriteConfig()
1252

    
1253
  def _UnlockedLookupNodeGroup(self, target):
1254
    """Lookup a node group's UUID.
1255

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

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

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

1281
    This function is just a wrapper over L{_UnlockedLookupNodeGroup}.
1282

1283
    @type target: string or None
1284
    @param target: group name or UUID or None to look for the default
1285
    @rtype: string
1286
    @return: nodegroup UUID
1287

1288
    """
1289
    return self._UnlockedLookupNodeGroup(target)
1290

    
1291
  def _UnlockedGetNodeGroup(self, uuid):
1292
    """Lookup a node group.
1293

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

1299
    """
1300
    if uuid not in self._config_data.nodegroups:
1301
      return None
1302

    
1303
    return self._config_data.nodegroups[uuid]
1304

    
1305
  @locking.ssynchronized(_config_lock, shared=1)
1306
  def GetNodeGroup(self, uuid):
1307
    """Lookup a node group.
1308

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

1314
    """
1315
    return self._UnlockedGetNodeGroup(uuid)
1316

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

1321
    """
1322
    return dict(self._config_data.nodegroups)
1323

    
1324
  @locking.ssynchronized(_config_lock, shared=1)
1325
  def GetNodeGroupList(self):
1326
    """Get a list of node groups.
1327

1328
    """
1329
    return self._config_data.nodegroups.keys()
1330

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

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

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

1346
    @param group_uuids: List of node group UUIDs
1347
    @rtype: list
1348
    @return: List of tuples of (group_uuid, group_info)
1349

1350
    """
1351
    return [(uuid, self._UnlockedGetNodeGroup(uuid)) for uuid in group_uuids]
1352

    
1353
  @locking.ssynchronized(_config_lock)
1354
  def AddInstance(self, instance, ec_id):
1355
    """Add an instance to the config.
1356

1357
    This should be used after creating a new instance.
1358

1359
    @type instance: L{objects.Instance}
1360
    @param instance: the instance object
1361

1362
    """
1363
    if not isinstance(instance, objects.Instance):
1364
      raise errors.ProgrammerError("Invalid type passed to AddInstance")
1365

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

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

    
1377
    self._EnsureUUID(instance, ec_id)
1378

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

    
1387
  def _EnsureUUID(self, item, ec_id):
1388
    """Ensures a given object has a valid UUID.
1389

1390
    @param item: the instance or node to be checked
1391
    @param ec_id: the execution context id for the uuid reservation
1392

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

    
1400
  def _SetInstanceStatus(self, instance_name, status):
1401
    """Set the instance's status to a given value.
1402

1403
    """
1404
    assert status in constants.ADMINST_ALL, \
1405
           "Invalid status '%s' passed to SetInstanceStatus" % (status,)
1406

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

    
1417
  @locking.ssynchronized(_config_lock)
1418
  def MarkInstanceUp(self, instance_name):
1419
    """Mark the instance status to up in the config.
1420

1421
    """
1422
    self._SetInstanceStatus(instance_name, constants.ADMINST_UP)
1423

    
1424
  @locking.ssynchronized(_config_lock)
1425
  def MarkInstanceOffline(self, instance_name):
1426
    """Mark the instance status to down in the config.
1427

1428
    """
1429
    self._SetInstanceStatus(instance_name, constants.ADMINST_OFFLINE)
1430

    
1431
  @locking.ssynchronized(_config_lock)
1432
  def RemoveInstance(self, instance_name):
1433
    """Remove the instance from the configuration.
1434

1435
    """
1436
    if instance_name not in self._config_data.instances:
1437
      raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1438

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

    
1446
    instance = self._UnlockedGetInstanceInfo(instance_name)
1447

    
1448
    for nic in instance.nics:
1449
      if nic.network is not None and nic.ip is not None:
1450
        net_uuid = self._UnlockedLookupNetwork(nic.network)
1451
        if net_uuid:
1452
          # Return all IP addresses to the respective address pools
1453
          self._UnlockedCommitIp(constants.RELEASE_ACTION, net_uuid, nic.ip)
1454

    
1455

    
1456
    del self._config_data.instances[instance_name]
1457
    self._config_data.cluster.serial_no += 1
1458
    self._WriteConfig()
1459

    
1460
  @locking.ssynchronized(_config_lock)
1461
  def RenameInstance(self, old_name, new_name):
1462
    """Rename an instance.
1463

1464
    This needs to be done in ConfigWriter and not by RemoveInstance
1465
    combined with AddInstance as only we can guarantee an atomic
1466
    rename.
1467

1468
    """
1469
    if old_name not in self._config_data.instances:
1470
      raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
1471

    
1472
    # Operate on a copy to not loose instance object in case of a failure
1473
    inst = self._config_data.instances[old_name].Copy()
1474
    inst.name = new_name
1475

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

    
1485
    # Actually replace instance object
1486
    del self._config_data.instances[old_name]
1487
    self._config_data.instances[inst.name] = inst
1488

    
1489
    # Force update of ssconf files
1490
    self._config_data.cluster.serial_no += 1
1491

    
1492
    self._WriteConfig()
1493

    
1494
  @locking.ssynchronized(_config_lock)
1495
  def MarkInstanceDown(self, instance_name):
1496
    """Mark the status of an instance to down in the configuration.
1497

1498
    """
1499
    self._SetInstanceStatus(instance_name, constants.ADMINST_DOWN)
1500

    
1501
  def _UnlockedGetInstanceList(self):
1502
    """Get the list of instances.
1503

1504
    This function is for internal use, when the config lock is already held.
1505

1506
    """
1507
    return self._config_data.instances.keys()
1508

    
1509
  @locking.ssynchronized(_config_lock, shared=1)
1510
  def GetInstanceList(self):
1511
    """Get the list of instances.
1512

1513
    @return: array of instances, ex. ['instance2.example.com',
1514
        'instance1.example.com']
1515

1516
    """
1517
    return self._UnlockedGetInstanceList()
1518

    
1519
  def ExpandInstanceName(self, short_name):
1520
    """Attempt to expand an incomplete instance name.
1521

1522
    """
1523
    # Locking is done in L{ConfigWriter.GetInstanceList}
1524
    return _MatchNameComponentIgnoreCase(short_name, self.GetInstanceList())
1525

    
1526
  def _UnlockedGetInstanceInfo(self, instance_name):
1527
    """Returns information about an instance.
1528

1529
    This function is for internal use, when the config lock is already held.
1530

1531
    """
1532
    if instance_name not in self._config_data.instances:
1533
      return None
1534

    
1535
    return self._config_data.instances[instance_name]
1536

    
1537
  @locking.ssynchronized(_config_lock, shared=1)
1538
  def GetInstanceInfo(self, instance_name):
1539
    """Returns information about an instance.
1540

1541
    It takes the information from the configuration file. Other information of
1542
    an instance are taken from the live systems.
1543

1544
    @param instance_name: name of the instance, e.g.
1545
        I{instance1.example.com}
1546

1547
    @rtype: L{objects.Instance}
1548
    @return: the instance object
1549

1550
    """
1551
    return self._UnlockedGetInstanceInfo(instance_name)
1552

    
1553
  @locking.ssynchronized(_config_lock, shared=1)
1554
  def GetInstanceNodeGroups(self, instance_name, primary_only=False):
1555
    """Returns set of node group UUIDs for instance's nodes.
1556

1557
    @rtype: frozenset
1558

1559
    """
1560
    instance = self._UnlockedGetInstanceInfo(instance_name)
1561
    if not instance:
1562
      raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1563

    
1564
    if primary_only:
1565
      nodes = [instance.primary_node]
1566
    else:
1567
      nodes = instance.all_nodes
1568

    
1569
    return frozenset(self._UnlockedGetNodeInfo(node_name).group
1570
                     for node_name in nodes)
1571

    
1572
  @locking.ssynchronized(_config_lock, shared=1)
1573
  def GetMultiInstanceInfo(self, instances):
1574
    """Get the configuration of multiple instances.
1575

1576
    @param instances: list of instance names
1577
    @rtype: list
1578
    @return: list of tuples (instance, instance_info), where
1579
        instance_info is what would GetInstanceInfo return for the
1580
        node, while keeping the original order
1581

1582
    """
1583
    return [(name, self._UnlockedGetInstanceInfo(name)) for name in instances]
1584

    
1585
  @locking.ssynchronized(_config_lock, shared=1)
1586
  def GetAllInstancesInfo(self):
1587
    """Get the configuration of all instances.
1588

1589
    @rtype: dict
1590
    @return: dict of (instance, instance_info), where instance_info is what
1591
              would GetInstanceInfo return for the node
1592

1593
    """
1594
    my_dict = dict([(instance, self._UnlockedGetInstanceInfo(instance))
1595
                    for instance in self._UnlockedGetInstanceList()])
1596
    return my_dict
1597

    
1598
  @locking.ssynchronized(_config_lock, shared=1)
1599
  def GetInstancesInfoByFilter(self, filter_fn):
1600
    """Get instance configuration with a filter.
1601

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

1609
    """
1610
    return dict((name, inst)
1611
                for (name, inst) in self._config_data.instances.items()
1612
                if filter_fn(inst))
1613

    
1614
  @locking.ssynchronized(_config_lock)
1615
  def AddNode(self, node, ec_id):
1616
    """Add a node to the configuration.
1617

1618
    @type node: L{objects.Node}
1619
    @param node: a Node instance
1620

1621
    """
1622
    logging.info("Adding node %s to configuration", node.name)
1623

    
1624
    self._EnsureUUID(node, ec_id)
1625

    
1626
    node.serial_no = 1
1627
    node.ctime = node.mtime = time.time()
1628
    self._UnlockedAddNodeToGroup(node.name, node.group)
1629
    self._config_data.nodes[node.name] = node
1630
    self._config_data.cluster.serial_no += 1
1631
    self._WriteConfig()
1632

    
1633
  @locking.ssynchronized(_config_lock)
1634
  def RemoveNode(self, node_name):
1635
    """Remove a node from the configuration.
1636

1637
    """
1638
    logging.info("Removing node %s from configuration", node_name)
1639

    
1640
    if node_name not in self._config_data.nodes:
1641
      raise errors.ConfigurationError("Unknown node '%s'" % node_name)
1642

    
1643
    self._UnlockedRemoveNodeFromGroup(self._config_data.nodes[node_name])
1644
    del self._config_data.nodes[node_name]
1645
    self._config_data.cluster.serial_no += 1
1646
    self._WriteConfig()
1647

    
1648
  def ExpandNodeName(self, short_name):
1649
    """Attempt to expand an incomplete node name.
1650

1651
    """
1652
    # Locking is done in L{ConfigWriter.GetNodeList}
1653
    return _MatchNameComponentIgnoreCase(short_name, self.GetNodeList())
1654

    
1655
  def _UnlockedGetNodeInfo(self, node_name):
1656
    """Get the configuration of a node, as stored in the config.
1657

1658
    This function is for internal use, when the config lock is already
1659
    held.
1660

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

1663
    @rtype: L{objects.Node}
1664
    @return: the node object
1665

1666
    """
1667
    if node_name not in self._config_data.nodes:
1668
      return None
1669

    
1670
    return self._config_data.nodes[node_name]
1671

    
1672
  @locking.ssynchronized(_config_lock, shared=1)
1673
  def GetNodeInfo(self, node_name):
1674
    """Get the configuration of a node, as stored in the config.
1675

1676
    This is just a locked wrapper over L{_UnlockedGetNodeInfo}.
1677

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

1680
    @rtype: L{objects.Node}
1681
    @return: the node object
1682

1683
    """
1684
    return self._UnlockedGetNodeInfo(node_name)
1685

    
1686
  @locking.ssynchronized(_config_lock, shared=1)
1687
  def GetNodeInstances(self, node_name):
1688
    """Get the instances of a node, as stored in the config.
1689

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

1692
    @rtype: (list, list)
1693
    @return: a tuple with two lists: the primary and the secondary instances
1694

1695
    """
1696
    pri = []
1697
    sec = []
1698
    for inst in self._config_data.instances.values():
1699
      if inst.primary_node == node_name:
1700
        pri.append(inst.name)
1701
      if node_name in inst.secondary_nodes:
1702
        sec.append(inst.name)
1703
    return (pri, sec)
1704

    
1705
  @locking.ssynchronized(_config_lock, shared=1)
1706
  def GetNodeGroupInstances(self, uuid, primary_only=False):
1707
    """Get the instances of a node group.
1708

1709
    @param uuid: Node group UUID
1710
    @param primary_only: Whether to only consider primary nodes
1711
    @rtype: frozenset
1712
    @return: List of instance names in node group
1713

1714
    """
1715
    if primary_only:
1716
      nodes_fn = lambda inst: [inst.primary_node]
1717
    else:
1718
      nodes_fn = lambda inst: inst.all_nodes
1719

    
1720
    return frozenset(inst.name
1721
                     for inst in self._config_data.instances.values()
1722
                     for node_name in nodes_fn(inst)
1723
                     if self._UnlockedGetNodeInfo(node_name).group == uuid)
1724

    
1725
  def _UnlockedGetNodeList(self):
1726
    """Return the list of nodes which are in the configuration.
1727

1728
    This function is for internal use, when the config lock is already
1729
    held.
1730

1731
    @rtype: list
1732

1733
    """
1734
    return self._config_data.nodes.keys()
1735

    
1736
  @locking.ssynchronized(_config_lock, shared=1)
1737
  def GetNodeList(self):
1738
    """Return the list of nodes which are in the configuration.
1739

1740
    """
1741
    return self._UnlockedGetNodeList()
1742

    
1743
  def _UnlockedGetOnlineNodeList(self):
1744
    """Return the list of nodes which are online.
1745

1746
    """
1747
    all_nodes = [self._UnlockedGetNodeInfo(node)
1748
                 for node in self._UnlockedGetNodeList()]
1749
    return [node.name for node in all_nodes if not node.offline]
1750

    
1751
  @locking.ssynchronized(_config_lock, shared=1)
1752
  def GetOnlineNodeList(self):
1753
    """Return the list of nodes which are online.
1754

1755
    """
1756
    return self._UnlockedGetOnlineNodeList()
1757

    
1758
  @locking.ssynchronized(_config_lock, shared=1)
1759
  def GetVmCapableNodeList(self):
1760
    """Return the list of nodes which are not vm capable.
1761

1762
    """
1763
    all_nodes = [self._UnlockedGetNodeInfo(node)
1764
                 for node in self._UnlockedGetNodeList()]
1765
    return [node.name for node in all_nodes if node.vm_capable]
1766

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

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

    
1776
  @locking.ssynchronized(_config_lock, shared=1)
1777
  def GetMultiNodeInfo(self, nodes):
1778
    """Get the configuration of multiple nodes.
1779

1780
    @param nodes: list of node names
1781
    @rtype: list
1782
    @return: list of tuples of (node, node_info), where node_info is
1783
        what would GetNodeInfo return for the node, in the original
1784
        order
1785

1786
    """
1787
    return [(name, self._UnlockedGetNodeInfo(name)) for name in nodes]
1788

    
1789
  @locking.ssynchronized(_config_lock, shared=1)
1790
  def GetAllNodesInfo(self):
1791
    """Get the configuration of all nodes.
1792

1793
    @rtype: dict
1794
    @return: dict of (node, node_info), where node_info is what
1795
              would GetNodeInfo return for the node
1796

1797
    """
1798
    return self._UnlockedGetAllNodesInfo()
1799

    
1800
  def _UnlockedGetAllNodesInfo(self):
1801
    """Gets configuration of all nodes.
1802

1803
    @note: See L{GetAllNodesInfo}
1804

1805
    """
1806
    return dict([(node, self._UnlockedGetNodeInfo(node))
1807
                 for node in self._UnlockedGetNodeList()])
1808

    
1809
  @locking.ssynchronized(_config_lock, shared=1)
1810
  def GetNodeGroupsFromNodes(self, nodes):
1811
    """Returns groups for a list of nodes.
1812

1813
    @type nodes: list of string
1814
    @param nodes: List of node names
1815
    @rtype: frozenset
1816

1817
    """
1818
    return frozenset(self._UnlockedGetNodeInfo(name).group for name in nodes)
1819

    
1820
  def _UnlockedGetMasterCandidateStats(self, exceptions=None):
1821
    """Get the number of current and maximum desired and possible candidates.
1822

1823
    @type exceptions: list
1824
    @param exceptions: if passed, list of nodes that should be ignored
1825
    @rtype: tuple
1826
    @return: tuple of (current, desired and possible, possible)
1827

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

    
1840
  @locking.ssynchronized(_config_lock, shared=1)
1841
  def GetMasterCandidateStats(self, exceptions=None):
1842
    """Get the number of current and maximum possible candidates.
1843

1844
    This is just a wrapper over L{_UnlockedGetMasterCandidateStats}.
1845

1846
    @type exceptions: list
1847
    @param exceptions: if passed, list of nodes that should be ignored
1848
    @rtype: tuple
1849
    @return: tuple of (current, max)
1850

1851
    """
1852
    return self._UnlockedGetMasterCandidateStats(exceptions)
1853

    
1854
  @locking.ssynchronized(_config_lock)
1855
  def MaintainCandidatePool(self, exceptions):
1856
    """Try to grow the candidate pool to the desired size.
1857

1858
    @type exceptions: list
1859
    @param exceptions: if passed, list of nodes that should be ignored
1860
    @rtype: list
1861
    @return: list with the adjusted nodes (L{objects.Node} instances)
1862

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

    
1888
    return mod_list
1889

    
1890
  def _UnlockedAddNodeToGroup(self, node_name, nodegroup_uuid):
1891
    """Add a given node to the specified group.
1892

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

    
1903
  def _UnlockedRemoveNodeFromGroup(self, node):
1904
    """Remove a given node from its group.
1905

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

    
1918
  @locking.ssynchronized(_config_lock)
1919
  def AssignGroupNodes(self, mods):
1920
    """Changes the group of a number of nodes.
1921

1922
    @type mods: list of tuples; (node name, new group UUID)
1923
    @param mods: Node membership modifications
1924

1925
    """
1926
    groups = self._config_data.nodegroups
1927
    nodes = self._config_data.nodes
1928

    
1929
    resmod = []
1930

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

    
1938
      if node.group == new_group_uuid:
1939
        # Node is being assigned to its current group
1940
        logging.debug("Node '%s' was assigned to its current group (%s)",
1941
                      node_name, node.group)
1942
        continue
1943

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

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

    
1958
      assert node.name in old_group.members, \
1959
        ("Inconsistent configuration: node '%s' not listed in members for its"
1960
         " old group '%s'" % (node.name, old_group.uuid))
1961
      assert node.name not in new_group.members, \
1962
        ("Inconsistent configuration: node '%s' already listed in members for"
1963
         " its new group '%s'" % (node.name, new_group.uuid))
1964

    
1965
      resmod.append((node, old_group, new_group))
1966

    
1967
    # Apply changes
1968
    for (node, old_group, new_group) in resmod:
1969
      assert node.uuid != new_group.uuid and old_group.uuid != new_group.uuid, \
1970
        "Assigning to current group is not possible"
1971

    
1972
      node.group = new_group.uuid
1973

    
1974
      # Update members of involved groups
1975
      if node.name in old_group.members:
1976
        old_group.members.remove(node.name)
1977
      if node.name not in new_group.members:
1978
        new_group.members.append(node.name)
1979

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

    
1986
    # Force ssconf update
1987
    self._config_data.cluster.serial_no += 1
1988

    
1989
    self._WriteConfig()
1990

    
1991
  def _BumpSerialNo(self):
1992
    """Bump up the serial number of the config.
1993

1994
    """
1995
    self._config_data.serial_no += 1
1996
    self._config_data.mtime = time.time()
1997

    
1998
  def _AllUUIDObjects(self):
1999
    """Returns all objects with uuid attributes.
2000

2001
    """
2002
    return (self._config_data.instances.values() +
2003
            self._config_data.nodes.values() +
2004
            self._config_data.nodegroups.values() +
2005
            [self._config_data.cluster])
2006

    
2007
  def _OpenConfig(self, accept_foreign):
2008
    """Read the config data from disk.
2009

2010
    """
2011
    raw_data = utils.ReadFile(self._cfg_file)
2012

    
2013
    try:
2014
      data = objects.ConfigData.FromDict(serializer.Load(raw_data))
2015
    except Exception, err:
2016
      raise errors.ConfigurationError(err)
2017

    
2018
    # Make sure the configuration has the right version
2019
    _ValidateConfig(data)
2020

    
2021
    if (not hasattr(data, "cluster") or
2022
        not hasattr(data.cluster, "rsahostkeypub")):
2023
      raise errors.ConfigurationError("Incomplete configuration"
2024
                                      " (missing cluster.rsahostkeypub)")
2025

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

    
2033
    # Upgrade configuration if needed
2034
    data.UpgradeConfig()
2035

    
2036
    self._config_data = data
2037
    # reset the last serial as -1 so that the next write will cause
2038
    # ssconf update
2039
    self._last_cluster_serial = -1
2040

    
2041
    # And finally run our (custom) config upgrade sequence
2042
    self._UpgradeConfig()
2043

    
2044
    self._cfg_id = utils.GetFileID(path=self._cfg_file)
2045

    
2046
  def _UpgradeConfig(self):
2047
    """Run upgrade steps that cannot be done purely in the objects.
2048

2049
    This is because some data elements need uniqueness across the
2050
    whole configuration, etc.
2051

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

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

    
2085
  def _DistributeConfig(self, feedback_fn):
2086
    """Distribute the configuration to the other nodes.
2087

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

2091
    """
2092
    if self._offline:
2093
      return True
2094

    
2095
    bad = False
2096

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

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

    
2123
        if feedback_fn:
2124
          feedback_fn(msg)
2125

    
2126
        bad = True
2127

    
2128
    return not bad
2129

    
2130
  def _WriteConfig(self, destination=None, feedback_fn=None):
2131
    """Write the configuration data to persistent storage.
2132

2133
    """
2134
    assert feedback_fn is None or callable(feedback_fn)
2135

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

    
2148
    if destination is None:
2149
      destination = self._cfg_file
2150
    self._BumpSerialNo()
2151
    txt = serializer.Dump(self._config_data.ToDict())
2152

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

    
2166
    self.write_count += 1
2167

    
2168
    # and redistribute the config file to master candidates
2169
    self._DistributeConfig(feedback_fn)
2170

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

    
2178
        for nname, nresu in result.items():
2179
          msg = nresu.fail_msg
2180
          if msg:
2181
            errmsg = ("Error while uploading ssconf files to"
2182
                      " node %s: %s" % (nname, msg))
2183
            logging.warning(errmsg)
2184

    
2185
            if feedback_fn:
2186
              feedback_fn(errmsg)
2187

    
2188
      self._last_cluster_serial = self._config_data.cluster.serial_no
2189

    
2190
  def _UnlockedGetSsconfValues(self):
2191
    """Return the values needed by ssconf.
2192

2193
    @rtype: dict
2194
    @return: a dictionary with keys the ssconf names and values their
2195
        associated value
2196

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

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

    
2217
    cluster = self._config_data.cluster
2218
    cluster_tags = fn(cluster.GetTags())
2219

    
2220
    hypervisor_list = fn(cluster.enabled_hypervisors)
2221

    
2222
    uid_pool = uidpool.FormatUidPool(cluster.uid_pool, separator="\n")
2223

    
2224
    nodegroups = ["%s %s" % (nodegroup.uuid, nodegroup.name) for nodegroup in
2225
                  self._config_data.nodegroups.values()]
2226
    nodegroups_data = fn(utils.NiceSort(nodegroups))
2227
    networks = ["%s %s" % (net.uuid, net.name) for net in
2228
                self._config_data.networks.values()]
2229
    networks_data = fn(utils.NiceSort(networks))
2230

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

    
2264
  @locking.ssynchronized(_config_lock, shared=1)
2265
  def GetSsconfValues(self):
2266
    """Wrapper using lock around _UnlockedGetSsconf().
2267

2268
    """
2269
    return self._UnlockedGetSsconfValues()
2270

    
2271
  @locking.ssynchronized(_config_lock, shared=1)
2272
  def GetVGName(self):
2273
    """Return the volume group name.
2274

2275
    """
2276
    return self._config_data.cluster.volume_group_name
2277

    
2278
  @locking.ssynchronized(_config_lock)
2279
  def SetVGName(self, vg_name):
2280
    """Set the volume group name.
2281

2282
    """
2283
    self._config_data.cluster.volume_group_name = vg_name
2284
    self._config_data.cluster.serial_no += 1
2285
    self._WriteConfig()
2286

    
2287
  @locking.ssynchronized(_config_lock, shared=1)
2288
  def GetDRBDHelper(self):
2289
    """Return DRBD usermode helper.
2290

2291
    """
2292
    return self._config_data.cluster.drbd_usermode_helper
2293

    
2294
  @locking.ssynchronized(_config_lock)
2295
  def SetDRBDHelper(self, drbd_helper):
2296
    """Set DRBD usermode helper.
2297

2298
    """
2299
    self._config_data.cluster.drbd_usermode_helper = drbd_helper
2300
    self._config_data.cluster.serial_no += 1
2301
    self._WriteConfig()
2302

    
2303
  @locking.ssynchronized(_config_lock, shared=1)
2304
  def GetMACPrefix(self):
2305
    """Return the mac prefix.
2306

2307
    """
2308
    return self._config_data.cluster.mac_prefix
2309

    
2310
  @locking.ssynchronized(_config_lock, shared=1)
2311
  def GetClusterInfo(self):
2312
    """Returns information about the cluster
2313

2314
    @rtype: L{objects.Cluster}
2315
    @return: the cluster object
2316

2317
    """
2318
    return self._config_data.cluster
2319

    
2320
  @locking.ssynchronized(_config_lock, shared=1)
2321
  def HasAnyDiskOfType(self, dev_type):
2322
    """Check if in there is at disk of the given type in the configuration.
2323

2324
    """
2325
    return self._config_data.HasAnyDiskOfType(dev_type)
2326

    
2327
  @locking.ssynchronized(_config_lock)
2328
  def Update(self, target, feedback_fn, ec_id=None):
2329
    """Notify function to be called after updates.
2330

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

2337
    @param target: an instance of either L{objects.Cluster},
2338
        L{objects.Node} or L{objects.Instance} which is existing in
2339
        the cluster
2340
    @param feedback_fn: Callable feedback function
2341

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

    
2367
    if update_serial:
2368
      # for node updates, we need to increase the cluster serial too
2369
      self._config_data.cluster.serial_no += 1
2370
      self._config_data.cluster.mtime = now
2371

    
2372
    if isinstance(target, objects.Instance):
2373
      self._UnlockedReleaseDRBDMinors(target.name)
2374

    
2375
    if ec_id is not None:
2376
      # Commit all ips reserved by OpInstanceSetParams and OpGroupSetParams
2377
      self._UnlockedCommitTemporaryIps(ec_id)
2378

    
2379
    self._WriteConfig(feedback_fn=feedback_fn)
2380

    
2381
  @locking.ssynchronized(_config_lock)
2382
  def DropECReservations(self, ec_id):
2383
    """Drop per-execution-context reservations
2384

2385
    """
2386
    for rm in self._all_rms:
2387
      rm.DropECReservations(ec_id)
2388

    
2389
  @locking.ssynchronized(_config_lock, shared=1)
2390
  def GetAllNetworksInfo(self):
2391
    """Get the configuration of all networks
2392

2393
    """
2394
    return dict(self._config_data.networks)
2395

    
2396
  def _UnlockedGetNetworkList(self):
2397
    """Get the list of networks.
2398

2399
    This function is for internal use, when the config lock is already held.
2400

2401
    """
2402
    return self._config_data.networks.keys()
2403

    
2404
  @locking.ssynchronized(_config_lock, shared=1)
2405
  def GetNetworkList(self):
2406
    """Get the list of networks.
2407

2408
    @return: array of networks, ex. ["main", "vlan100", "200]
2409

2410
    """
2411
    return self._UnlockedGetNetworkList()
2412

    
2413
  @locking.ssynchronized(_config_lock, shared=1)
2414
  def GetNetworkNames(self):
2415
    """Get a list of network names
2416

2417
    """
2418
    names = [network.name
2419
             for network in self._config_data.networks.values()]
2420
    return names
2421

    
2422
  def _UnlockedGetNetwork(self, uuid):
2423
    """Returns information about a network.
2424

2425
    This function is for internal use, when the config lock is already held.
2426

2427
    """
2428
    if uuid not in self._config_data.networks:
2429
      return None
2430

    
2431
    return self._config_data.networks[uuid]
2432

    
2433
  @locking.ssynchronized(_config_lock, shared=1)
2434
  def GetNetwork(self, uuid):
2435
    """Returns information about a network.
2436

2437
    It takes the information from the configuration file.
2438

2439
    @param uuid: UUID of the network
2440

2441
    @rtype: L{objects.Network}
2442
    @return: the network object
2443

2444
    """
2445
    return self._UnlockedGetNetwork(uuid)
2446

    
2447
  @locking.ssynchronized(_config_lock)
2448
  def AddNetwork(self, net, ec_id, check_uuid=True):
2449
    """Add a network to the configuration.
2450

2451
    @type net: L{objects.Network}
2452
    @param net: the Network object to add
2453
    @type ec_id: string
2454
    @param ec_id: unique id for the job to use when creating a missing UUID
2455

2456
    """
2457
    self._UnlockedAddNetwork(net, ec_id, check_uuid)
2458
    self._WriteConfig()
2459

    
2460
  def _UnlockedAddNetwork(self, net, ec_id, check_uuid):
2461
    """Add a network to the configuration.
2462

2463
    """
2464
    logging.info("Adding network %s to configuration", net.name)
2465

    
2466
    if check_uuid:
2467
      self._EnsureUUID(net, ec_id)
2468

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

    
2479
  def _UnlockedLookupNetwork(self, target):
2480
    """Lookup a network's UUID.
2481

2482
    @type target: string
2483
    @param target: network name or UUID
2484
    @rtype: string
2485
    @return: network UUID
2486
    @raises errors.OpPrereqError: when the target network cannot be found
2487

2488
    """
2489
    if target in self._config_data.networks:
2490
      return target
2491
    for net in self._config_data.networks.values():
2492
      if net.name == target:
2493
        return net.uuid
2494
    return None
2495

    
2496
  @locking.ssynchronized(_config_lock, shared=1)
2497
  def LookupNetwork(self, target):
2498
    """Lookup a network's UUID.
2499

2500
    This function is just a wrapper over L{_UnlockedLookupNetwork}.
2501

2502
    @type target: string
2503
    @param target: network name or UUID
2504
    @rtype: string
2505
    @return: network UUID
2506

2507
    """
2508
    return self._UnlockedLookupNetwork(target)
2509

    
2510
  @locking.ssynchronized(_config_lock)
2511
  def RemoveNetwork(self, network_uuid):
2512
    """Remove a network from the configuration.
2513

2514
    @type network_uuid: string
2515
    @param network_uuid: the UUID of the network to remove
2516

2517
    """
2518
    logging.info("Removing network %s from configuration", network_uuid)
2519

    
2520
    if network_uuid not in self._config_data.networks:
2521
      raise errors.ConfigurationError("Unknown network '%s'" % network_uuid)
2522

    
2523
    del self._config_data.networks[network_uuid]
2524
    self._config_data.cluster.serial_no += 1
2525
    self._WriteConfig()
2526

    
2527
  def _UnlockedGetGroupNetParams(self, net, node):
2528
    """Get the netparams (mode, link) of a network.
2529

2530
    Get a network's netparams for a given node.
2531

2532
    @type net: string
2533
    @param net: network name
2534
    @type node: string
2535
    @param node: node name
2536
    @rtype: dict or None
2537
    @return: netparams
2538

2539
    """
2540
    net_uuid = self._UnlockedLookupNetwork(net)
2541
    if net_uuid is None:
2542
      return None
2543

    
2544
    node_info = self._UnlockedGetNodeInfo(node)
2545
    nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2546
    netparams = nodegroup_info.networks.get(net_uuid, None)
2547

    
2548
    return netparams
2549

    
2550
  @locking.ssynchronized(_config_lock, shared=1)
2551
  def GetGroupNetParams(self, net, node):
2552
    """Locking wrapper of _UnlockedGetGroupNetParams()
2553

2554
    """
2555
    return self._UnlockedGetGroupNetParams(net, node)
2556

    
2557

    
2558
  @locking.ssynchronized(_config_lock, shared=1)
2559
  def CheckIPInNodeGroup(self, ip, node):
2560
    """Check for conflictig IP.
2561

2562
    @type ip: string
2563
    @param ip: ip address
2564
    @type node: string
2565
    @param node: node name
2566
    @rtype: (string, dict) or (None, None)
2567
    @return: (network name, netparams)
2568

2569
    """
2570
    if ip is None:
2571
      return (None, None)
2572
    node_info = self._UnlockedGetNodeInfo(node)
2573
    nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2574
    for net_uuid in nodegroup_info.networks.keys():
2575
      net_info = self._UnlockedGetNetwork(net_uuid)
2576
      pool = network.AddressPool(net_info)
2577
      if pool._Contains(ip):
2578
        return (net_info.name, nodegroup_info.networks[net_uuid])
2579

    
2580
    return (None, None)