Statistics
| Branch: | Tag: | Revision:

root / lib / config.py @ c54784d9

History | View | Annotate | Download (23.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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
import os
35
import tempfile
36
import random
37

    
38
from ganeti import errors
39
from ganeti import locking
40
from ganeti import logger
41
from ganeti import utils
42
from ganeti import constants
43
from ganeti import rpc
44
from ganeti import objects
45
from ganeti import serializer
46
from ganeti import ssconf
47

    
48

    
49
_config_lock = locking.SharedLock()
50

    
51

    
52
def ValidateConfig():
53
  sstore = ssconf.SimpleStore()
54

    
55
  if sstore.GetConfigVersion() != constants.CONFIG_VERSION:
56
    raise errors.ConfigurationError("Cluster configuration version"
57
                                    " mismatch, got %s instead of %s" %
58
                                    (sstore.GetConfigVersion(),
59
                                     constants.CONFIG_VERSION))
60

    
61

    
62
class ConfigWriter:
63
  """The interface to the cluster configuration.
64

65
  """
66
  def __init__(self, cfg_file=None, offline=False):
67
    self.write_count = 0
68
    self._lock = _config_lock
69
    self._config_data = None
70
    self._config_time = None
71
    self._config_size = None
72
    self._config_inode = None
73
    self._offline = offline
74
    if cfg_file is None:
75
      self._cfg_file = constants.CLUSTER_CONF_FILE
76
    else:
77
      self._cfg_file = cfg_file
78
    self._temporary_ids = set()
79
    # Note: in order to prevent errors when resolving our name in
80
    # _DistributeConfig, we compute it here once and reuse it; it's
81
    # better to raise an error before starting to modify the config
82
    # file than after it was modified
83
    self._my_hostname = utils.HostInfo().name
84

    
85
  # this method needs to be static, so that we can call it on the class
86
  @staticmethod
87
  def IsCluster():
88
    """Check if the cluster is configured.
89

90
    """
91
    return os.path.exists(constants.CLUSTER_CONF_FILE)
92

    
93
  @locking.ssynchronized(_config_lock, shared=1)
94
  def GenerateMAC(self):
95
    """Generate a MAC for an instance.
96

97
    This should check the current instances for duplicates.
98

99
    """
100
    self._OpenConfig()
101
    prefix = self._config_data.cluster.mac_prefix
102
    all_macs = self._AllMACs()
103
    retries = 64
104
    while retries > 0:
105
      byte1 = random.randrange(0, 256)
106
      byte2 = random.randrange(0, 256)
107
      byte3 = random.randrange(0, 256)
108
      mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
109
      if mac not in all_macs:
110
        break
111
      retries -= 1
112
    else:
113
      raise errors.ConfigurationError("Can't generate unique MAC")
114
    return mac
115

    
116
  @locking.ssynchronized(_config_lock, shared=1)
117
  def IsMacInUse(self, mac):
118
    """Predicate: check if the specified MAC is in use in the Ganeti cluster.
119

120
    This only checks instances managed by this cluster, it does not
121
    check for potential collisions elsewhere.
122

123
    """
124
    self._OpenConfig()
125
    all_macs = self._AllMACs()
126
    return mac in all_macs
127

    
128
  def _ComputeAllLVs(self):
129
    """Compute the list of all LVs.
130

131
    """
132
    self._OpenConfig()
133
    lvnames = set()
134
    for instance in self._config_data.instances.values():
135
      node_data = instance.MapLVsByNode()
136
      for lv_list in node_data.values():
137
        lvnames.update(lv_list)
138
    return lvnames
139

    
140
  @locking.ssynchronized(_config_lock, shared=1)
141
  def GenerateUniqueID(self, exceptions=None):
142
    """Generate an unique disk name.
143

144
    This checks the current node, instances and disk names for
145
    duplicates.
146

147
    Args:
148
      - exceptions: a list with some other names which should be checked
149
                    for uniqueness (used for example when you want to get
150
                    more than one id at one time without adding each one in
151
                    turn to the config file
152

153
    Returns: the unique id as a string
154

155
    """
156
    existing = set()
157
    existing.update(self._temporary_ids)
158
    existing.update(self._ComputeAllLVs())
159
    existing.update(self._config_data.instances.keys())
160
    existing.update(self._config_data.nodes.keys())
161
    if exceptions is not None:
162
      existing.update(exceptions)
163
    retries = 64
164
    while retries > 0:
165
      unique_id = utils.NewUUID()
166
      if unique_id not in existing and unique_id is not None:
167
        break
168
    else:
169
      raise errors.ConfigurationError("Not able generate an unique ID"
170
                                      " (last tried ID: %s" % unique_id)
171
    self._temporary_ids.add(unique_id)
172
    return unique_id
173

    
174
  def _AllMACs(self):
175
    """Return all MACs present in the config.
176

177
    """
178
    self._OpenConfig()
179

    
180
    result = []
181
    for instance in self._config_data.instances.values():
182
      for nic in instance.nics:
183
        result.append(nic.mac)
184

    
185
    return result
186

    
187
  @locking.ssynchronized(_config_lock, shared=1)
188
  def VerifyConfig(self):
189
    """Stub verify function.
190
    """
191
    self._OpenConfig()
192

    
193
    result = []
194
    seen_macs = []
195
    data = self._config_data
196
    for instance_name in data.instances:
197
      instance = data.instances[instance_name]
198
      if instance.primary_node not in data.nodes:
199
        result.append("instance '%s' has invalid primary node '%s'" %
200
                      (instance_name, instance.primary_node))
201
      for snode in instance.secondary_nodes:
202
        if snode not in data.nodes:
203
          result.append("instance '%s' has invalid secondary node '%s'" %
204
                        (instance_name, snode))
205
      for idx, nic in enumerate(instance.nics):
206
        if nic.mac in seen_macs:
207
          result.append("instance '%s' has NIC %d mac %s duplicate" %
208
                        (instance_name, idx, nic.mac))
209
        else:
210
          seen_macs.append(nic.mac)
211
    return result
212

    
213
  def _UnlockedSetDiskID(self, disk, node_name):
214
    """Convert the unique ID to the ID needed on the target nodes.
215

216
    This is used only for drbd, which needs ip/port configuration.
217

218
    The routine descends down and updates its children also, because
219
    this helps when the only the top device is passed to the remote
220
    node.
221

222
    This function is for internal use, when the config lock is already held.
223

224
    """
225
    if disk.children:
226
      for child in disk.children:
227
        self._UnlockedSetDiskID(child, node_name)
228

    
229
    if disk.logical_id is None and disk.physical_id is not None:
230
      return
231
    if disk.dev_type in constants.LDS_DRBD:
232
      pnode, snode, port = disk.logical_id
233
      if node_name not in (pnode, snode):
234
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
235
                                        node_name)
