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