Use network's mac prefix
[ganeti-local] / lib / config.py
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 from functools import wraps
43
44 from ganeti import errors
45 from ganeti import locking
46 from ganeti import utils
47 from ganeti import constants
48 from ganeti import rpc
49 from ganeti import objects
50 from ganeti import serializer
51 from ganeti import uidpool
52 from ganeti import netutils
53 from ganeti import runtime
54 from ganeti import pathutils
55 from ganeti import network
56
57
58 _config_lock = locking.SharedLock("ConfigWriter")
59
60 # job id used for resource management at config upgrade time
61 _UPGRADE_CONFIG_JID = "jid-cfg-upgrade"
62
63
64 def _ValidateConfig(data):
65   """Verifies that a configuration objects looks valid.
66
67   This only verifies the version of the configuration.
68
69   @raise errors.ConfigurationError: if the version differs from what
70       we expect
71
72   """
73   if data.version != constants.CONFIG_VERSION:
74     raise errors.ConfigVersionMismatch(constants.CONFIG_VERSION, data.version)
75
76
77 class TemporaryReservationManager:
78   """A temporary resource reservation manager.
79
80   This is used to reserve resources in a job, before using them, making sure
81   other jobs cannot get them in the meantime.
82
83   """
84   def __init__(self):
85     self._ec_reserved = {}
86
87   def Reserved(self, resource):
88     for holder_reserved in self._ec_reserved.values():
89       if resource in holder_reserved:
90         return True
91     return False
92
93   def Reserve(self, ec_id, resource):
94     if self.Reserved(resource):
95       raise errors.ReservationError("Duplicate reservation for resource '%s'"
96                                     % str(resource))
97     if ec_id not in self._ec_reserved:
98       self._ec_reserved[ec_id] = set([resource])
99     else:
100       self._ec_reserved[ec_id].add(resource)
101
102   def DropECReservations(self, ec_id):
103     if ec_id in self._ec_reserved:
104       del self._ec_reserved[ec_id]
105
106   def GetReserved(self):
107     all_reserved = set()
108     for holder_reserved in self._ec_reserved.values():
109       all_reserved.update(holder_reserved)
110     return all_reserved
111
112   def GetECReserved(self, ec_id):
113     ec_reserved = set()
114     if ec_id in self._ec_reserved:
115       ec_reserved.update(self._ec_reserved[ec_id])
116     return ec_reserved
117
118
119   def Generate(self, existing, generate_one_fn, ec_id):
120     """Generate a new resource of this type
121
122     """
123     assert callable(generate_one_fn)
124
125     all_elems = self.GetReserved()
126     all_elems.update(existing)
127     retries = 64
128     while retries > 0:
129       new_resource = generate_one_fn()
130       if new_resource is not None and new_resource not in all_elems:
131         break
132     else:
133       raise errors.ConfigurationError("Not able generate new resource"
134                                       " (last tried: %s)" % new_resource)
135     self.Reserve(ec_id, new_resource)
136     return new_resource
137
138
139 def _MatchNameComponentIgnoreCase(short_name, names):
140   """Wrapper around L{utils.text.MatchNameComponent}.
141
142   """
143   return utils.MatchNameComponent(short_name, names, case_sensitive=False)
144
145
146 def _CheckInstanceDiskIvNames(disks):
147   """Checks if instance's disks' C{iv_name} attributes are in order.
148
149   @type disks: list of L{objects.Disk}
150   @param disks: List of disks
151   @rtype: list of tuples; (int, string, string)
152   @return: List of wrongly named disks, each tuple contains disk index,
153     expected and actual name
154
155   """
156   result = []
157
158   for (idx, disk) in enumerate(disks):
159     exp_iv_name = "disk/%s" % idx
160     if disk.iv_name != exp_iv_name:
161       result.append((idx, exp_iv_name, disk.iv_name))
162
163   return result
164
165
166 class ConfigWriter:
167   """The interface to the cluster configuration.
168
169   @ivar _temporary_lvs: reservation manager for temporary LVs
170   @ivar _all_rms: a list of all temporary reservation managers
171
172   """
173   def __init__(self, cfg_file=None, offline=False, _getents=runtime.GetEnts,
174                accept_foreign=False):
175     self.write_count = 0
176     self._lock = _config_lock
177     self._config_data = None
178     self._offline = offline
179     if cfg_file is None:
180       self._cfg_file = pathutils.CLUSTER_CONF_FILE
181     else:
182       self._cfg_file = cfg_file
183     self._getents = _getents
184     self._temporary_ids = TemporaryReservationManager()
185     self._temporary_drbds = {}
186     self._temporary_macs = TemporaryReservationManager()
187     self._temporary_secrets = TemporaryReservationManager()
188     self._temporary_lvs = TemporaryReservationManager()
189     self._temporary_ips = TemporaryReservationManager()
190     self._all_rms = [self._temporary_ids, self._temporary_macs,
191                      self._temporary_secrets, self._temporary_lvs,
192                      self._temporary_ips]
193     # Note: in order to prevent errors when resolving our name in
194     # _DistributeConfig, we compute it here once and reuse it; it's
195     # better to raise an error before starting to modify the config
196     # file than after it was modified
197     self._my_hostname = netutils.Hostname.GetSysName()
198     self._last_cluster_serial = -1
199     self._cfg_id = None
200     self._context = None
201     self._OpenConfig(accept_foreign)
202
203   def _GetRpc(self, address_list):
204     """Returns RPC runner for configuration.
205
206     """
207     return rpc.ConfigRunner(self._context, address_list)
208
209   def SetContext(self, context):
210     """Sets Ganeti context.
211
212     """
213     self._context = context
214
215   # this method needs to be static, so that we can call it on the class
216   @staticmethod
217   def IsCluster():
218     """Check if the cluster is configured.
219
220     """
221     return os.path.exists(pathutils.CLUSTER_CONF_FILE)
222
223   def _GenerateMACPrefix(self, net=None):
224     def _get_mac_prefix(view_func):
225       def _decorator(*args, **kwargs):
226         prefix = self._config_data.cluster.mac_prefix
227         if net:
228           net_uuid = self._UnlockedLookupNetwork(net)
229           if net_uuid:
230             nobj = self._UnlockedGetNetwork(net_uuid)
231             if nobj.mac_prefix:
232               prefix = nobj.mac_prefix
233         suffix = view_func(*args, **kwargs)
234         return prefix+':'+suffix
235       return wraps(view_func)(_decorator)
236     return _get_mac_prefix
237
238   def _GenerateMACSuffix(self):
239     """Generate one mac address
240
241     """
242     byte1 = random.randrange(0, 256)
243     byte2 = random.randrange(0, 256)
244     byte3 = random.randrange(0, 256)
245     suffix = "%02x:%02x:%02x" % (byte1, byte2, byte3)
246     return suffix
247
248   @locking.ssynchronized(_config_lock, shared=1)
249   def GetNdParams(self, node):
250     """Get the node params populated with cluster defaults.
251
252     @type node: L{objects.Node}
253     @param node: The node we want to know the params for
254     @return: A dict with the filled in node params
255
256     """
257     nodegroup = self._UnlockedGetNodeGroup(node.group)
258     return self._config_data.cluster.FillND(node, nodegroup)
259
260   @locking.ssynchronized(_config_lock, shared=1)
261   def GetInstanceDiskParams(self, instance):
262     """Get the disk params populated with inherit chain.
263
264     @type instance: L{objects.Instance}
265     @param instance: The instance we want to know the params for
266     @return: A dict with the filled in disk params
267
268     """
269     node = self._UnlockedGetNodeInfo(instance.primary_node)
270     nodegroup = self._UnlockedGetNodeGroup(node.group)
271     return self._UnlockedGetGroupDiskParams(nodegroup)
272
273   @locking.ssynchronized(_config_lock, shared=1)
274   def GetGroupDiskParams(self, group):
275     """Get the disk params populated with inherit chain.
276
277     @type group: L{objects.NodeGroup}
278     @param group: The group we want to know the params for
279     @return: A dict with the filled in disk params
280
281     """
282     return self._UnlockedGetGroupDiskParams(group)
283
284   def _UnlockedGetGroupDiskParams(self, group):
285     """Get the disk params populated with inherit chain down to node-group.
286
287     @type group: L{objects.NodeGroup}
288     @param group: The group we want to know the params for
289     @return: A dict with the filled in disk params
290
291     """
292     return self._config_data.cluster.SimpleFillDP(group.diskparams)
293
294   @locking.ssynchronized(_config_lock, shared=1)
295   def GenerateMAC(self, net, ec_id):
296     """Generate a MAC for an instance.
297
298     This should check the current instances for duplicates.
299
300     """
301     existing = self._AllMACs()
302     gen_mac = self._GenerateMACPrefix(net)(self._GenerateMACSuffix)
303     return self._temporary_ids.Generate(existing, gen_mac, ec_id)
304
305   @locking.ssynchronized(_config_lock, shared=1)
306   def ReserveMAC(self, mac, ec_id):
307     """Reserve a MAC for an instance.
308
309     This only checks instances managed by this cluster, it does not
310     check for potential collisions elsewhere.
311
312     """
313     all_macs = self._AllMACs()
314     if mac in all_macs:
315       raise errors.ReservationError("mac already in use")
316     else:
317       self._temporary_macs.Reserve(ec_id, mac)
318
319   def _UnlockedCommitTemporaryIps(self, ec_id):
320     """Commit all reserved IP address to their respective pools
321
322     """
323     for action, address, net_uuid in self._temporary_ips.GetECReserved(ec_id):
324       self._UnlockedCommitIp(action, net_uuid, address)
325
326   def _UnlockedCommitIp(self, action, net_uuid, address):
327     """Commit a reserved IP address to an IP pool.
328
329     The IP address is taken from the network's IP pool and marked as reserved.
330
331     """
332     nobj = self._UnlockedGetNetwork(net_uuid)
333     pool = network.AddressPool(nobj)
334     if action == 'reserve':
335       pool.Reserve(address)
336     elif action == 'release':
337       pool.Release(address)
338
339   def _UnlockedReleaseIp(self, net_uuid, address, ec_id):
340     """Give a specific IP address back to an IP pool.
341
342     The IP address is returned to the IP pool designated by pool_id and marked
343     as reserved.
344
345     """
346     nobj = self._UnlockedGetNetwork(net_uuid)
347     pool = network.AddressPool(nobj)
348     self._temporary_ips.Reserve(ec_id, ('release', address, net_uuid))
349
350   @locking.ssynchronized(_config_lock, shared=1)
351   def ReleaseIp(self, network, address, ec_id):
352     """Give a specified IP address back to an IP pool.
353
354     This is just a wrapper around _UnlockedReleaseIp.
355
356     """
357     net_uuid = self._UnlockedLookupNetwork(network)
358     if net_uuid:
359       self._UnlockedReleaseIp(net_uuid, address, ec_id)
360
361   @locking.ssynchronized(_config_lock, shared=1)
362   def GenerateIp(self, net, ec_id):
363     """Find a free IPv4 address for an instance.
364
365     """
366     net_uuid = self._UnlockedLookupNetwork(net)
367     nobj = self._UnlockedGetNetwork(net_uuid)
368     pool = network.AddressPool(nobj)
369     gen_free = pool.GenerateFree()
370
371     def gen_one():
372       try:
373         ip = gen_free()
374       except StopIteration:
375         raise errors.ReservationError("Cannot generate IP. Network is full")
376       return ("reserve", ip, net_uuid)
377
378     _ ,address, _ = self._temporary_ips.Generate([], gen_one, ec_id)
379     return address
380
381   def _UnlockedReserveIp(self, net_uuid, address, ec_id):
382     """Reserve a given IPv4 address for use by an instance.
383
384     """
385     nobj = self._UnlockedGetNetwork(net_uuid)
386     pool = network.AddressPool(nobj)
387     try:
388       isreserved = pool.IsReserved(address)
389     except errors.AddressPoolError:
390       raise errors.ReservationError("IP address not in network")
391     if isreserved:
392       raise errors.ReservationError("IP address already in use")
393
394     return self._temporary_ips.Reserve(ec_id, ('reserve', address, net_uuid))
395
396
397   @locking.ssynchronized(_config_lock, shared=1)
398   def ReserveIp(self, net, address, ec_id):
399     """Reserve a given IPv4 address for use by an instance.
400
401     """
402     net_uuid = self._UnlockedLookupNetwork(net)
403     if net_uuid:
404       return self._UnlockedReserveIp(net_uuid, address, ec_id)
405
406   @locking.ssynchronized(_config_lock, shared=1)
407   def ReserveLV(self, lv_name, ec_id):
408     """Reserve an VG/LV pair for an instance.
409
410     @type lv_name: string
411     @param lv_name: the logical volume name to reserve
412
413     """
414     all_lvs = self._AllLVs()
415     if lv_name in all_lvs:
416       raise errors.ReservationError("LV already in use")
417     else:
418       self._temporary_lvs.Reserve(ec_id, lv_name)
419
420   @locking.ssynchronized(_config_lock, shared=1)
421   def GenerateDRBDSecret(self, ec_id):
422     """Generate a DRBD secret.
423
424     This checks the current disks for duplicates.
425
426     """
427     return self._temporary_secrets.Generate(self._AllDRBDSecrets(),
428                                             utils.GenerateSecret,
429                                             ec_id)
430
431   def _AllLVs(self):
432     """Compute the list of all LVs.
433
434     """
435     lvnames = set()
436     for instance in self._config_data.instances.values():
437       node_data = instance.MapLVsByNode()
438       for lv_list in node_data.values():
439         lvnames.update(lv_list)
440     return lvnames
441
442   def _AllIDs(self, include_temporary):
443     """Compute the list of all UUIDs and names we have.
444
445     @type include_temporary: boolean
446     @param include_temporary: whether to include the _temporary_ids set
447     @rtype: set
448     @return: a set of IDs
449
450     """
451     existing = set()
452     if include_temporary:
453       existing.update(self._temporary_ids.GetReserved())
454     existing.update(self._AllLVs())
455     existing.update(self._config_data.instances.keys())
456     existing.update(self._config_data.nodes.keys())
457     existing.update([i.uuid for i in self._AllUUIDObjects() if i.uuid])
458     return existing
459
460   def _GenerateUniqueID(self, ec_id):
461     """Generate an unique UUID.
462
463     This checks the current node, instances and disk names for
464     duplicates.
465
466     @rtype: string
467     @return: the unique id
468
469     """
470     existing = self._AllIDs(include_temporary=False)
471     return self._temporary_ids.Generate(existing, utils.NewUUID, ec_id)
472
473   @locking.ssynchronized(_config_lock, shared=1)
474   def GenerateUniqueID(self, ec_id):
475     """Generate an unique ID.
476
477     This is just a wrapper over the unlocked version.
478
479     @type ec_id: string
480     @param ec_id: unique id for the job to reserve the id to
481
482     """
483     return self._GenerateUniqueID(ec_id)
484
485   def _AllMACs(self):
486     """Return all MACs present in the config.
487
488     @rtype: list
489     @return: the list of all MACs
490
491     """
492     result = []
493     for instance in self._config_data.instances.values():
494       for nic in instance.nics:
495         result.append(nic.mac)
496
497     return result
498
499   def _AllDRBDSecrets(self):
500     """Return all DRBD secrets present in the config.
501
502     @rtype: list
503     @return: the list of all DRBD secrets
504
505     """
506     def helper(disk, result):
507       """Recursively gather secrets from this disk."""
508       if disk.dev_type == constants.DT_DRBD8:
509         result.append(disk.logical_id[5])
510       if disk.children:
511         for child in disk.children:
512           helper(child, result)
513
514     result = []
515     for instance in self._config_data.instances.values():
516       for disk in instance.disks:
517         helper(disk, result)
518
519     return result
520
521   def _CheckDiskIDs(self, disk, l_ids, p_ids):
522     """Compute duplicate disk IDs
523
524     @type disk: L{objects.Disk}
525     @param disk: the disk at which to start searching
526     @type l_ids: list
527     @param l_ids: list of current logical ids
528     @type p_ids: list
529     @param p_ids: list of current physical ids
530     @rtype: list
531     @return: a list of error messages
532
533     """
534     result = []
535     if disk.logical_id is not None:
536       if disk.logical_id in l_ids:
537         result.append("duplicate logical id %s" % str(disk.logical_id))
538       else:
539         l_ids.append(disk.logical_id)
540     if disk.physical_id is not None:
541       if disk.physical_id in p_ids:
542         result.append("duplicate physical id %s" % str(disk.physical_id))
543       else:
544         p_ids.append(disk.physical_id)
545
546     if disk.children:
547       for child in disk.children:
548         result.extend(self._CheckDiskIDs(child, l_ids, p_ids))
549     return result
550
551   def _UnlockedVerifyConfig(self):
552     """Verify function.
553
554     @rtype: list
555     @return: a list of error messages; a non-empty list signifies
556         configuration errors
557
558     """
559     # pylint: disable=R0914
560     result = []
561     seen_macs = []
562     ports = {}
563     data = self._config_data
564     cluster = data.cluster
565     seen_lids = []
566     seen_pids = []
567
568     # global cluster checks
569     if not cluster.enabled_hypervisors:
570       result.append("enabled hypervisors list doesn't have any entries")
571     invalid_hvs = set(cluster.enabled_hypervisors) - constants.HYPER_TYPES
572     if invalid_hvs:
573       result.append("enabled hypervisors contains invalid entries: %s" %
574                     invalid_hvs)
575     missing_hvp = (set(cluster.enabled_hypervisors) -
576                    set(cluster.hvparams.keys()))
577     if missing_hvp:
578       result.append("hypervisor parameters missing for the enabled"
579                     " hypervisor(s) %s" % utils.CommaJoin(missing_hvp))
580
581     if cluster.master_node not in data.nodes:
582       result.append("cluster has invalid primary node '%s'" %
583                     cluster.master_node)
584
585     def _helper(owner, attr, value, template):
586       try:
587         utils.ForceDictType(value, template)
588       except errors.GenericError, err:
589         result.append("%s has invalid %s: %s" % (owner, attr, err))
590
591     def _helper_nic(owner, params):
592       try:
593         objects.NIC.CheckParameterSyntax(params)
594       except errors.ConfigurationError, err:
595         result.append("%s has invalid nicparams: %s" % (owner, err))
596
597     def _helper_ipolicy(owner, params, check_std):
598       try:
599         objects.InstancePolicy.CheckParameterSyntax(params, check_std)
600       except errors.ConfigurationError, err:
601         result.append("%s has invalid instance policy: %s" % (owner, err))
602
603     def _helper_ispecs(owner, params):
604       for key, value in params.items():
605         if key in constants.IPOLICY_ISPECS:
606           fullkey = "ipolicy/" + key
607           _helper(owner, fullkey, value, constants.ISPECS_PARAMETER_TYPES)
608         else:
609           # FIXME: assuming list type
610           if key in constants.IPOLICY_PARAMETERS:
611             exp_type = float
612           else:
613             exp_type = list
614           if not isinstance(value, exp_type):
615             result.append("%s has invalid instance policy: for %s,"
616                           " expecting %s, got %s" %
617                           (owner, key, exp_type.__name__, type(value)))
618
619     # check cluster parameters
620     _helper("cluster", "beparams", cluster.SimpleFillBE({}),
621             constants.BES_PARAMETER_TYPES)
622     _helper("cluster", "nicparams", cluster.SimpleFillNIC({}),
623             constants.NICS_PARAMETER_TYPES)
624     _helper_nic("cluster", cluster.SimpleFillNIC({}))
625     _helper("cluster", "ndparams", cluster.SimpleFillND({}),
626             constants.NDS_PARAMETER_TYPES)
627     _helper_ipolicy("cluster", cluster.SimpleFillIPolicy({}), True)
628     _helper_ispecs("cluster", cluster.SimpleFillIPolicy({}))
629
630     # per-instance checks
631     for instance_name in data.instances:
632       instance = data.instances[instance_name]
633       if instance.name != instance_name:
634         result.append("instance '%s' is indexed by wrong name '%s'" %
635                       (instance.name, instance_name))
636       if instance.primary_node not in data.nodes:
637         result.append("instance '%s' has invalid primary node '%s'" %
638                       (instance_name, instance.primary_node))
639       for snode in instance.secondary_nodes:
640         if snode not in data.nodes:
641           result.append("instance '%s' has invalid secondary node '%s'" %
642                         (instance_name, snode))
643       for idx, nic in enumerate(instance.nics):
644         if nic.mac in seen_macs:
645           result.append("instance '%s' has NIC %d mac %s duplicate" %
646                         (instance_name, idx, nic.mac))
647         else:
648           seen_macs.append(nic.mac)
649         if nic.nicparams:
650           filled = cluster.SimpleFillNIC(nic.nicparams)
651           owner = "instance %s nic %d" % (instance.name, idx)
652           _helper(owner, "nicparams",
653                   filled, constants.NICS_PARAMETER_TYPES)
654           _helper_nic(owner, filled)
655
656       # parameter checks
657       if instance.beparams:
658         _helper("instance %s" % instance.name, "beparams",
659                 cluster.FillBE(instance), constants.BES_PARAMETER_TYPES)
660
661       # gather the drbd ports for duplicate checks
662       for (idx, dsk) in enumerate(instance.disks):
663         if dsk.dev_type in constants.LDS_DRBD:
664           tcp_port = dsk.logical_id[2]
665           if tcp_port not in ports:
666             ports[tcp_port] = []
667           ports[tcp_port].append((instance.name, "drbd disk %s" % idx))
668       # gather network port reservation
669       net_port = getattr(instance, "network_port", None)
670       if net_port is not None:
671         if net_port not in ports:
672           ports[net_port] = []
673         ports[net_port].append((instance.name, "network port"))
674
675       # instance disk verify
676       for idx, disk in enumerate(instance.disks):
677         result.extend(["instance '%s' disk %d error: %s" %
678                        (instance.name, idx, msg) for msg in disk.Verify()])
679         result.extend(self._CheckDiskIDs(disk, seen_lids, seen_pids))
680
681       wrong_names = _CheckInstanceDiskIvNames(instance.disks)
682       if wrong_names:
683         tmp = "; ".join(("name of disk %s should be '%s', but is '%s'" %
684                          (idx, exp_name, actual_name))
685                         for (idx, exp_name, actual_name) in wrong_names)
686
687         result.append("Instance '%s' has wrongly named disks: %s" %
688                       (instance.name, tmp))
689
690     # cluster-wide pool of free ports
691     for free_port in cluster.tcpudp_port_pool:
692       if free_port not in ports:
693         ports[free_port] = []
694       ports[free_port].append(("cluster", "port marked as free"))
695
696     # compute tcp/udp duplicate ports
697     keys = ports.keys()
698     keys.sort()
699     for pnum in keys:
700       pdata = ports[pnum]
701       if len(pdata) > 1:
702         txt = utils.CommaJoin(["%s/%s" % val for val in pdata])
703         result.append("tcp/udp port %s has duplicates: %s" % (pnum, txt))
704
705     # highest used tcp port check
706     if keys:
707       if keys[-1] > cluster.highest_used_port:
708         result.append("Highest used port mismatch, saved %s, computed %s" %
709                       (cluster.highest_used_port, keys[-1]))
710
711     if not data.nodes[cluster.master_node].master_candidate:
712       result.append("Master node is not a master candidate")
713
714     # master candidate checks
715     mc_now, mc_max, _ = self._UnlockedGetMasterCandidateStats()
716     if mc_now < mc_max:
717       result.append("Not enough master candidates: actual %d, target %d" %
718                     (mc_now, mc_max))
719
720     # node checks
721     for node_name, node in data.nodes.items():
722       if node.name != node_name:
723         result.append("Node '%s' is indexed by wrong name '%s'" %
724                       (node.name, node_name))
725       if [node.master_candidate, node.drained, node.offline].count(True) > 1:
726         result.append("Node %s state is invalid: master_candidate=%s,"
727                       " drain=%s, offline=%s" %
728                       (node.name, node.master_candidate, node.drained,
729                        node.offline))
730       if node.group not in data.nodegroups:
731         result.append("Node '%s' has invalid group '%s'" %
732                       (node.name, node.group))
733       else:
734         _helper("node %s" % node.name, "ndparams",
735                 cluster.FillND(node, data.nodegroups[node.group]),
736                 constants.NDS_PARAMETER_TYPES)
737
738     # nodegroups checks
739     nodegroups_names = set()
740     for nodegroup_uuid in data.nodegroups:
741       nodegroup = data.nodegroups[nodegroup_uuid]
742       if nodegroup.uuid != nodegroup_uuid:
743         result.append("node group '%s' (uuid: '%s') indexed by wrong uuid '%s'"
744                       % (nodegroup.name, nodegroup.uuid, nodegroup_uuid))
745       if utils.UUID_RE.match(nodegroup.name.lower()):
746         result.append("node group '%s' (uuid: '%s') has uuid-like name" %
747                       (nodegroup.name, nodegroup.uuid))
748       if nodegroup.name in nodegroups_names:
749         result.append("duplicate node group name '%s'" % nodegroup.name)
750       else:
751         nodegroups_names.add(nodegroup.name)
752       group_name = "group %s" % nodegroup.name
753       _helper_ipolicy(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy),
754                       False)
755       _helper_ispecs(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy))
756       if nodegroup.ndparams:
757         _helper(group_name, "ndparams",
758                 cluster.SimpleFillND(nodegroup.ndparams),
759                 constants.NDS_PARAMETER_TYPES)
760
761     # drbd minors check
762     _, duplicates = self._UnlockedComputeDRBDMap()
763     for node, minor, instance_a, instance_b in duplicates:
764       result.append("DRBD minor %d on node %s is assigned twice to instances"
765                     " %s and %s" % (minor, node, instance_a, instance_b))
766
767     # IP checks
768     default_nicparams = cluster.nicparams[constants.PP_DEFAULT]
769     ips = {}
770
771     def _AddIpAddress(ip, name):
772       ips.setdefault(ip, []).append(name)
773
774     _AddIpAddress(cluster.master_ip, "cluster_ip")
775
776     for node in data.nodes.values():
777       _AddIpAddress(node.primary_ip, "node:%s/primary" % node.name)
778       if node.secondary_ip != node.primary_ip:
779         _AddIpAddress(node.secondary_ip, "node:%s/secondary" % node.name)
780
781     for instance in data.instances.values():
782       for idx, nic in enumerate(instance.nics):
783         if nic.ip is None:
784           continue
785
786         nicparams = objects.FillDict(default_nicparams, nic.nicparams)
787         nic_mode = nicparams[constants.NIC_MODE]
788         nic_link = nicparams[constants.NIC_LINK]
789
790         if nic_mode == constants.NIC_MODE_BRIDGED:
791           link = "bridge:%s" % nic_link
792         elif nic_mode == constants.NIC_MODE_ROUTED:
793           link = "route:%s" % nic_link
794         else:
795           raise errors.ProgrammerError("NIC mode '%s' not handled" % nic_mode)
796
797         _AddIpAddress("%s/%s/%s" % (link, nic.ip, nic.network),
798                       "instance:%s/nic:%d" % (instance.name, idx))
799
800     for ip, owners in ips.items():
801       if len(owners) > 1:
802         result.append("IP address %s is used by multiple owners: %s" %
803                       (ip, utils.CommaJoin(owners)))
804
805     return result
806
807   @locking.ssynchronized(_config_lock, shared=1)
808   def VerifyConfig(self):
809     """Verify function.
810
811     This is just a wrapper over L{_UnlockedVerifyConfig}.
812
813     @rtype: list
814     @return: a list of error messages; a non-empty list signifies
815         configuration errors
816
817     """
818     return self._UnlockedVerifyConfig()
819
820   def _UnlockedSetDiskID(self, disk, node_name):
821     """Convert the unique ID to the ID needed on the target nodes.
822
823     This is used only for drbd, which needs ip/port configuration.
824
825     The routine descends down and updates its children also, because
826     this helps when the only the top device is passed to the remote
827     node.
828
829     This function is for internal use, when the config lock is already held.
830
831     """
832     if disk.children:
833       for child in disk.children:
834         self._UnlockedSetDiskID(child, node_name)
835
836     if disk.logical_id is None and disk.physical_id is not None:
837       return
838     if disk.dev_type == constants.LD_DRBD8:
839       pnode, snode, port, pminor, sminor, secret = disk.logical_id
840       if node_name not in (pnode, snode):
841         raise errors.ConfigurationError("DRBD device not knowing node %s" %
842                                         node_name)
843       pnode_info = self._UnlockedGetNodeInfo(pnode)
844       snode_info = self._UnlockedGetNodeInfo(snode)
845       if pnode_info is None or snode_info is None:
846         raise errors.ConfigurationError("Can't find primary or secondary node"
847                                         " for %s" % str(disk))
848       p_data = (pnode_info.secondary_ip, port)
849       s_data = (snode_info.secondary_ip, port)
850       if pnode == node_name:
851         disk.physical_id = p_data + s_data + (pminor, secret)
852       else: # it must be secondary, we tested above
853         disk.physical_id = s_data + p_data + (sminor, secret)
854     else:
855       disk.physical_id = disk.logical_id
856     return
857
858   @locking.ssynchronized(_config_lock)
859   def SetDiskID(self, disk, node_name):
860     """Convert the unique ID to the ID needed on the target nodes.
861
862     This is used only for drbd, which needs ip/port configuration.
863
864     The routine descends down and updates its children also, because
865     this helps when the only the top device is passed to the remote
866     node.
867
868     """
869     return self._UnlockedSetDiskID(disk, node_name)
870
871   @locking.ssynchronized(_config_lock)
872   def AddTcpUdpPort(self, port):
873     """Adds a new port to the available port pool.
874
875     @warning: this method does not "flush" the configuration (via
876         L{_WriteConfig}); callers should do that themselves once the
877         configuration is stable
878
879     """
880     if not isinstance(port, int):
881       raise errors.ProgrammerError("Invalid type passed for port")
882
883     self._config_data.cluster.tcpudp_port_pool.add(port)
884
885   @locking.ssynchronized(_config_lock, shared=1)
886   def GetPortList(self):
887     """Returns a copy of the current port list.
888
889     """
890     return self._config_data.cluster.tcpudp_port_pool.copy()
891
892   @locking.ssynchronized(_config_lock)
893   def AllocatePort(self):
894     """Allocate a port.
895
896     The port will be taken from the available port pool or from the
897     default port range (and in this case we increase
898     highest_used_port).
899
900     """
901     # If there are TCP/IP ports configured, we use them first.
902     if self._config_data.cluster.tcpudp_port_pool:
903       port = self._config_data.cluster.tcpudp_port_pool.pop()
904     else:
905       port = self._config_data.cluster.highest_used_port + 1
906       if port >= constants.LAST_DRBD_PORT:
907         raise errors.ConfigurationError("The highest used port is greater"
908                                         " than %s. Aborting." %
909                                         constants.LAST_DRBD_PORT)
910       self._config_data.cluster.highest_used_port = port
911
912     self._WriteConfig()
913     return port
914
915   def _UnlockedComputeDRBDMap(self):
916     """Compute the used DRBD minor/nodes.
917
918     @rtype: (dict, list)
919     @return: dictionary of node_name: dict of minor: instance_name;
920         the returned dict will have all the nodes in it (even if with
921         an empty list), and a list of duplicates; if the duplicates
922         list is not empty, the configuration is corrupted and its caller
923         should raise an exception
924
925     """
926     def _AppendUsedPorts(instance_name, disk, used):
927       duplicates = []
928       if disk.dev_type == constants.LD_DRBD8 and len(disk.logical_id) >= 5:
929         node_a, node_b, _, minor_a, minor_b = disk.logical_id[:5]
930         for node, port in ((node_a, minor_a), (node_b, minor_b)):
931           assert node in used, ("Node '%s' of instance '%s' not found"
932                                 " in node list" % (node, instance_name))
933           if port in used[node]:
934             duplicates.append((node, port, instance_name, used[node][port]))
935           else:
936             used[node][port] = instance_name
937       if disk.children:
938         for child in disk.children:
939           duplicates.extend(_AppendUsedPorts(instance_name, child, used))
940       return duplicates
941
942     duplicates = []
943     my_dict = dict((node, {}) for node in self._config_data.nodes)
944     for instance in self._config_data.instances.itervalues():
945       for disk in instance.disks:
946         duplicates.extend(_AppendUsedPorts(instance.name, disk, my_dict))
947     for (node, minor), instance in self._temporary_drbds.iteritems():
948       if minor in my_dict[node] and my_dict[node][minor] != instance:
949         duplicates.append((node, minor, instance, my_dict[node][minor]))
950       else:
951         my_dict[node][minor] = instance
952     return my_dict, duplicates
953
954   @locking.ssynchronized(_config_lock)
955   def ComputeDRBDMap(self):
956     """Compute the used DRBD minor/nodes.
957
958     This is just a wrapper over L{_UnlockedComputeDRBDMap}.
959
960     @return: dictionary of node_name: dict of minor: instance_name;
961         the returned dict will have all the nodes in it (even if with
962         an empty list).
963
964     """
965     d_map, duplicates = self._UnlockedComputeDRBDMap()
966     if duplicates:
967       raise errors.ConfigurationError("Duplicate DRBD ports detected: %s" %
968                                       str(duplicates))
969     return d_map
970
971   @locking.ssynchronized(_config_lock)
972   def AllocateDRBDMinor(self, nodes, instance):
973     """Allocate a drbd minor.
974
975     The free minor will be automatically computed from the existing
976     devices. A node can be given multiple times in order to allocate
977     multiple minors. The result is the list of minors, in the same
978     order as the passed nodes.
979
980     @type instance: string
981     @param instance: the instance for which we allocate minors
982
983     """
984     assert isinstance(instance, basestring), \
985            "Invalid argument '%s' passed to AllocateDRBDMinor" % instance
986
987     d_map, duplicates = self._UnlockedComputeDRBDMap()
988     if duplicates:
989       raise errors.ConfigurationError("Duplicate DRBD ports detected: %s" %
990                                       str(duplicates))
991     result = []
992     for nname in nodes:
993       ndata = d_map[nname]
994       if not ndata:
995         # no minors used, we can start at 0
996         result.append(0)
997         ndata[0] = instance
998         self._temporary_drbds[(nname, 0)] = instance
999         continue
1000       keys = ndata.keys()
1001       keys.sort()
1002       ffree = utils.FirstFree(keys)
1003       if ffree is None:
1004         # return the next minor
1005         # TODO: implement high-limit check
1006         minor = keys[-1] + 1
1007       else:
1008         minor = ffree
1009       # double-check minor against current instances
1010       assert minor not in d_map[nname], \
1011              ("Attempt to reuse allocated DRBD minor %d on node %s,"
1012               " already allocated to instance %s" %
1013               (minor, nname, d_map[nname][minor]))
1014       ndata[minor] = instance
1015       # double-check minor against reservation
1016       r_key = (nname, minor)
1017       assert r_key not in self._temporary_drbds, \
1018              ("Attempt to reuse reserved DRBD minor %d on node %s,"
1019               " reserved for instance %s" %
1020               (minor, nname, self._temporary_drbds[r_key]))
1021       self._temporary_drbds[r_key] = instance
1022       result.append(minor)
1023     logging.debug("Request to allocate drbd minors, input: %s, returning %s",
1024                   nodes, result)
1025     return result
1026
1027   def _UnlockedReleaseDRBDMinors(self, instance):
1028     """Release temporary drbd minors allocated for a given instance.
1029
1030     @type instance: string
1031     @param instance: the instance for which temporary minors should be
1032                      released
1033
1034     """
1035     assert isinstance(instance, basestring), \
1036            "Invalid argument passed to ReleaseDRBDMinors"
1037     for key, name in self._temporary_drbds.items():
1038       if name == instance:
1039         del self._temporary_drbds[key]
1040
1041   @locking.ssynchronized(_config_lock)
1042   def ReleaseDRBDMinors(self, instance):
1043     """Release temporary drbd minors allocated for a given instance.
1044
1045     This should be called on the error paths, on the success paths
1046     it's automatically called by the ConfigWriter add and update
1047     functions.
1048
1049     This function is just a wrapper over L{_UnlockedReleaseDRBDMinors}.
1050
1051     @type instance: string
1052     @param instance: the instance for which temporary minors should be
1053                      released
1054
1055     """
1056     self._UnlockedReleaseDRBDMinors(instance)
1057
1058   @locking.ssynchronized(_config_lock, shared=1)
1059   def GetConfigVersion(self):
1060     """Get the configuration version.
1061
1062     @return: Config version
1063
1064     """
1065     return self._config_data.version
1066
1067   @locking.ssynchronized(_config_lock, shared=1)
1068   def GetClusterName(self):
1069     """Get cluster name.
1070
1071     @return: Cluster name
1072
1073     """
1074     return self._config_data.cluster.cluster_name
1075
1076   @locking.ssynchronized(_config_lock, shared=1)
1077   def GetMasterNode(self):
1078     """Get the hostname of the master node for this cluster.
1079
1080     @return: Master hostname
1081
1082     """
1083     return self._config_data.cluster.master_node
1084
1085   @locking.ssynchronized(_config_lock, shared=1)
1086   def GetMasterIP(self):
1087     """Get the IP of the master node for this cluster.
1088
1089     @return: Master IP
1090
1091     """
1092     return self._config_data.cluster.master_ip
1093
1094   @locking.ssynchronized(_config_lock, shared=1)
1095   def GetMasterNetdev(self):
1096     """Get the master network device for this cluster.
1097
1098     """
1099     return self._config_data.cluster.master_netdev
1100
1101   @locking.ssynchronized(_config_lock, shared=1)
1102   def GetMasterNetmask(self):
1103     """Get the netmask of the master node for this cluster.
1104
1105     """
1106     return self._config_data.cluster.master_netmask
1107
1108   @locking.ssynchronized(_config_lock, shared=1)
1109   def GetUseExternalMipScript(self):
1110     """Get flag representing whether to use the external master IP setup script.
1111
1112     """
1113     return self._config_data.cluster.use_external_mip_script
1114
1115   @locking.ssynchronized(_config_lock, shared=1)
1116   def GetFileStorageDir(self):
1117     """Get the file storage dir for this cluster.
1118
1119     """
1120     return self._config_data.cluster.file_storage_dir
1121
1122   @locking.ssynchronized(_config_lock, shared=1)
1123   def GetSharedFileStorageDir(self):
1124     """Get the shared file storage dir for this cluster.
1125
1126     """
1127     return self._config_data.cluster.shared_file_storage_dir
1128
1129   @locking.ssynchronized(_config_lock, shared=1)
1130   def GetHypervisorType(self):
1131     """Get the hypervisor type for this cluster.
1132
1133     """
1134     return self._config_data.cluster.enabled_hypervisors[0]
1135
1136   @locking.ssynchronized(_config_lock, shared=1)
1137   def GetHostKey(self):
1138     """Return the rsa hostkey from the config.
1139
1140     @rtype: string
1141     @return: the rsa hostkey
1142
1143     """
1144     return self._config_data.cluster.rsahostkeypub
1145
1146   @locking.ssynchronized(_config_lock, shared=1)
1147   def GetDefaultIAllocator(self):
1148     """Get the default instance allocator for this cluster.
1149
1150     """
1151     return self._config_data.cluster.default_iallocator
1152
1153   @locking.ssynchronized(_config_lock, shared=1)
1154   def GetPrimaryIPFamily(self):
1155     """Get cluster primary ip family.
1156
1157     @return: primary ip family
1158
1159     """
1160     return self._config_data.cluster.primary_ip_family
1161
1162   @locking.ssynchronized(_config_lock, shared=1)
1163   def GetMasterNetworkParameters(self):
1164     """Get network parameters of the master node.
1165
1166     @rtype: L{object.MasterNetworkParameters}
1167     @return: network parameters of the master node
1168
1169     """
1170     cluster = self._config_data.cluster
1171     result = objects.MasterNetworkParameters(
1172       name=cluster.master_node, ip=cluster.master_ip,
1173       netmask=cluster.master_netmask, netdev=cluster.master_netdev,
1174       ip_family=cluster.primary_ip_family)
1175
1176     return result
1177
1178   @locking.ssynchronized(_config_lock)
1179   def AddNodeGroup(self, group, ec_id, check_uuid=True):
1180     """Add a node group to the configuration.
1181
1182     This method calls group.UpgradeConfig() to fill any missing attributes
1183     according to their default values.
1184
1185     @type group: L{objects.NodeGroup}
1186     @param group: the NodeGroup object to add
1187     @type ec_id: string
1188     @param ec_id: unique id for the job to use when creating a missing UUID
1189     @type check_uuid: bool
1190     @param check_uuid: add an UUID to the group if it doesn't have one or, if
1191                        it does, ensure that it does not exist in the
1192                        configuration already
1193
1194     """
1195     self._UnlockedAddNodeGroup(group, ec_id, check_uuid)
1196     self._WriteConfig()
1197
1198   def _UnlockedAddNodeGroup(self, group, ec_id, check_uuid):
1199     """Add a node group to the configuration.
1200
1201     """
1202     logging.info("Adding node group %s to configuration", group.name)
1203
1204     # Some code might need to add a node group with a pre-populated UUID
1205     # generated with ConfigWriter.GenerateUniqueID(). We allow them to bypass
1206     # the "does this UUID" exist already check.
1207     if check_uuid:
1208       self._EnsureUUID(group, ec_id)
1209
1210     try:
1211       existing_uuid = self._UnlockedLookupNodeGroup(group.name)
1212     except errors.OpPrereqError:
1213       pass
1214     else:
1215       raise errors.OpPrereqError("Desired group name '%s' already exists as a"
1216                                  " node group (UUID: %s)" %
1217                                  (group.name, existing_uuid),
1218                                  errors.ECODE_EXISTS)
1219
1220     group.serial_no = 1
1221     group.ctime = group.mtime = time.time()
1222     group.UpgradeConfig()
1223
1224     self._config_data.nodegroups[group.uuid] = group
1225     self._config_data.cluster.serial_no += 1
1226
1227   @locking.ssynchronized(_config_lock)
1228   def RemoveNodeGroup(self, group_uuid):
1229     """Remove a node group from the configuration.
1230
1231     @type group_uuid: string
1232     @param group_uuid: the UUID of the node group to remove
1233
1234     """
1235     logging.info("Removing node group %s from configuration", group_uuid)
1236
1237     if group_uuid not in self._config_data.nodegroups:
1238       raise errors.ConfigurationError("Unknown node group '%s'" % group_uuid)
1239
1240     assert len(self._config_data.nodegroups) != 1, \
1241             "Group '%s' is the only group, cannot be removed" % group_uuid
1242
1243     del self._config_data.nodegroups[group_uuid]
1244     self._config_data.cluster.serial_no += 1
1245     self._WriteConfig()
1246
1247   def _UnlockedLookupNodeGroup(self, target):
1248     """Lookup a node group's UUID.
1249
1250     @type target: string or None
1251     @param target: group name or UUID or None to look for the default
1252     @rtype: string
1253     @return: nodegroup UUID
1254     @raises errors.OpPrereqError: when the target group cannot be found
1255
1256     """
1257     if target is None:
1258       if len(self._config_data.nodegroups) != 1:
1259         raise errors.OpPrereqError("More than one node group exists. Target"
1260                                    " group must be specified explicitly.")
1261       else:
1262         return self._config_data.nodegroups.keys()[0]
1263     if target in self._config_data.nodegroups:
1264       return target
1265     for nodegroup in self._config_data.nodegroups.values():
1266       if nodegroup.name == target:
1267         return nodegroup.uuid
1268     raise errors.OpPrereqError("Node group '%s' not found" % target,
1269                                errors.ECODE_NOENT)
1270
1271   @locking.ssynchronized(_config_lock, shared=1)
1272   def LookupNodeGroup(self, target):
1273     """Lookup a node group's UUID.
1274
1275     This function is just a wrapper over L{_UnlockedLookupNodeGroup}.
1276
1277     @type target: string or None
1278     @param target: group name or UUID or None to look for the default
1279     @rtype: string
1280     @return: nodegroup UUID
1281
1282     """
1283     return self._UnlockedLookupNodeGroup(target)
1284
1285   def _UnlockedGetNodeGroup(self, uuid):
1286     """Lookup a node group.
1287
1288     @type uuid: string
1289     @param uuid: group UUID
1290     @rtype: L{objects.NodeGroup} or None
1291     @return: nodegroup object, or None if not found
1292
1293     """
1294     if uuid not in self._config_data.nodegroups:
1295       return None
1296
1297     return self._config_data.nodegroups[uuid]
1298
1299   @locking.ssynchronized(_config_lock, shared=1)
1300   def GetNodeGroup(self, uuid):
1301     """Lookup a node group.
1302
1303     @type uuid: string
1304     @param uuid: group UUID
1305     @rtype: L{objects.NodeGroup} or None
1306     @return: nodegroup object, or None if not found
1307
1308     """
1309     return self._UnlockedGetNodeGroup(uuid)
1310
1311   @locking.ssynchronized(_config_lock, shared=1)
1312   def GetAllNodeGroupsInfo(self):
1313     """Get the configuration of all node groups.
1314
1315     """
1316     return dict(self._config_data.nodegroups)
1317
1318   @locking.ssynchronized(_config_lock, shared=1)
1319   def GetNodeGroupList(self):
1320     """Get a list of node groups.
1321
1322     """
1323     return self._config_data.nodegroups.keys()
1324
1325   @locking.ssynchronized(_config_lock, shared=1)
1326   def GetNodeGroupMembersByNodes(self, nodes):
1327     """Get nodes which are member in the same nodegroups as the given nodes.
1328
1329     """
1330     ngfn = lambda node_name: self._UnlockedGetNodeInfo(node_name).group
1331     return frozenset(member_name
1332                      for node_name in nodes
1333                      for member_name in
1334                        self._UnlockedGetNodeGroup(ngfn(node_name)).members)
1335
1336   @locking.ssynchronized(_config_lock, shared=1)
1337   def GetMultiNodeGroupInfo(self, group_uuids):
1338     """Get the configuration of multiple node groups.
1339
1340     @param group_uuids: List of node group UUIDs
1341     @rtype: list
1342     @return: List of tuples of (group_uuid, group_info)
1343
1344     """
1345     return [(uuid, self._UnlockedGetNodeGroup(uuid)) for uuid in group_uuids]
1346
1347   @locking.ssynchronized(_config_lock)
1348   def AddInstance(self, instance, ec_id):
1349     """Add an instance to the config.
1350
1351     This should be used after creating a new instance.
1352
1353     @type instance: L{objects.Instance}
1354     @param instance: the instance object
1355
1356     """
1357     if not isinstance(instance, objects.Instance):
1358       raise errors.ProgrammerError("Invalid type passed to AddInstance")
1359
1360     if instance.disk_template != constants.DT_DISKLESS:
1361       all_lvs = instance.MapLVsByNode()
1362       logging.info("Instance '%s' DISK_LAYOUT: %s", instance.name, all_lvs)
1363
1364     all_macs = self._AllMACs()
1365     for nic in instance.nics:
1366       if nic.mac in all_macs:
1367         raise errors.ConfigurationError("Cannot add instance %s:"
1368                                         " MAC address '%s' already in use." %
1369                                         (instance.name, nic.mac))
1370
1371     self._EnsureUUID(instance, ec_id)
1372
1373     instance.serial_no = 1
1374     instance.ctime = instance.mtime = time.time()
1375     self._config_data.instances[instance.name] = instance
1376     self._config_data.cluster.serial_no += 1
1377     self._UnlockedReleaseDRBDMinors(instance.name)
1378     self._UnlockedCommitTemporaryIps(ec_id)
1379     self._WriteConfig()
1380
1381   def _EnsureUUID(self, item, ec_id):
1382     """Ensures a given object has a valid UUID.
1383
1384     @param item: the instance or node to be checked
1385     @param ec_id: the execution context id for the uuid reservation
1386
1387     """
1388     if not item.uuid:
1389       item.uuid = self._GenerateUniqueID(ec_id)
1390     elif item.uuid in self._AllIDs(include_temporary=True):
1391       raise errors.ConfigurationError("Cannot add '%s': UUID %s already"
1392                                       " in use" % (item.name, item.uuid))
1393
1394   def _SetInstanceStatus(self, instance_name, status):
1395     """Set the instance's status to a given value.
1396
1397     """
1398     assert status in constants.ADMINST_ALL, \
1399            "Invalid status '%s' passed to SetInstanceStatus" % (status,)
1400
1401     if instance_name not in self._config_data.instances:
1402       raise errors.ConfigurationError("Unknown instance '%s'" %
1403                                       instance_name)
1404     instance = self._config_data.instances[instance_name]
1405     if instance.admin_state != status:
1406       instance.admin_state = status
1407       instance.serial_no += 1
1408       instance.mtime = time.time()
1409       self._WriteConfig()
1410
1411   @locking.ssynchronized(_config_lock)
1412   def MarkInstanceUp(self, instance_name):
1413     """Mark the instance status to up in the config.
1414
1415     """
1416     self._SetInstanceStatus(instance_name, constants.ADMINST_UP)
1417
1418   @locking.ssynchronized(_config_lock)
1419   def MarkInstanceOffline(self, instance_name):
1420     """Mark the instance status to down in the config.
1421
1422     """
1423     self._SetInstanceStatus(instance_name, constants.ADMINST_OFFLINE)
1424
1425   @locking.ssynchronized(_config_lock)
1426   def RemoveInstance(self, instance_name):
1427     """Remove the instance from the configuration.
1428
1429     """
1430     if instance_name not in self._config_data.instances:
1431       raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1432
1433     # If a network port has been allocated to the instance,
1434     # return it to the pool of free ports.
1435     inst = self._config_data.instances[instance_name]
1436     network_port = getattr(inst, "network_port", None)
1437     if network_port is not None:
1438       self._config_data.cluster.tcpudp_port_pool.add(network_port)
1439
1440     instance = self._UnlockedGetInstanceInfo(instance_name)
1441
1442     for nic in instance.nics:
1443       if nic.network is not None and nic.ip is not None:
1444         net_uuid = self._UnlockedLookupNetwork(nic.network)
1445         if net_uuid:
1446           # Return all IP addresses to the respective address pools
1447           self._UnlockedCommitIp('release', net_uuid, nic.ip)
1448
1449
1450     del self._config_data.instances[instance_name]
1451     self._config_data.cluster.serial_no += 1
1452     self._WriteConfig()
1453
1454   @locking.ssynchronized(_config_lock)
1455   def RenameInstance(self, old_name, new_name):
1456     """Rename an instance.
1457
1458     This needs to be done in ConfigWriter and not by RemoveInstance
1459     combined with AddInstance as only we can guarantee an atomic
1460     rename.
1461
1462     """
1463     if old_name not in self._config_data.instances:
1464       raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
1465
1466     # Operate on a copy to not loose instance object in case of a failure
1467     inst = self._config_data.instances[old_name].Copy()
1468     inst.name = new_name
1469
1470     for (idx, disk) in enumerate(inst.disks):
1471       if disk.dev_type == constants.LD_FILE:
1472         # rename the file paths in logical and physical id
1473         file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1]))
1474         disk.logical_id = (disk.logical_id[0],
1475                            utils.PathJoin(file_storage_dir, inst.name,
1476                                           "disk%s" % idx))
1477         disk.physical_id = disk.logical_id
1478
1479     # Actually replace instance object
1480     del self._config_data.instances[old_name]
1481     self._config_data.instances[inst.name] = inst
1482
1483     # Force update of ssconf files
1484     self._config_data.cluster.serial_no += 1
1485
1486     self._WriteConfig()
1487
1488   @locking.ssynchronized(_config_lock)
1489   def MarkInstanceDown(self, instance_name):
1490     """Mark the status of an instance to down in the configuration.
1491
1492     """
1493     self._SetInstanceStatus(instance_name, constants.ADMINST_DOWN)
1494
1495   def _UnlockedGetInstanceList(self):
1496     """Get the list of instances.
1497
1498     This function is for internal use, when the config lock is already held.
1499
1500     """
1501     return self._config_data.instances.keys()
1502
1503   @locking.ssynchronized(_config_lock, shared=1)
1504   def GetInstanceList(self):
1505     """Get the list of instances.
1506
1507     @return: array of instances, ex. ['instance2.example.com',
1508         'instance1.example.com']
1509
1510     """
1511     return self._UnlockedGetInstanceList()
1512
1513   def ExpandInstanceName(self, short_name):
1514     """Attempt to expand an incomplete instance name.
1515
1516     """
1517     # Locking is done in L{ConfigWriter.GetInstanceList}
1518     return _MatchNameComponentIgnoreCase(short_name, self.GetInstanceList())
1519
1520   def _UnlockedGetInstanceInfo(self, instance_name):
1521     """Returns information about an instance.
1522
1523     This function is for internal use, when the config lock is already held.
1524
1525     """
1526     if instance_name not in self._config_data.instances:
1527       return None
1528
1529     return self._config_data.instances[instance_name]
1530
1531   @locking.ssynchronized(_config_lock, shared=1)
1532   def GetInstanceInfo(self, instance_name):
1533     """Returns information about an instance.
1534
1535     It takes the information from the configuration file. Other information of
1536     an instance are taken from the live systems.
1537
1538     @param instance_name: name of the instance, e.g.
1539         I{instance1.example.com}
1540
1541     @rtype: L{objects.Instance}
1542     @return: the instance object
1543
1544     """
1545     return self._UnlockedGetInstanceInfo(instance_name)
1546
1547   @locking.ssynchronized(_config_lock, shared=1)
1548   def GetInstanceNodeGroups(self, instance_name, primary_only=False):
1549     """Returns set of node group UUIDs for instance's nodes.
1550
1551     @rtype: frozenset
1552
1553     """
1554     instance = self._UnlockedGetInstanceInfo(instance_name)
1555     if not instance:
1556       raise errors.ConfigurationError("Unknown instance '%s'" % instance_name)
1557
1558     if primary_only:
1559       nodes = [instance.primary_node]
1560     else:
1561       nodes = instance.all_nodes
1562
1563     return frozenset(self._UnlockedGetNodeInfo(node_name).group
1564                      for node_name in nodes)
1565
1566   @locking.ssynchronized(_config_lock, shared=1)
1567   def GetMultiInstanceInfo(self, instances):
1568     """Get the configuration of multiple instances.
1569
1570     @param instances: list of instance names
1571     @rtype: list
1572     @return: list of tuples (instance, instance_info), where
1573         instance_info is what would GetInstanceInfo return for the
1574         node, while keeping the original order
1575
1576     """
1577     return [(name, self._UnlockedGetInstanceInfo(name)) for name in instances]
1578
1579   @locking.ssynchronized(_config_lock, shared=1)
1580   def GetAllInstancesInfo(self):
1581     """Get the configuration of all instances.
1582
1583     @rtype: dict
1584     @return: dict of (instance, instance_info), where instance_info is what
1585               would GetInstanceInfo return for the node
1586
1587     """
1588     my_dict = dict([(instance, self._UnlockedGetInstanceInfo(instance))
1589                     for instance in self._UnlockedGetInstanceList()])
1590     return my_dict
1591
1592   @locking.ssynchronized(_config_lock, shared=1)
1593   def GetInstancesInfoByFilter(self, filter_fn):
1594     """Get instance configuration with a filter.
1595
1596     @type filter_fn: callable
1597     @param filter_fn: Filter function receiving instance object as parameter,
1598       returning boolean. Important: this function is called while the
1599       configuration locks is held. It must not do any complex work or call
1600       functions potentially leading to a deadlock. Ideally it doesn't call any
1601       other functions and just compares instance attributes.
1602
1603     """
1604     return dict((name, inst)
1605                 for (name, inst) in self._config_data.instances.items()
1606                 if filter_fn(inst))
1607
1608   @locking.ssynchronized(_config_lock)
1609   def AddNode(self, node, ec_id):
1610     """Add a node to the configuration.
1611
1612     @type node: L{objects.Node}
1613     @param node: a Node instance
1614
1615     """
1616     logging.info("Adding node %s to configuration", node.name)
1617
1618     self._EnsureUUID(node, ec_id)
1619
1620     node.serial_no = 1
1621     node.ctime = node.mtime = time.time()
1622     self._UnlockedAddNodeToGroup(node.name, node.group)
1623     self._config_data.nodes[node.name] = node
1624     self._config_data.cluster.serial_no += 1
1625     self._WriteConfig()
1626
1627   @locking.ssynchronized(_config_lock)
1628   def RemoveNode(self, node_name):
1629     """Remove a node from the configuration.
1630
1631     """
1632     logging.info("Removing node %s from configuration", node_name)
1633
1634     if node_name not in self._config_data.nodes:
1635       raise errors.ConfigurationError("Unknown node '%s'" % node_name)
1636
1637     self._UnlockedRemoveNodeFromGroup(self._config_data.nodes[node_name])
1638     del self._config_data.nodes[node_name]
1639     self._config_data.cluster.serial_no += 1
1640     self._WriteConfig()
1641
1642   def ExpandNodeName(self, short_name):
1643     """Attempt to expand an incomplete node name.
1644
1645     """
1646     # Locking is done in L{ConfigWriter.GetNodeList}
1647     return _MatchNameComponentIgnoreCase(short_name, self.GetNodeList())
1648
1649   def _UnlockedGetNodeInfo(self, node_name):
1650     """Get the configuration of a node, as stored in the config.
1651
1652     This function is for internal use, when the config lock is already
1653     held.
1654
1655     @param node_name: the node name, e.g. I{node1.example.com}
1656
1657     @rtype: L{objects.Node}
1658     @return: the node object
1659
1660     """
1661     if node_name not in self._config_data.nodes:
1662       return None
1663
1664     return self._config_data.nodes[node_name]
1665
1666   @locking.ssynchronized(_config_lock, shared=1)
1667   def GetNodeInfo(self, node_name):
1668     """Get the configuration of a node, as stored in the config.
1669
1670     This is just a locked wrapper over L{_UnlockedGetNodeInfo}.
1671
1672     @param node_name: the node name, e.g. I{node1.example.com}
1673
1674     @rtype: L{objects.Node}
1675     @return: the node object
1676
1677     """
1678     return self._UnlockedGetNodeInfo(node_name)
1679
1680   @locking.ssynchronized(_config_lock, shared=1)
1681   def GetNodeInstances(self, node_name):
1682     """Get the instances of a node, as stored in the config.
1683
1684     @param node_name: the node name, e.g. I{node1.example.com}
1685
1686     @rtype: (list, list)
1687     @return: a tuple with two lists: the primary and the secondary instances
1688
1689     """
1690     pri = []
1691     sec = []
1692     for inst in self._config_data.instances.values():
1693       if inst.primary_node == node_name:
1694         pri.append(inst.name)
1695       if node_name in inst.secondary_nodes:
1696         sec.append(inst.name)
1697     return (pri, sec)
1698
1699   @locking.ssynchronized(_config_lock, shared=1)
1700   def GetNodeGroupInstances(self, uuid, primary_only=False):
1701     """Get the instances of a node group.
1702
1703     @param uuid: Node group UUID
1704     @param primary_only: Whether to only consider primary nodes
1705     @rtype: frozenset
1706     @return: List of instance names in node group
1707
1708     """
1709     if primary_only:
1710       nodes_fn = lambda inst: [inst.primary_node]
1711     else:
1712       nodes_fn = lambda inst: inst.all_nodes
1713
1714     return frozenset(inst.name
1715                      for inst in self._config_data.instances.values()
1716                      for node_name in nodes_fn(inst)
1717                      if self._UnlockedGetNodeInfo(node_name).group == uuid)
1718
1719   def _UnlockedGetNodeList(self):
1720     """Return the list of nodes which are in the configuration.
1721
1722     This function is for internal use, when the config lock is already
1723     held.
1724
1725     @rtype: list
1726
1727     """
1728     return self._config_data.nodes.keys()
1729
1730   @locking.ssynchronized(_config_lock, shared=1)
1731   def GetNodeList(self):
1732     """Return the list of nodes which are in the configuration.
1733
1734     """
1735     return self._UnlockedGetNodeList()
1736
1737   def _UnlockedGetOnlineNodeList(self):
1738     """Return the list of nodes which are online.
1739
1740     """
1741     all_nodes = [self._UnlockedGetNodeInfo(node)
1742                  for node in self._UnlockedGetNodeList()]
1743     return [node.name for node in all_nodes if not node.offline]
1744
1745   @locking.ssynchronized(_config_lock, shared=1)
1746   def GetOnlineNodeList(self):
1747     """Return the list of nodes which are online.
1748
1749     """
1750     return self._UnlockedGetOnlineNodeList()
1751
1752   @locking.ssynchronized(_config_lock, shared=1)
1753   def GetVmCapableNodeList(self):
1754     """Return the list of nodes which are not vm capable.
1755
1756     """
1757     all_nodes = [self._UnlockedGetNodeInfo(node)
1758                  for node in self._UnlockedGetNodeList()]
1759     return [node.name for node in all_nodes if node.vm_capable]
1760
1761   @locking.ssynchronized(_config_lock, shared=1)
1762   def GetNonVmCapableNodeList(self):
1763     """Return the list of nodes which are not vm capable.
1764
1765     """
1766     all_nodes = [self._UnlockedGetNodeInfo(node)
1767                  for node in self._UnlockedGetNodeList()]
1768     return [node.name for node in all_nodes if not node.vm_capable]
1769
1770   @locking.ssynchronized(_config_lock, shared=1)
1771   def GetMultiNodeInfo(self, nodes):
1772     """Get the configuration of multiple nodes.
1773
1774     @param nodes: list of node names
1775     @rtype: list
1776     @return: list of tuples of (node, node_info), where node_info is
1777         what would GetNodeInfo return for the node, in the original
1778         order
1779
1780     """
1781     return [(name, self._UnlockedGetNodeInfo(name)) for name in nodes]
1782
1783   @locking.ssynchronized(_config_lock, shared=1)
1784   def GetAllNodesInfo(self):
1785     """Get the configuration of all nodes.
1786
1787     @rtype: dict
1788     @return: dict of (node, node_info), where node_info is what
1789               would GetNodeInfo return for the node
1790
1791     """
1792     return self._UnlockedGetAllNodesInfo()
1793
1794   def _UnlockedGetAllNodesInfo(self):
1795     """Gets configuration of all nodes.
1796
1797     @note: See L{GetAllNodesInfo}
1798
1799     """
1800     return dict([(node, self._UnlockedGetNodeInfo(node))
1801                  for node in self._UnlockedGetNodeList()])
1802
1803   @locking.ssynchronized(_config_lock, shared=1)
1804   def GetNodeGroupsFromNodes(self, nodes):
1805     """Returns groups for a list of nodes.
1806
1807     @type nodes: list of string
1808     @param nodes: List of node names
1809     @rtype: frozenset
1810
1811     """
1812     return frozenset(self._UnlockedGetNodeInfo(name).group for name in nodes)
1813
1814   def _UnlockedGetMasterCandidateStats(self, exceptions=None):
1815     """Get the number of current and maximum desired and possible candidates.
1816
1817     @type exceptions: list
1818     @param exceptions: if passed, list of nodes that should be ignored
1819     @rtype: tuple
1820     @return: tuple of (current, desired and possible, possible)
1821
1822     """
1823     mc_now = mc_should = mc_max = 0
1824     for node in self._config_data.nodes.values():
1825       if exceptions and node.name in exceptions:
1826         continue
1827       if not (node.offline or node.drained) and node.master_capable:
1828         mc_max += 1
1829       if node.master_candidate:
1830         mc_now += 1
1831     mc_should = min(mc_max, self._config_data.cluster.candidate_pool_size)
1832     return (mc_now, mc_should, mc_max)
1833
1834   @locking.ssynchronized(_config_lock, shared=1)
1835   def GetMasterCandidateStats(self, exceptions=None):
1836     """Get the number of current and maximum possible candidates.
1837
1838     This is just a wrapper over L{_UnlockedGetMasterCandidateStats}.
1839
1840     @type exceptions: list
1841     @param exceptions: if passed, list of nodes that should be ignored
1842     @rtype: tuple
1843     @return: tuple of (current, max)
1844
1845     """
1846     return self._UnlockedGetMasterCandidateStats(exceptions)
1847
1848   @locking.ssynchronized(_config_lock)
1849   def MaintainCandidatePool(self, exceptions):
1850     """Try to grow the candidate pool to the desired size.
1851
1852     @type exceptions: list
1853     @param exceptions: if passed, list of nodes that should be ignored
1854     @rtype: list
1855     @return: list with the adjusted nodes (L{objects.Node} instances)
1856
1857     """
1858     mc_now, mc_max, _ = self._UnlockedGetMasterCandidateStats(exceptions)
1859     mod_list = []
1860     if mc_now < mc_max:
1861       node_list = self._config_data.nodes.keys()
1862       random.shuffle(node_list)
1863       for name in node_list:
1864         if mc_now >= mc_max:
1865           break
1866         node = self._config_data.nodes[name]
1867         if (node.master_candidate or node.offline or node.drained or
1868             node.name in exceptions or not node.master_capable):
1869           continue
1870         mod_list.append(node)
1871         node.master_candidate = True
1872         node.serial_no += 1
1873         mc_now += 1
1874       if mc_now != mc_max:
1875         # this should not happen
1876         logging.warning("Warning: MaintainCandidatePool didn't manage to"
1877                         " fill the candidate pool (%d/%d)", mc_now, mc_max)
1878       if mod_list:
1879         self._config_data.cluster.serial_no += 1
1880         self._WriteConfig()
1881
1882     return mod_list
1883
1884   def _UnlockedAddNodeToGroup(self, node_name, nodegroup_uuid):
1885     """Add a given node to the specified group.
1886
1887     """
1888     if nodegroup_uuid not in self._config_data.nodegroups:
1889       # This can happen if a node group gets deleted between its lookup and
1890       # when we're adding the first node to it, since we don't keep a lock in
1891       # the meantime. It's ok though, as we'll fail cleanly if the node group
1892       # is not found anymore.
1893       raise errors.OpExecError("Unknown node group: %s" % nodegroup_uuid)
1894     if node_name not in self._config_data.nodegroups[nodegroup_uuid].members:
1895       self._config_data.nodegroups[nodegroup_uuid].members.append(node_name)
1896
1897   def _UnlockedRemoveNodeFromGroup(self, node):
1898     """Remove a given node from its group.
1899
1900     """
1901     nodegroup = node.group
1902     if nodegroup not in self._config_data.nodegroups:
1903       logging.warning("Warning: node '%s' has unknown node group '%s'"
1904                       " (while being removed from it)", node.name, nodegroup)
1905     nodegroup_obj = self._config_data.nodegroups[nodegroup]
1906     if node.name not in nodegroup_obj.members:
1907       logging.warning("Warning: node '%s' not a member of its node group '%s'"
1908                       " (while being removed from it)", node.name, nodegroup)
1909     else:
1910       nodegroup_obj.members.remove(node.name)
1911
1912   @locking.ssynchronized(_config_lock)
1913   def AssignGroupNodes(self, mods):
1914     """Changes the group of a number of nodes.
1915
1916     @type mods: list of tuples; (node name, new group UUID)
1917     @param mods: Node membership modifications
1918
1919     """
1920     groups = self._config_data.nodegroups
1921     nodes = self._config_data.nodes
1922
1923     resmod = []
1924
1925     # Try to resolve names/UUIDs first
1926     for (node_name, new_group_uuid) in mods:
1927       try:
1928         node = nodes[node_name]
1929       except KeyError:
1930         raise errors.ConfigurationError("Unable to find node '%s'" % node_name)
1931
1932       if node.group == new_group_uuid:
1933         # Node is being assigned to its current group
1934         logging.debug("Node '%s' was assigned to its current group (%s)",
1935                       node_name, node.group)
1936         continue
1937
1938       # Try to find current group of node
1939       try:
1940         old_group = groups[node.group]
1941       except KeyError:
1942         raise errors.ConfigurationError("Unable to find old group '%s'" %
1943                                         node.group)
1944
1945       # Try to find new group for node
1946       try:
1947         new_group = groups[new_group_uuid]
1948       except KeyError:
1949         raise errors.ConfigurationError("Unable to find new group '%s'" %
1950                                         new_group_uuid)
1951
1952       assert node.name in old_group.members, \
1953         ("Inconsistent configuration: node '%s' not listed in members for its"
1954          " old group '%s'" % (node.name, old_group.uuid))
1955       assert node.name not in new_group.members, \
1956         ("Inconsistent configuration: node '%s' already listed in members for"
1957          " its new group '%s'" % (node.name, new_group.uuid))
1958
1959       resmod.append((node, old_group, new_group))
1960
1961     # Apply changes
1962     for (node, old_group, new_group) in resmod:
1963       assert node.uuid != new_group.uuid and old_group.uuid != new_group.uuid, \
1964         "Assigning to current group is not possible"
1965
1966       node.group = new_group.uuid
1967
1968       # Update members of involved groups
1969       if node.name in old_group.members:
1970         old_group.members.remove(node.name)
1971       if node.name not in new_group.members:
1972         new_group.members.append(node.name)
1973
1974     # Update timestamps and serials (only once per node/group object)
1975     now = time.time()
1976     for obj in frozenset(itertools.chain(*resmod)): # pylint: disable=W0142
1977       obj.serial_no += 1
1978       obj.mtime = now
1979
1980     # Force ssconf update
1981     self._config_data.cluster.serial_no += 1
1982
1983     self._WriteConfig()
1984
1985   def _BumpSerialNo(self):
1986     """Bump up the serial number of the config.
1987
1988     """
1989     self._config_data.serial_no += 1
1990     self._config_data.mtime = time.time()
1991
1992   def _AllUUIDObjects(self):
1993     """Returns all objects with uuid attributes.
1994
1995     """
1996     return (self._config_data.instances.values() +
1997             self._config_data.nodes.values() +
1998             self._config_data.nodegroups.values() +
1999             [self._config_data.cluster])
2000
2001   def _OpenConfig(self, accept_foreign):
2002     """Read the config data from disk.
2003
2004     """
2005     raw_data = utils.ReadFile(self._cfg_file)
2006
2007     try:
2008       data = objects.ConfigData.FromDict(serializer.Load(raw_data))
2009     except Exception, err:
2010       raise errors.ConfigurationError(err)
2011
2012     # Make sure the configuration has the right version
2013     _ValidateConfig(data)
2014
2015     if (not hasattr(data, "cluster") or
2016         not hasattr(data.cluster, "rsahostkeypub")):
2017       raise errors.ConfigurationError("Incomplete configuration"
2018                                       " (missing cluster.rsahostkeypub)")
2019
2020     if data.cluster.master_node != self._my_hostname and not accept_foreign:
2021       msg = ("The configuration denotes node %s as master, while my"
2022              " hostname is %s; opening a foreign configuration is only"
2023              " possible in accept_foreign mode" %
2024              (data.cluster.master_node, self._my_hostname))
2025       raise errors.ConfigurationError(msg)
2026
2027     # Upgrade configuration if needed
2028     data.UpgradeConfig()
2029
2030     self._config_data = data
2031     # reset the last serial as -1 so that the next write will cause
2032     # ssconf update
2033     self._last_cluster_serial = -1
2034
2035     # And finally run our (custom) config upgrade sequence
2036     self._UpgradeConfig()
2037
2038     self._cfg_id = utils.GetFileID(path=self._cfg_file)
2039
2040   def _UpgradeConfig(self):
2041     """Run upgrade steps that cannot be done purely in the objects.
2042
2043     This is because some data elements need uniqueness across the
2044     whole configuration, etc.
2045
2046     @warning: this function will call L{_WriteConfig()}, but also
2047         L{DropECReservations} so it needs to be called only from a
2048         "safe" place (the constructor). If one wanted to call it with
2049         the lock held, a DropECReservationUnlocked would need to be
2050         created first, to avoid causing deadlock.
2051
2052     """
2053     modified = False
2054     for item in self._AllUUIDObjects():
2055       if item.uuid is None:
2056         item.uuid = self._GenerateUniqueID(_UPGRADE_CONFIG_JID)
2057         modified = True
2058     if not self._config_data.nodegroups:
2059       default_nodegroup_name = constants.INITIAL_NODE_GROUP_NAME
2060       default_nodegroup = objects.NodeGroup(name=default_nodegroup_name,
2061                                             members=[])
2062       self._UnlockedAddNodeGroup(default_nodegroup, _UPGRADE_CONFIG_JID, True)
2063       modified = True
2064     for node in self._config_data.nodes.values():
2065       if not node.group:
2066         node.group = self.LookupNodeGroup(None)
2067         modified = True
2068       # This is technically *not* an upgrade, but needs to be done both when
2069       # nodegroups are being added, and upon normally loading the config,
2070       # because the members list of a node group is discarded upon
2071       # serializing/deserializing the object.
2072       self._UnlockedAddNodeToGroup(node.name, node.group)
2073     if modified:
2074       self._WriteConfig()
2075       # This is ok even if it acquires the internal lock, as _UpgradeConfig is
2076       # only called at config init time, without the lock held
2077       self.DropECReservations(_UPGRADE_CONFIG_JID)
2078
2079   def _DistributeConfig(self, feedback_fn):
2080     """Distribute the configuration to the other nodes.
2081
2082     Currently, this only copies the configuration file. In the future,
2083     it could be used to encapsulate the 2/3-phase update mechanism.
2084
2085     """
2086     if self._offline:
2087       return True
2088
2089     bad = False
2090
2091     node_list = []
2092     addr_list = []
2093     myhostname = self._my_hostname
2094     # we can skip checking whether _UnlockedGetNodeInfo returns None
2095     # since the node list comes from _UnlocketGetNodeList, and we are
2096     # called with the lock held, so no modifications should take place
2097     # in between
2098     for node_name in self._UnlockedGetNodeList():
2099       if node_name == myhostname:
2100         continue
2101       node_info = self._UnlockedGetNodeInfo(node_name)
2102       if not node_info.master_candidate:
2103         continue
2104       node_list.append(node_info.name)
2105       addr_list.append(node_info.primary_ip)
2106
2107     # TODO: Use dedicated resolver talking to config writer for name resolution
2108     result = \
2109       self._GetRpc(addr_list).call_upload_file(node_list, self._cfg_file)
2110     for to_node, to_result in result.items():
2111       msg = to_result.fail_msg
2112       if msg:
2113         msg = ("Copy of file %s to node %s failed: %s" %
2114                (self._cfg_file, to_node, msg))
2115         logging.error(msg)
2116
2117         if feedback_fn:
2118           feedback_fn(msg)
2119
2120         bad = True
2121
2122     return not bad
2123
2124   def _WriteConfig(self, destination=None, feedback_fn=None):
2125     """Write the configuration data to persistent storage.
2126
2127     """
2128     assert feedback_fn is None or callable(feedback_fn)
2129
2130     # Warn on config errors, but don't abort the save - the
2131     # configuration has already been modified, and we can't revert;
2132     # the best we can do is to warn the user and save as is, leaving
2133     # recovery to the user
2134     config_errors = self._UnlockedVerifyConfig()
2135     if config_errors:
2136       errmsg = ("Configuration data is not consistent: %s" %
2137                 (utils.CommaJoin(config_errors)))
2138       logging.critical(errmsg)
2139       if feedback_fn:
2140         feedback_fn(errmsg)
2141
2142     if destination is None:
2143       destination = self._cfg_file
2144     self._BumpSerialNo()
2145     txt = serializer.Dump(self._config_data.ToDict())
2146
2147     getents = self._getents()
2148     try:
2149       fd = utils.SafeWriteFile(destination, self._cfg_id, data=txt,
2150                                close=False, gid=getents.confd_gid, mode=0640)
2151     except errors.LockError:
2152       raise errors.ConfigurationError("The configuration file has been"
2153                                       " modified since the last write, cannot"
2154                                       " update")
2155     try:
2156       self._cfg_id = utils.GetFileID(fd=fd)
2157     finally:
2158       os.close(fd)
2159
2160     self.write_count += 1
2161
2162     # and redistribute the config file to master candidates
2163     self._DistributeConfig(feedback_fn)
2164
2165     # Write ssconf files on all nodes (including locally)
2166     if self._last_cluster_serial < self._config_data.cluster.serial_no:
2167       if not self._offline:
2168         result = self._GetRpc(None).call_write_ssconf_files(
2169           self._UnlockedGetOnlineNodeList(),
2170           self._UnlockedGetSsconfValues())
2171
2172         for nname, nresu in result.items():
2173           msg = nresu.fail_msg
2174           if msg:
2175             errmsg = ("Error while uploading ssconf files to"
2176                       " node %s: %s" % (nname, msg))
2177             logging.warning(errmsg)
2178
2179             if feedback_fn:
2180               feedback_fn(errmsg)
2181
2182       self._last_cluster_serial = self._config_data.cluster.serial_no
2183
2184   def _UnlockedGetSsconfValues(self):
2185     """Return the values needed by ssconf.
2186
2187     @rtype: dict
2188     @return: a dictionary with keys the ssconf names and values their
2189         associated value
2190
2191     """
2192     fn = "\n".join
2193     instance_names = utils.NiceSort(self._UnlockedGetInstanceList())
2194     node_names = utils.NiceSort(self._UnlockedGetNodeList())
2195     node_info = [self._UnlockedGetNodeInfo(name) for name in node_names]
2196     node_pri_ips = ["%s %s" % (ninfo.name, ninfo.primary_ip)
2197                     for ninfo in node_info]
2198     node_snd_ips = ["%s %s" % (ninfo.name, ninfo.secondary_ip)
2199                     for ninfo in node_info]
2200
2201     instance_data = fn(instance_names)
2202     off_data = fn(node.name for node in node_info if node.offline)
2203     on_data = fn(node.name for node in node_info if not node.offline)
2204     mc_data = fn(node.name for node in node_info if node.master_candidate)
2205     mc_ips_data = fn(node.primary_ip for node in node_info
2206                      if node.master_candidate)
2207     node_data = fn(node_names)
2208     node_pri_ips_data = fn(node_pri_ips)
2209     node_snd_ips_data = fn(node_snd_ips)
2210
2211     cluster = self._config_data.cluster
2212     cluster_tags = fn(cluster.GetTags())
2213
2214     hypervisor_list = fn(cluster.enabled_hypervisors)
2215
2216     uid_pool = uidpool.FormatUidPool(cluster.uid_pool, separator="\n")
2217
2218     nodegroups = ["%s %s" % (nodegroup.uuid, nodegroup.name) for nodegroup in
2219                   self._config_data.nodegroups.values()]
2220     nodegroups_data = fn(utils.NiceSort(nodegroups))
2221     networks = ["%s %s" % (net.uuid, net.name) for net in
2222                 self._config_data.networks.values()]
2223     networks_data = fn(utils.NiceSort(networks))
2224
2225     ssconf_values = {
2226       constants.SS_CLUSTER_NAME: cluster.cluster_name,
2227       constants.SS_CLUSTER_TAGS: cluster_tags,
2228       constants.SS_FILE_STORAGE_DIR: cluster.file_storage_dir,
2229       constants.SS_SHARED_FILE_STORAGE_DIR: cluster.shared_file_storage_dir,
2230       constants.SS_MASTER_CANDIDATES: mc_data,
2231       constants.SS_MASTER_CANDIDATES_IPS: mc_ips_data,
2232       constants.SS_MASTER_IP: cluster.master_ip,
2233       constants.SS_MASTER_NETDEV: cluster.master_netdev,
2234       constants.SS_MASTER_NETMASK: str(cluster.master_netmask),
2235       constants.SS_MASTER_NODE: cluster.master_node,
2236       constants.SS_NODE_LIST: node_data,
2237       constants.SS_NODE_PRIMARY_IPS: node_pri_ips_data,
2238       constants.SS_NODE_SECONDARY_IPS: node_snd_ips_data,
2239       constants.SS_OFFLINE_NODES: off_data,
2240       constants.SS_ONLINE_NODES: on_data,
2241       constants.SS_PRIMARY_IP_FAMILY: str(cluster.primary_ip_family),
2242       constants.SS_INSTANCE_LIST: instance_data,
2243       constants.SS_RELEASE_VERSION: constants.RELEASE_VERSION,
2244       constants.SS_HYPERVISOR_LIST: hypervisor_list,
2245       constants.SS_MAINTAIN_NODE_HEALTH: str(cluster.maintain_node_health),
2246       constants.SS_UID_POOL: uid_pool,
2247       constants.SS_NODEGROUPS: nodegroups_data,
2248       constants.SS_NETWORKS: networks_data,
2249       }
2250     bad_values = [(k, v) for k, v in ssconf_values.items()
2251                   if not isinstance(v, (str, basestring))]
2252     if bad_values:
2253       err = utils.CommaJoin("%s=%s" % (k, v) for k, v in bad_values)
2254       raise errors.ConfigurationError("Some ssconf key(s) have non-string"
2255                                       " values: %s" % err)
2256     return ssconf_values
2257
2258   @locking.ssynchronized(_config_lock, shared=1)
2259   def GetSsconfValues(self):
2260     """Wrapper using lock around _UnlockedGetSsconf().
2261
2262     """
2263     return self._UnlockedGetSsconfValues()
2264
2265   @locking.ssynchronized(_config_lock, shared=1)
2266   def GetVGName(self):
2267     """Return the volume group name.
2268
2269     """
2270     return self._config_data.cluster.volume_group_name
2271
2272   @locking.ssynchronized(_config_lock)
2273   def SetVGName(self, vg_name):
2274     """Set the volume group name.
2275
2276     """
2277     self._config_data.cluster.volume_group_name = vg_name
2278     self._config_data.cluster.serial_no += 1
2279     self._WriteConfig()
2280
2281   @locking.ssynchronized(_config_lock, shared=1)
2282   def GetDRBDHelper(self):
2283     """Return DRBD usermode helper.
2284
2285     """
2286     return self._config_data.cluster.drbd_usermode_helper
2287
2288   @locking.ssynchronized(_config_lock)
2289   def SetDRBDHelper(self, drbd_helper):
2290     """Set DRBD usermode helper.
2291
2292     """
2293     self._config_data.cluster.drbd_usermode_helper = drbd_helper
2294     self._config_data.cluster.serial_no += 1
2295     self._WriteConfig()
2296
2297   @locking.ssynchronized(_config_lock, shared=1)
2298   def GetMACPrefix(self):
2299     """Return the mac prefix.
2300
2301     """
2302     return self._config_data.cluster.mac_prefix
2303
2304   @locking.ssynchronized(_config_lock, shared=1)
2305   def GetClusterInfo(self):
2306     """Returns information about the cluster
2307
2308     @rtype: L{objects.Cluster}
2309     @return: the cluster object
2310
2311     """
2312     return self._config_data.cluster
2313
2314   @locking.ssynchronized(_config_lock, shared=1)
2315   def HasAnyDiskOfType(self, dev_type):
2316     """Check if in there is at disk of the given type in the configuration.
2317
2318     """
2319     return self._config_data.HasAnyDiskOfType(dev_type)
2320
2321   @locking.ssynchronized(_config_lock)
2322   def Update(self, target, feedback_fn, ec_id=None):
2323     """Notify function to be called after updates.
2324
2325     This function must be called when an object (as returned by
2326     GetInstanceInfo, GetNodeInfo, GetCluster) has been updated and the
2327     caller wants the modifications saved to the backing store. Note
2328     that all modified objects will be saved, but the target argument
2329     is the one the caller wants to ensure that it's saved.
2330
2331     @param target: an instance of either L{objects.Cluster},
2332         L{objects.Node} or L{objects.Instance} which is existing in
2333         the cluster
2334     @param feedback_fn: Callable feedback function
2335
2336     """
2337     if self._config_data is None:
2338       raise errors.ProgrammerError("Configuration file not read,"
2339                                    " cannot save.")
2340     update_serial = False
2341     if isinstance(target, objects.Cluster):
2342       test = target == self._config_data.cluster
2343     elif isinstance(target, objects.Node):
2344       test = target in self._config_data.nodes.values()
2345       update_serial = True
2346     elif isinstance(target, objects.Instance):
2347       test = target in self._config_data.instances.values()
2348     elif isinstance(target, objects.NodeGroup):
2349       test = target in self._config_data.nodegroups.values()
2350     elif isinstance(target, objects.Network):
2351       test = target in self._config_data.networks.values()
2352     else:
2353       raise errors.ProgrammerError("Invalid object type (%s) passed to"
2354                                    " ConfigWriter.Update" % type(target))
2355     if not test:
2356       raise errors.ConfigurationError("Configuration updated since object"
2357                                       " has been read or unknown object")
2358     target.serial_no += 1
2359     target.mtime = now = time.time()
2360
2361     if update_serial:
2362       # for node updates, we need to increase the cluster serial too
2363       self._config_data.cluster.serial_no += 1
2364       self._config_data.cluster.mtime = now
2365
2366     if isinstance(target, objects.Instance):
2367       self._UnlockedReleaseDRBDMinors(target.name)
2368
2369     if ec_id is not None:
2370       # Commit all ips reserved by OpInstanceSetParams and OpGroupSetParams
2371       self._UnlockedCommitTemporaryIps(ec_id)
2372
2373     self._WriteConfig(feedback_fn=feedback_fn)
2374
2375   @locking.ssynchronized(_config_lock)
2376   def DropECReservations(self, ec_id):
2377     """Drop per-execution-context reservations
2378
2379     """
2380     for rm in self._all_rms:
2381       rm.DropECReservations(ec_id)
2382
2383   @locking.ssynchronized(_config_lock, shared=1)
2384   def GetAllNetworksInfo(self):
2385     """Get the configuration of all networks
2386
2387     """
2388     return dict(self._config_data.networks)
2389
2390   def _UnlockedGetNetworkList(self):
2391     """Get the list of networks.
2392
2393     This function is for internal use, when the config lock is already held.
2394
2395     """
2396     return self._config_data.networks.keys()
2397
2398   @locking.ssynchronized(_config_lock, shared=1)
2399   def GetNetworkList(self):
2400     """Get the list of networks.
2401
2402     @return: array of networks, ex. ["main", "vlan100", "200]
2403
2404     """
2405     return self._UnlockedGetNetworkList()
2406
2407   @locking.ssynchronized(_config_lock, shared=1)
2408   def GetNetworkNames(self):
2409     """Get a list of network names
2410
2411     """
2412     names = [network.name
2413              for network in self._config_data.networks.values()]
2414     return names
2415
2416   def _UnlockedGetNetwork(self, uuid):
2417     """Returns information about a network.
2418
2419     This function is for internal use, when the config lock is already held.
2420
2421     """
2422     if uuid not in self._config_data.networks:
2423       return None
2424
2425     return self._config_data.networks[uuid]
2426
2427   @locking.ssynchronized(_config_lock, shared=1)
2428   def GetNetwork(self, uuid):
2429     """Returns information about a network.
2430
2431     It takes the information from the configuration file.
2432
2433     @param uuid: UUID of the network
2434
2435     @rtype: L{objects.Network}
2436     @return: the network object
2437
2438     """
2439     return self._UnlockedGetNetwork(uuid)
2440
2441   @locking.ssynchronized(_config_lock)
2442   def AddNetwork(self, net, ec_id, check_uuid=True):
2443     """Add a network to the configuration.
2444
2445     @type net: L{objects.Network}
2446     @param net: the Network object to add
2447     @type ec_id: string
2448     @param ec_id: unique id for the job to use when creating a missing UUID
2449
2450     """
2451     self._UnlockedAddNetwork(net, ec_id, check_uuid)
2452     self._WriteConfig()
2453
2454   def _UnlockedAddNetwork(self, net, ec_id, check_uuid):
2455     """Add a network to the configuration.
2456
2457     """
2458     logging.info("Adding network %s to configuration", net.name)
2459
2460     if check_uuid:
2461       self._EnsureUUID(net, ec_id)
2462
2463     existing_uuid = self._UnlockedLookupNetwork(net.name)
2464     if existing_uuid:
2465       raise errors.OpPrereqError("Desired network name '%s' already"
2466                                  " exists as a network (UUID: %s)" %
2467                                  (net.name, existing_uuid),
2468                                  errors.ECODE_EXISTS)
2469     net.serial_no = 1
2470     self._config_data.networks[net.uuid] = net
2471     self._config_data.cluster.serial_no += 1
2472
2473   def _UnlockedLookupNetwork(self, target):
2474     """Lookup a network's UUID.
2475
2476     @type target: string
2477     @param target: network name or UUID
2478     @rtype: string
2479     @return: network UUID
2480     @raises errors.OpPrereqError: when the target network cannot be found
2481
2482     """
2483     if target in self._config_data.networks:
2484       return target
2485     for net in self._config_data.networks.values():
2486       if net.name == target:
2487         return net.uuid
2488     return None
2489
2490   @locking.ssynchronized(_config_lock, shared=1)
2491   def LookupNetwork(self, target):
2492     """Lookup a network's UUID.
2493
2494     This function is just a wrapper over L{_UnlockedLookupNetwork}.
2495
2496     @type target: string
2497     @param target: network name or UUID
2498     @rtype: string
2499     @return: network UUID
2500
2501     """
2502     return self._UnlockedLookupNetwork(target)
2503
2504   @locking.ssynchronized(_config_lock)
2505   def RemoveNetwork(self, network_uuid):
2506     """Remove a network from the configuration.
2507
2508     @type network_uuid: string
2509     @param network_uuid: the UUID of the network to remove
2510
2511     """
2512     logging.info("Removing network %s from configuration", network_uuid)
2513
2514     if network_uuid not in self._config_data.networks:
2515       raise errors.ConfigurationError("Unknown network '%s'" % network_uuid)
2516
2517     del self._config_data.networks[network_uuid]
2518     self._config_data.cluster.serial_no += 1
2519     self._WriteConfig()
2520
2521   def _UnlockedGetGroupNetParams(self, net, node):
2522     """Get the netparams (mode, link) of a network.
2523
2524     Get a network's netparams for a given node.
2525
2526     @type net: string
2527     @param net: network name
2528     @type node: string
2529     @param node: node name
2530     @rtype: dict or None
2531     @return: netparams
2532
2533     """
2534     net_uuid = self._UnlockedLookupNetwork(net)
2535     if net_uuid is None:
2536       return None
2537
2538     node_info = self._UnlockedGetNodeInfo(node)
2539     nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2540     netparams = nodegroup_info.networks.get(net_uuid, None)
2541
2542     return netparams
2543
2544   @locking.ssynchronized(_config_lock, shared=1)
2545   def GetGroupNetParams(self, net, node):
2546     """Locking wrapper of _UnlockedGetGroupNetParams()
2547
2548     """
2549     return self._UnlockedGetGroupNetParams(net, node)
2550
2551
2552   @locking.ssynchronized(_config_lock, shared=1)
2553   def CheckIPInNodeGroup(self, ip, node):
2554     """Check for conflictig IP.
2555
2556     @type ip: string
2557     @param ip: ip address
2558     @type node: string
2559     @param node: node name
2560     @rtype: (string, dict) or (None, None)
2561     @return: (network name, netparams)
2562
2563     """
2564     if ip is None:
2565       return (None, None)
2566     node_info = self._UnlockedGetNodeInfo(node)
2567     nodegroup_info = self._UnlockedGetNodeGroup(node_info.group)
2568     for net_uuid in nodegroup_info.networks.keys():
2569       net_info = self._UnlockedGetNetwork(net_uuid)
2570       pool = network.AddressPool(net_info)
2571       if pool._Contains(ip):
2572         return (net_info.name, nodegroup_info.networks[net_uuid])
2573
2574     return (None, None)