236
      pnode_info = self._UnlockedGetNodeInfo(pnode)
237
      snode_info = self._UnlockedGetNodeInfo(snode)
238
      if pnode_info is None or snode_info is None:
239
        raise errors.ConfigurationError("Can't find primary or secondary node"
240
                                        " for %s" % str(disk))
241
      if pnode == node_name:
242
        disk.physical_id = (pnode_info.secondary_ip, port,
243
                            snode_info.secondary_ip, port)
244
      else: # it must be secondary, we tested above
245
        disk.physical_id = (snode_info.secondary_ip, port,
246
                            pnode_info.secondary_ip, port)
247
    else:
248
      disk.physical_id = disk.logical_id
249
    return
250

    
251
  @locking.ssynchronized(_config_lock)
252
  def SetDiskID(self, disk, node_name):
253
    """Convert the unique ID to the ID needed on the target nodes.
254

255
    This is used only for drbd, which needs ip/port configuration.
256

257
    The routine descends down and updates its children also, because
258
    this helps when the only the top device is passed to the remote
259
    node.
260

261
    """
262
    return self._UnlockedSetDiskID(disk, node_name)
263

    
264
  @locking.ssynchronized(_config_lock)
265
  def AddTcpUdpPort(self, port):
266
    """Adds a new port to the available port pool.
267

268
    """
269
    if not isinstance(port, int):
270
      raise errors.ProgrammerError("Invalid type passed for port")
271

    
272
    self._OpenConfig()
273
    self._config_data.cluster.tcpudp_port_pool.add(port)
274
    self._WriteConfig()
275

    
276
  @locking.ssynchronized(_config_lock, shared=1)
277
  def GetPortList(self):
278
    """Returns a copy of the current port list.
279

280
    """
281
    self._OpenConfig()
282
    return self._config_data.cluster.tcpudp_port_pool.copy()
283

    
284
  @locking.ssynchronized(_config_lock)
285
  def AllocatePort(self):
286
    """Allocate a port.
287

288
    The port will be taken from the available port pool or from the
289
    default port range (and in this case we increase
290
    highest_used_port).
291

292
    """
293
    self._OpenConfig()
294

    
295
    # If there are TCP/IP ports configured, we use them first.
296
    if self._config_data.cluster.tcpudp_port_pool:
297
      port = self._config_data.cluster.tcpudp_port_pool.pop()
298
    else:
299
      port = self._config_data.cluster.highest_used_port + 1
300
      if port >= constants.LAST_DRBD_PORT:
301
        raise errors.ConfigurationError("The highest used port is greater"
302
                                        " than %s. Aborting." %
303
                                        constants.LAST_DRBD_PORT)
304
      self._config_data.cluster.highest_used_port = port
305

    
306
    self._WriteConfig()
307
    return port
308

    
309
  @locking.ssynchronized(_config_lock, shared=1)
310
  def GetHostKey(self):
311
    """Return the rsa hostkey from the config.
312

313
    Args: None
314

315
    Returns: rsa hostkey
316
    """
317
    self._OpenConfig()
318
    return self._config_data.cluster.rsahostkeypub
319

    
320
  @locking.ssynchronized(_config_lock)
321
  def AddInstance(self, instance):
322
    """Add an instance to the config.
323

324
    This should be used after creating a new instance.
325

326
    Args:
327
      instance: the instance object
328
    """
329
    if not isinstance(instance, objects.Instance):
330
      raise errors.ProgrammerError("Invalid type passed to AddInstance")
331

    
332
    if instance.disk_template != constants.DT_DISKLESS:
333
      all_lvs = instance.MapLVsByNode()
334
      logger.Info("Instance '%s' DISK_LAYOUT: %s" % (instance.name, all_lvs))
335

    
336
    self._OpenConfig()
337
    self._config_data.instances[instance.name] = instance
338
    self._WriteConfig()
339

    
340
  def _SetInstanceStatus(self, instance_name, status):
341
    """Set the instance's status to a given value.
342

343
    """
344
    if status not in ("up", "down"):
345
      raise errors.ProgrammerError("Invalid status '%s' passed to"
346
                                   " ConfigWriter._SetInstanceStatus()" %
347
                                   status)
348
    self._OpenConfig()
349

    
350
    if instance_name not in self._config_data.instances:
351
      raise errors.ConfigurationError("Unknown instance '%s'" %
352
                                      instance_name)
353
    instance = self._config_data.instances[instance_name]
354
    if instance.status != status:
355
      instance.status = status
356
      self._WriteConfig()
357

    
358
  @locking.ssynchronized(_config_lock)
359
  def MarkInstanceUp(self, instance_name):
360
    """Mark the instance status to up in the config.
361

362
    """
363
    self._SetInstanceStatus(instance_name, "up")
364

    
365
  @locking.ssynchronized(_config_lock)
366
  def RemoveInstance(self, instance_name):
367
    """Remove the instance from the configuration.
368

369
    """
370
    self._OpenConfig()
371

    
372
    if instance_name not in self._config_data.instances:
373
      raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
374
    del self._config_data.instances[instance_name]
375
    self._WriteConfig()
376

    
377
  @locking.ssynchronized(_config_lock)
378
  def RenameInstance(self, old_name, new_name):
379
    """Rename an instance.
380

381
    This needs to be done in ConfigWriter and not by RemoveInstance
382
    combined with AddInstance as only we can guarantee an atomic
383
    rename.
384

385
    """
386
    self._OpenConfig()
387
    if old_name not in self._config_data.instances:
388
      raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
389
    inst = self._config_data.instances[old_name]
390
    del self._config_data.instances[old_name]
391
    inst.name = new_name
392

    
393
    for disk in inst.disks:
394
      if disk.dev_type == constants.LD_FILE:
395
        # rename the file paths in logical and physical id
396
        file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1]))
397
        disk.physical_id = disk.logical_id = (disk.logical_id[0],
398
                                              os.path.join(file_storage_dir,
399
                                                           inst.name,
400
                                                           disk.iv_name))
401

    
402
    self._config_data.instances[inst.name] = inst
403
    self._WriteConfig()
404

    
405
  @locking.ssynchronized(_config_lock)
406
  def MarkInstanceDown(self, instance_name):
407
    """Mark the status of an instance to down in the configuration.
408

409
    """
410
    self._SetInstanceStatus(instance_name, "down")
411

    
412
  @locking.ssynchronized(_config_lock, shared=1)
413
  def GetInstanceList(self):
414
    """Get the list of instances.
415

416
    Returns:
417
      array of instances, ex. ['instance2.example.com','instance1.example.com']
418
      these contains all the instances, also the ones in Admin_down state
419

420
    """
421
    self._OpenConfig()
422

    
423
    return self._config_data.instances.keys()
424

    
425
  @locking.ssynchronized(_config_lock, shared=1)
426
  def ExpandInstanceName(self, short_name):
427
    """Attempt to expand an incomplete instance name.
428

429
    """
430
    self._OpenConfig()
431

    
432
    return utils.MatchNameComponent(short_name,
433
                                    self._config_data.instances.keys())
434

    
435
  @locking.ssynchronized(_config_lock, shared=1)
436
  def GetInstanceInfo(self, instance_name):
437
    """Returns informations about an instance.
438

439
    It takes the information from the configuration file. Other informations of
440
    an instance are taken from the live systems.
441

442
    Args:
443
      instance: name of the instance, ex instance1.example.com
444

445
    Returns:
446
      the instance object
447

448
    """
449
    self._OpenConfig()
450

    
451
    if instance_name not in self._config_data.instances:
452
      return None
453

    
454
    return self._config_data.instances[instance_name]
455

    
456
  @locking.ssynchronized(_config_lock)
457
  def AddNode(self, node):
458
    """Add a node to the configuration.
459

460
    Args:
461
      node: an object.Node instance
462

463
    """
464
    self._OpenConfig()
465
    self._config_data.nodes[node.name] = node
466
    self._WriteConfig()
467

    
468
  @locking.ssynchronized(_config_lock)
469
  def RemoveNode(self, node_name):
470
    """Remove a node from the configuration.
471

472
    """
473
    self._OpenConfig()
474
    if node_name not in self._config_data.nodes:
475
      raise errors.ConfigurationError("Unknown node '%s'" % node_name)
476

    
477
    del self._config_data.nodes[node_name]
478
    self._WriteConfig()
479

    
480
  @locking.ssynchronized(_config_lock, shared=1)
481
  def ExpandNodeName(self, short_name):
482
    """Attempt to expand an incomplete instance name.
483

484
    """
485
    self._OpenConfig()
486

    
487
    return utils.MatchNameComponent(short_name,
488
                                    self._config_data.nodes.keys())
489

    
490
  def _UnlockedGetNodeInfo(self, node_name):
491
    """Get the configuration of a node, as stored in the config.
492

493
    This function is for internal use, when the config lock is already held.
494

495
    Args: node: nodename (tuple) of the node
496

497
    Returns: the node object
498

499
    """
500
    self._OpenConfig()
501

    
502
    if node_name not in self._config_data.nodes:
503
      return None
504

    
505
    return self._config_data.nodes[node_name]
506

    
507

    
508
  @locking.ssynchronized(_config_lock, shared=1)
509
  def GetNodeInfo(self, node_name):
510
    """Get the configuration of a node, as stored in the config.
511

512
    Args: node: nodename (tuple) of the node
513

514
    Returns: the node object
515

516
    """
517
    return self._UnlockedGetNodeInfo(node_name)
518

    
519
  def _UnlockedGetNodeList(self):
520
    """Return the list of nodes which are in the configuration.
521

522
    This function is for internal use, when the config lock is already held.
523

524
    """
525
    self._OpenConfig()
526
    return self._config_data.nodes.keys()
527

    
528

    
529
  @locking.ssynchronized(_config_lock, shared=1)
530
  def GetNodeList(self):
531
    """Return the list of nodes which are in the configuration.
532

533
    """
534
    return self._UnlockedGetNodeList()
535

    
536
  @locking.ssynchronized(_config_lock, shared=1)
537
  def DumpConfig(self):
538
    """Return the entire configuration of the cluster.
539
    """
540
    self._OpenConfig()
541
    return self._config_data
542

    
543
  def _BumpSerialNo(self):
544
    """Bump up the serial number of the config.
545

546
    """
547
    self._config_data.cluster.serial_no += 1
548

    
549
  def _OpenConfig(self):
550
    """Read the config data from disk.
551

552
    In case we already have configuration data and the config file has
553
    the same mtime as when we read it, we skip the parsing of the
554
    file, since de-serialisation could be slow.
555

556
    """
557
    try:
558
      st = os.stat(self._cfg_file)
559
    except OSError, err:
560
      raise errors.ConfigurationError("Can't stat config file: %s" % err)
561
    if (self._config_data is not None and
562
        self._config_time is not None and
563
        self._config_time == st.st_mtime and
564
        self._config_size == st.st_size and
565
        self._config_inode == st.st_ino):
566
      # data is current, so skip loading of config file
567
      return
568

    
569
    # Make sure the configuration has the right version
570
    ValidateConfig()
571

    
572
    f = open(self._cfg_file, 'r')
573
    try:
574
      try:
575
        data = objects.ConfigData.FromDict(serializer.Load(f.read()))
576
      except Exception, err:
577
        raise errors.ConfigurationError(err)
578
    finally:
579
      f.close()
580
    if (not hasattr(data, 'cluster') or
581
        not hasattr(data.cluster, 'rsahostkeypub')):
582
      raise errors.ConfigurationError("Incomplete configuration"
583
                                      " (missing cluster.rsahostkeypub)")
584
    self._config_data = data
585
    self._config_time = st.st_mtime
586
    self._config_size = st.st_size
587
    self._config_inode = st.st_ino
588

    
589
  def _DistributeConfig(self):
590
    """Distribute the configuration to the other nodes.
591

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

595
    """
596
    if self._offline:
597
      return True
598
    bad = False
599
    nodelist = self._UnlockedGetNodeList()
600
    myhostname = self._my_hostname
601

    
602
    try:
603
      nodelist.remove(myhostname)
604
    except ValueError:
605
      pass
606

    
607
    result = rpc.call_upload_file(nodelist, self._cfg_file)
608
    for node in nodelist:
609
      if not result[node]:
610
        logger.Error("copy of file %s to node %s failed" %
611
                     (self._cfg_file, node))
612
        bad = True
613
    return not bad
614

    
615
  def _WriteConfig(self, destination=None):
616
    """Write the configuration data to persistent storage.
617

618
    """
619
    if destination is None:
620
      destination = self._cfg_file
621
    self._BumpSerialNo()
622
    txt = serializer.Dump(self._config_data.ToDict())
623
    dir_name, file_name = os.path.split(destination)
624
    fd, name = tempfile.mkstemp('.newconfig', file_name, dir_name)
625
    f = os.fdopen(fd, 'w')
626
    try:
627
      f.write(txt)
628
      os.fsync(f.fileno())
629
    finally:
630
      f.close()
631
    # we don't need to do os.close(fd) as f.close() did it
632
    os.rename(name, destination)
633
    self.write_count += 1
634
    # re-set our cache as not to re-read the config file
635
    try:
636
      st = os.stat(destination)
637
    except OSError, err:
638
      raise errors.ConfigurationError("Can't stat config file: %s" % err)
639
    self._config_time = st.st_mtime
640
    self._config_size = st.st_size
641
    self._config_inode = st.st_ino
642
    # and redistribute the config file
643
    self._DistributeConfig()
644

    
645
  @locking.ssynchronized(_config_lock)
646
  def InitConfig(self, node, primary_ip, secondary_ip,
647
                 hostkeypub, mac_prefix, vg_name, def_bridge):
648
    """Create the initial cluster configuration.
649

650
    It will contain the current node, which will also be the master
651
    node, and no instances or operating systmes.
652

653
    Args:
654
      node: the nodename of the initial node
655
      primary_ip: the IP address of the current host
656
      secondary_ip: the secondary IP of the current host or None
657
      hostkeypub: the public hostkey of this host
658

659
    """
660
    hu_port = constants.FIRST_DRBD_PORT - 1
661
    globalconfig = objects.Cluster(serial_no=1,
662
                                   rsahostkeypub=hostkeypub,
663
                                   highest_used_port=hu_port,
664
                                   mac_prefix=mac_prefix,
665
                                   volume_group_name=vg_name,
666
                                   default_bridge=def_bridge,
667
                                   tcpudp_port_pool=set())
668
    if secondary_ip is None:
669
      secondary_ip = primary_ip
670
    nodeconfig = objects.Node(name=node, primary_ip=primary_ip,
671
                              secondary_ip=secondary_ip)
672

    
673
    self._config_data = objects.ConfigData(nodes={node: nodeconfig},
674
                                           instances={},
675
                                           cluster=globalconfig)
676
    self._WriteConfig()
677

    
678
  @locking.ssynchronized(_config_lock, shared=1)
679
  def GetVGName(self):
680
    """Return the volume group name.
681

682
    """
683
    self._OpenConfig()
684
    return self._config_data.cluster.volume_group_name
685

    
686
  @locking.ssynchronized(_config_lock)
687
  def SetVGName(self, vg_name):
688
    """Set the volume group name.
689

690
    """
691
    self._OpenConfig()
692
    self._config_data.cluster.volume_group_name = vg_name
693
    self._WriteConfig()
694

    
695
  @locking.ssynchronized(_config_lock, shared=1)
696
  def GetDefBridge(self):
697
    """Return the default bridge.
698

699
    """
700
    self._OpenConfig()
701
    return self._config_data.cluster.default_bridge
702

    
703
  @locking.ssynchronized(_config_lock, shared=1)
704
  def GetMACPrefix(self):
705
    """Return the mac prefix.
706

707
    """
708
    self._OpenConfig()
709
    return self._config_data.cluster.mac_prefix
710

    
711
  @locking.ssynchronized(_config_lock, shared=1)
712
  def GetClusterInfo(self):
713
    """Returns informations about the cluster
714

715
    Returns:
716
      the cluster object
717

718
    """
719
    self._OpenConfig()
720

    
721
    return self._config_data.cluster
722

    
723
  @locking.ssynchronized(_config_lock)
724
  def Update(self, target):
725
    """Notify function to be called after updates.
726

727
    This function must be called when an object (as returned by
728
    GetInstanceInfo, GetNodeInfo, GetCluster) has been updated and the
729
    caller wants the modifications saved to the backing store. Note
730
    that all modified objects will be saved, but the target argument
731
    is the one the caller wants to ensure that it's saved.
732

733
    """
734
    if self._config_data is None:
735
      raise errors.ProgrammerError("Configuration file not read,"
736
                                   " cannot save.")
737
    if isinstance(target, objects.Cluster):
738
      test = target == self._config_data.cluster
739
    elif isinstance(target, objects.Node):
740
      test = target in self._config_data.nodes.values()
741
    elif isinstance(target, objects.Instance):
742
      test = target in self._config_data.instances.values()
743
    else:
744
      raise errors.ProgrammerError("Invalid object type (%s) passed to"
745
                                   " ConfigWriter.Update" % type(target))
746
    if not test:
747
      raise errors.ConfigurationError("Configuration updated since object"
748
                                      " has been read or unknown object")
749
    self._WriteConfig()