Statistics
| Branch: | Tag: | Revision:

root / lib / masterd / iallocator.py @ 1c3231aa

History | View | Annotate | Download (21.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 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
"""Module implementing the iallocator code."""
23

    
24
from ganeti import compat
25
from ganeti import constants
26
from ganeti import errors
27
from ganeti import ht
28
from ganeti import outils
29
from ganeti import opcodes
30
from ganeti import rpc
31
from ganeti import serializer
32
from ganeti import utils
33

    
34
import ganeti.masterd.instance as gmi
35

    
36

    
37
_STRING_LIST = ht.TListOf(ht.TString)
38
_JOB_LIST = ht.TListOf(ht.TListOf(ht.TStrictDict(True, False, {
39
   # pylint: disable=E1101
40
   # Class '...' has no 'OP_ID' member
41
   "OP_ID": ht.TElemOf([opcodes.OpInstanceFailover.OP_ID,
42
                        opcodes.OpInstanceMigrate.OP_ID,
43
                        opcodes.OpInstanceReplaceDisks.OP_ID]),
44
   })))
45

    
46
_NEVAC_MOVED = \
47
  ht.TListOf(ht.TAnd(ht.TIsLength(3),
48
                     ht.TItems([ht.TNonEmptyString,
49
                                ht.TNonEmptyString,
50
                                ht.TListOf(ht.TNonEmptyString),
51
                                ])))
52
_NEVAC_FAILED = \
53
  ht.TListOf(ht.TAnd(ht.TIsLength(2),
54
                     ht.TItems([ht.TNonEmptyString,
55
                                ht.TMaybeString,
56
                                ])))
57
_NEVAC_RESULT = ht.TAnd(ht.TIsLength(3),
58
                        ht.TItems([_NEVAC_MOVED, _NEVAC_FAILED, _JOB_LIST]))
59

    
60
_INST_NAME = ("name", ht.TNonEmptyString)
61

    
62

    
63
class _AutoReqParam(outils.AutoSlots):
64
  """Meta class for request definitions.
65

66
  """
67
  @classmethod
68
  def _GetSlots(mcs, attrs):
69
    """Extract the slots out of REQ_PARAMS.
70

71
    """
72
    params = attrs.setdefault("REQ_PARAMS", [])
73
    return [slot for (slot, _) in params]
74

    
75

    
76
class IARequestBase(outils.ValidatedSlots):
77
  """A generic IAllocator request object.
78

79
  """
80
  __metaclass__ = _AutoReqParam
81

    
82
  MODE = NotImplemented
83
  REQ_PARAMS = []
84
  REQ_RESULT = NotImplemented
85

    
86
  def __init__(self, **kwargs):
87
    """Constructor for IARequestBase.
88

89
    The constructor takes only keyword arguments and will set
90
    attributes on this object based on the passed arguments. As such,
91
    it means that you should not pass arguments which are not in the
92
    REQ_PARAMS attribute for this class.
93

94
    """
95
    outils.ValidatedSlots.__init__(self, **kwargs)
96

    
97
    self.Validate()
98

    
99
  def Validate(self):
100
    """Validates all parameters of the request.
101

102
    """
103
    assert self.MODE in constants.VALID_IALLOCATOR_MODES
104

    
105
    for (param, validator) in self.REQ_PARAMS:
106
      if not hasattr(self, param):
107
        raise errors.OpPrereqError("Request is missing '%s' parameter" % param,
108
                                   errors.ECODE_INVAL)
109

    
110
      value = getattr(self, param)
111
      if not validator(value):
112
        raise errors.OpPrereqError(("Request parameter '%s' has invalid"
113
                                    " type %s/value %s") %
114
                                    (param, type(value), value),
115
                                    errors.ECODE_INVAL)
116

    
117
  def GetRequest(self, cfg):
118
    """Gets the request data dict.
119

120
    @param cfg: The configuration instance
121

122
    """
123
    raise NotImplementedError
124

    
125
  def ValidateResult(self, ia, result):
126
    """Validates the result of an request.
127

128
    @param ia: The IAllocator instance
129
    @param result: The IAllocator run result
130
    @raises ResultValidationError: If validation fails
131

132
    """
133
    if ia.success and not self.REQ_RESULT(result):
134
      raise errors.ResultValidationError("iallocator returned invalid result,"
135
                                         " expected %s, got %s" %
136
                                         (self.REQ_RESULT, result))
137

    
138

    
139
class IAReqInstanceAlloc(IARequestBase):
140
  """An instance allocation request.
141

142
  """
143
  # pylint: disable=E1101
144
  MODE = constants.IALLOCATOR_MODE_ALLOC
145
  REQ_PARAMS = [
146
    _INST_NAME,
147
    ("memory", ht.TNonNegativeInt),
148
    ("spindle_use", ht.TNonNegativeInt),
149
    ("disks", ht.TListOf(ht.TDict)),
150
    ("disk_template", ht.TString),
151
    ("os", ht.TString),
152
    ("tags", _STRING_LIST),
153
    ("nics", ht.TListOf(ht.TDict)),
154
    ("vcpus", ht.TInt),
155
    ("hypervisor", ht.TString),
156
    ("node_whitelist", ht.TMaybeListOf(ht.TNonEmptyString)),
157
    ]
158
  REQ_RESULT = ht.TList
159

    
160
  def RequiredNodes(self):
161
    """Calculates the required nodes based on the disk_template.
162

163
    """
164
    if self.disk_template in constants.DTS_INT_MIRROR:
165
      return 2
166
    else:
167
      return 1
168

    
169
  def GetRequest(self, cfg):
170
    """Requests a new instance.
171

172
    The checks for the completeness of the opcode must have already been
173
    done.
174

175
    """
176
    disk_space = gmi.ComputeDiskSize(self.disk_template, self.disks)
177

    
178
    return {
179
      "name": self.name,
180
      "disk_template": self.disk_template,
181
      "tags": self.tags,
182
      "os": self.os,
183
      "vcpus": self.vcpus,
184
      "memory": self.memory,
185
      "spindle_use": self.spindle_use,
186
      "disks": self.disks,
187
      "disk_space_total": disk_space,
188
      "nics": self.nics,
189
      "required_nodes": self.RequiredNodes(),
190
      "hypervisor": self.hypervisor,
191
      }
192

    
193
  def ValidateResult(self, ia, result):
194
    """Validates an single instance allocation request.
195

196
    """
197
    IARequestBase.ValidateResult(self, ia, result)
198

    
199
    if ia.success and len(result) != self.RequiredNodes():
200
      raise errors.ResultValidationError("iallocator returned invalid number"
201
                                         " of nodes (%s), required %s" %
202
                                         (len(result), self.RequiredNodes()))
203

    
204

    
205
class IAReqMultiInstanceAlloc(IARequestBase):
206
  """An multi instance allocation request.
207

208
  """
209
  # pylint: disable=E1101
210
  MODE = constants.IALLOCATOR_MODE_MULTI_ALLOC
211
  REQ_PARAMS = [
212
    ("instances", ht.TListOf(ht.TInstanceOf(IAReqInstanceAlloc))),
213
    ]
214
  _MASUCCESS = \
215
    ht.TListOf(ht.TAnd(ht.TIsLength(2),
216
                       ht.TItems([ht.TNonEmptyString,
217
                                  ht.TListOf(ht.TNonEmptyString),
218
                                  ])))
219
  _MAFAILED = ht.TListOf(ht.TNonEmptyString)
220
  REQ_RESULT = ht.TAnd(ht.TList, ht.TIsLength(2),
221
                       ht.TItems([_MASUCCESS, _MAFAILED]))
222

    
223
  def GetRequest(self, cfg):
224
    return {
225
      "instances": [iareq.GetRequest(cfg) for iareq in self.instances],
226
      }
227

    
228

    
229
class IAReqRelocate(IARequestBase):
230
  """A relocation request.
231

232
  """
233
  # pylint: disable=E1101
234
  MODE = constants.IALLOCATOR_MODE_RELOC
235
  REQ_PARAMS = [
236
    _INST_NAME,
237
    ("relocate_from_node_uuids", _STRING_LIST),
238
    ]
239
  REQ_RESULT = ht.TList
240

    
241
  def GetRequest(self, cfg):
242
    """Request an relocation of an instance
243

244
    The checks for the completeness of the opcode must have already been
245
    done.
246

247
    """
248
    instance = cfg.GetInstanceInfo(self.name)
249
    if instance is None:
250
      raise errors.ProgrammerError("Unknown instance '%s' passed to"
251
                                   " IAllocator" % self.name)
252

    
253
    if instance.disk_template not in constants.DTS_MIRRORED:
254
      raise errors.OpPrereqError("Can't relocate non-mirrored instances",
255
                                 errors.ECODE_INVAL)
256

    
257
    if (instance.disk_template in constants.DTS_INT_MIRROR and
258
        len(instance.secondary_nodes) != 1):
259
      raise errors.OpPrereqError("Instance has not exactly one secondary node",
260
                                 errors.ECODE_STATE)
261

    
262
    disk_sizes = [{constants.IDISK_SIZE: disk.size} for disk in instance.disks]
263
    disk_space = gmi.ComputeDiskSize(instance.disk_template, disk_sizes)
264

    
265
    return {
266
      "name": self.name,
267
      "disk_space_total": disk_space,
268
      "required_nodes": 1,
269
      "relocate_from": cfg.GetNodeNames(self.relocate_from_node_uuids),
270
      }
271

    
272
  def ValidateResult(self, ia, result):
273
    """Validates the result of an relocation request.
274

275
    """
276
    IARequestBase.ValidateResult(self, ia, result)
277

    
278
    node2group = dict((name, ndata["group"])
279
                      for (name, ndata) in ia.in_data["nodes"].items())
280

    
281
    fn = compat.partial(self._NodesToGroups, node2group,
282
                        ia.in_data["nodegroups"])
283

    
284
    instance = ia.cfg.GetInstanceInfo(self.name)
285
    request_groups = fn(ia.cfg.GetNodeNames(self.relocate_from_node_uuids) +
286
                        ia.cfg.GetNodeNames([instance.primary_node]))
287
    result_groups = fn(result + ia.cfg.GetNodeNames([instance.primary_node]))
288

    
289
    if ia.success and not set(result_groups).issubset(request_groups):
290
      raise errors.ResultValidationError("Groups of nodes returned by"
291
                                         " iallocator (%s) differ from original"
292
                                         " groups (%s)" %
293
                                         (utils.CommaJoin(result_groups),
294
                                          utils.CommaJoin(request_groups)))
295

    
296
  @staticmethod
297
  def _NodesToGroups(node2group, groups, nodes):
298
    """Returns a list of unique group names for a list of nodes.
299

300
    @type node2group: dict
301
    @param node2group: Map from node name to group UUID
302
    @type groups: dict
303
    @param groups: Group information
304
    @type nodes: list
305
    @param nodes: Node names
306

307
    """
308
    result = set()
309

    
310
    for node in nodes:
311
      try:
312
        group_uuid = node2group[node]
313
      except KeyError:
314
        # Ignore unknown node
315
        pass
316
      else:
317
        try:
318
          group = groups[group_uuid]
319
        except KeyError:
320
          # Can't find group, let's use UUID
321
          group_name = group_uuid
322
        else:
323
          group_name = group["name"]
324

    
325
        result.add(group_name)
326

    
327
    return sorted(result)
328

    
329

    
330
class IAReqNodeEvac(IARequestBase):
331
  """A node evacuation request.
332

333
  """
334
  # pylint: disable=E1101
335
  MODE = constants.IALLOCATOR_MODE_NODE_EVAC
336
  REQ_PARAMS = [
337
    ("instances", _STRING_LIST),
338
    ("evac_mode", ht.TElemOf(constants.IALLOCATOR_NEVAC_MODES)),
339
    ]
340
  REQ_RESULT = _NEVAC_RESULT
341

    
342
  def GetRequest(self, cfg):
343
    """Get data for node-evacuate requests.
344

345
    """
346
    return {
347
      "instances": self.instances,
348
      "evac_mode": self.evac_mode,
349
      }
350

    
351

    
352
class IAReqGroupChange(IARequestBase):
353
  """A group change request.
354

355
  """
356
  # pylint: disable=E1101
357
  MODE = constants.IALLOCATOR_MODE_CHG_GROUP
358
  REQ_PARAMS = [
359
    ("instances", _STRING_LIST),
360
    ("target_groups", _STRING_LIST),
361
    ]
362
  REQ_RESULT = _NEVAC_RESULT
363

    
364
  def GetRequest(self, cfg):
365
    """Get data for node-evacuate requests.
366

367
    """
368
    return {
369
      "instances": self.instances,
370
      "target_groups": self.target_groups,
371
      }
372

    
373

    
374
class IAllocator(object):
375
  """IAllocator framework.
376

377
  An IAllocator instance has three sets of attributes:
378
    - cfg that is needed to query the cluster
379
    - input data (all members of the _KEYS class attribute are required)
380
    - four buffer attributes (in|out_data|text), that represent the
381
      input (to the external script) in text and data structure format,
382
      and the output from it, again in two formats
383
    - the result variables from the script (success, info, nodes) for
384
      easy usage
385

386
  """
387
  # pylint: disable=R0902
388
  # lots of instance attributes
389

    
390
  def __init__(self, cfg, rpc_runner, req):
391
    self.cfg = cfg
392
    self.rpc = rpc_runner
393
    self.req = req
394
    # init buffer variables
395
    self.in_text = self.out_text = self.in_data = self.out_data = None
396
    # init result fields
397
    self.success = self.info = self.result = None
398

    
399
    self._BuildInputData(req)
400

    
401
  def _ComputeClusterData(self):
402
    """Compute the generic allocator input data.
403

404
    This is the data that is independent of the actual operation.
405

406
    """
407
    cfg = self.cfg
408
    cluster_info = cfg.GetClusterInfo()
409
    # cluster data
410
    data = {
411
      "version": constants.IALLOCATOR_VERSION,
412
      "cluster_name": cfg.GetClusterName(),
413
      "cluster_tags": list(cluster_info.GetTags()),
414
      "enabled_hypervisors": list(cluster_info.enabled_hypervisors),
415
      "ipolicy": cluster_info.ipolicy,
416
      }
417
    ninfo = cfg.GetAllNodesInfo()
418
    iinfo = cfg.GetAllInstancesInfo().values()
419
    i_list = [(inst, cluster_info.FillBE(inst)) for inst in iinfo]
420

    
421
    # node data
422
    node_list = [n.uuid for n in ninfo.values() if n.vm_capable]
423

    
424
    if isinstance(self.req, IAReqInstanceAlloc):
425
      hypervisor_name = self.req.hypervisor
426
      node_whitelist = self.req.node_whitelist
427
    elif isinstance(self.req, IAReqRelocate):
428
      hypervisor_name = cfg.GetInstanceInfo(self.req.name).hypervisor
429
      node_whitelist = None
430
    else:
431
      hypervisor_name = cluster_info.primary_hypervisor
432
      node_whitelist = None
433

    
434
    es_flags = rpc.GetExclusiveStorageForNodes(cfg, node_list)
435
    vg_req = rpc.BuildVgInfoQuery(cfg)
436
    has_lvm = bool(vg_req)
437
    hvspecs = [(hypervisor_name, cluster_info.hvparams[hypervisor_name])]
438
    node_data = self.rpc.call_node_info(node_list, vg_req,
439
                                        hvspecs, es_flags)
440
    node_iinfo = \
441
      self.rpc.call_all_instances_info(node_list,
442
                                       cluster_info.enabled_hypervisors,
443
                                       cluster_info.hvparams)
444

    
445
    data["nodegroups"] = self._ComputeNodeGroupData(cfg)
446

    
447
    config_ndata = self._ComputeBasicNodeData(cfg, ninfo, node_whitelist)
448
    data["nodes"] = self._ComputeDynamicNodeData(ninfo, node_data, node_iinfo,
449
                                                 i_list, config_ndata, has_lvm)
450
    assert len(data["nodes"]) == len(ninfo), \
451
        "Incomplete node data computed"
452

    
453
    data["instances"] = self._ComputeInstanceData(cfg, cluster_info, i_list)
454

    
455
    self.in_data = data
456

    
457
  @staticmethod
458
  def _ComputeNodeGroupData(cfg):
459
    """Compute node groups data.
460

461
    """
462
    cluster = cfg.GetClusterInfo()
463
    ng = dict((guuid, {
464
      "name": gdata.name,
465
      "alloc_policy": gdata.alloc_policy,
466
      "networks": [net_uuid for net_uuid, _ in gdata.networks.items()],
467
      "ipolicy": gmi.CalculateGroupIPolicy(cluster, gdata),
468
      "tags": list(gdata.GetTags()),
469
      })
470
      for guuid, gdata in cfg.GetAllNodeGroupsInfo().items())
471

    
472
    return ng
473

    
474
  @staticmethod
475
  def _ComputeBasicNodeData(cfg, node_cfg, node_whitelist):
476
    """Compute global node data.
477

478
    @rtype: dict
479
    @returns: a dict of name: (node dict, node config)
480

481
    """
482
    # fill in static (config-based) values
483
    node_results = dict((ninfo.name, {
484
      "tags": list(ninfo.GetTags()),
485
      "primary_ip": ninfo.primary_ip,
486
      "secondary_ip": ninfo.secondary_ip,
487
      "offline": (ninfo.offline or
488
                  not (node_whitelist is None or
489
                       ninfo.name in node_whitelist)),
490
      "drained": ninfo.drained,
491
      "master_candidate": ninfo.master_candidate,
492
      "group": ninfo.group,
493
      "master_capable": ninfo.master_capable,
494
      "vm_capable": ninfo.vm_capable,
495
      "ndparams": cfg.GetNdParams(ninfo),
496
      })
497
      for ninfo in node_cfg.values())
498

    
499
    return node_results
500

    
501
  @staticmethod
502
  def _ComputeDynamicNodeData(node_cfg, node_data, node_iinfo, i_list,
503
                              node_results, has_lvm):
504
    """Compute global node data.
505

506
    @param node_results: the basic node structures as filled from the config
507

508
    """
509
    #TODO(dynmem): compute the right data on MAX and MIN memory
510
    # make a copy of the current dict
511
    node_results = dict(node_results)
512
    for nuuid, nresult in node_data.items():
513
      ninfo = node_cfg[nuuid]
514
      assert ninfo.name in node_results, "Missing basic data for node %s" % \
515
                                         ninfo.name
516

    
517
      if not (ninfo.offline or ninfo.drained):
518
        nresult.Raise("Can't get data for node %s" % ninfo.name)
519
        node_iinfo[nuuid].Raise("Can't get node instance info from node %s" %
520
                                ninfo.name)
521
        remote_info = rpc.MakeLegacyNodeInfo(nresult.payload,
522
                                             require_vg_info=has_lvm)
523

    
524
        def get_attr(attr):
525
          if attr not in remote_info:
526
            raise errors.OpExecError("Node '%s' didn't return attribute"
527
                                     " '%s'" % (ninfo.name, attr))
528
          value = remote_info[attr]
529
          if not isinstance(value, int):
530
            raise errors.OpExecError("Node '%s' returned invalid value"
531
                                     " for '%s': %s" %
532
                                     (ninfo.name, attr, value))
533
          return value
534

    
535
        mem_free = get_attr("memory_free")
536

    
537
        # compute memory used by primary instances
538
        i_p_mem = i_p_up_mem = 0
539
        for iinfo, beinfo in i_list:
540
          if iinfo.primary_node == nuuid:
541
            i_p_mem += beinfo[constants.BE_MAXMEM]
542
            if iinfo.name not in node_iinfo[nuuid].payload:
543
              i_used_mem = 0
544
            else:
545
              i_used_mem = int(node_iinfo[nuuid].payload[iinfo.name]["memory"])
546
            i_mem_diff = beinfo[constants.BE_MAXMEM] - i_used_mem
547
            mem_free -= max(0, i_mem_diff)
548

    
549
            if iinfo.admin_state == constants.ADMINST_UP:
550
              i_p_up_mem += beinfo[constants.BE_MAXMEM]
551

    
552
        # TODO: replace this with proper storage reporting
553
        if has_lvm:
554
          total_disk = get_attr("vg_size")
555
          free_disk = get_attr("vg_free")
556
          total_spindles = get_attr("spindles_total")
557
          free_spindles = get_attr("spindles_free")
558
        else:
559
          # we didn't even ask the node for VG status, so use zeros
560
          total_disk = free_disk = 0
561
          total_spindles = free_spindles = 0
562

    
563
        # compute memory used by instances
564
        pnr_dyn = {
565
          "total_memory": get_attr("memory_total"),
566
          "reserved_memory": get_attr("memory_dom0"),
567
          "free_memory": mem_free,
568
          "total_disk": total_disk,
569
          "free_disk": free_disk,
570
          "total_spindles": total_spindles,
571
          "free_spindles": free_spindles,
572
          "total_cpus": get_attr("cpu_total"),
573
          "i_pri_memory": i_p_mem,
574
          "i_pri_up_memory": i_p_up_mem,
575
          }
576
        pnr_dyn.update(node_results[ninfo.name])
577
        node_results[ninfo.name] = pnr_dyn
578

    
579
    return node_results
580

    
581
  @staticmethod
582
  def _ComputeInstanceData(cfg, cluster_info, i_list):
583
    """Compute global instance data.
584

585
    """
586
    instance_data = {}
587
    for iinfo, beinfo in i_list:
588
      nic_data = []
589
      for nic in iinfo.nics:
590
        filled_params = cluster_info.SimpleFillNIC(nic.nicparams)
591
        nic_dict = {
592
          "mac": nic.mac,
593
          "ip": nic.ip,
594
          "mode": filled_params[constants.NIC_MODE],
595
          "link": filled_params[constants.NIC_LINK],
596
          }
597
        if filled_params[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
598
          nic_dict["bridge"] = filled_params[constants.NIC_LINK]
599
        nic_data.append(nic_dict)
600
      pir = {
601
        "tags": list(iinfo.GetTags()),
602
        "admin_state": iinfo.admin_state,
603
        "vcpus": beinfo[constants.BE_VCPUS],
604
        "memory": beinfo[constants.BE_MAXMEM],
605
        "spindle_use": beinfo[constants.BE_SPINDLE_USE],
606
        "os": iinfo.os,
607
        "nodes": [cfg.GetNodeName(iinfo.primary_node)] +
608
                 cfg.GetNodeNames(iinfo.secondary_nodes),
609
        "nics": nic_data,
610
        "disks": [{constants.IDISK_SIZE: dsk.size,
611
                   constants.IDISK_MODE: dsk.mode,
612
                   constants.IDISK_SPINDLES: dsk.spindles}
613
                  for dsk in iinfo.disks],
614
        "disk_template": iinfo.disk_template,
615
        "disks_active": iinfo.disks_active,
616
        "hypervisor": iinfo.hypervisor,
617
        }
618
      pir["disk_space_total"] = gmi.ComputeDiskSize(iinfo.disk_template,
619
                                                    pir["disks"])
620
      instance_data[iinfo.name] = pir
621

    
622
    return instance_data
623

    
624
  def _BuildInputData(self, req):
625
    """Build input data structures.
626

627
    """
628
    self._ComputeClusterData()
629

    
630
    request = req.GetRequest(self.cfg)
631
    request["type"] = req.MODE
632
    self.in_data["request"] = request
633

    
634
    self.in_text = serializer.Dump(self.in_data)
635

    
636
  def Run(self, name, validate=True, call_fn=None):
637
    """Run an instance allocator and return the results.
638

639
    """
640
    if call_fn is None:
641
      call_fn = self.rpc.call_iallocator_runner
642

    
643
    result = call_fn(self.cfg.GetMasterNode(), name, self.in_text)
644
    result.Raise("Failure while running the iallocator script")
645

    
646
    self.out_text = result.payload
647
    if validate:
648
      self._ValidateResult()
649

    
650
  def _ValidateResult(self):
651
    """Process the allocator results.
652

653
    This will process and if successful save the result in
654
    self.out_data and the other parameters.
655

656
    """
657
    try:
658
      rdict = serializer.Load(self.out_text)
659
    except Exception, err:
660
      raise errors.OpExecError("Can't parse iallocator results: %s" % str(err))
661

    
662
    if not isinstance(rdict, dict):
663
      raise errors.OpExecError("Can't parse iallocator results: not a dict")
664

    
665
    # TODO: remove backwards compatiblity in later versions
666
    if "nodes" in rdict and "result" not in rdict:
667
      rdict["result"] = rdict["nodes"]
668
      del rdict["nodes"]
669

    
670
    for key in "success", "info", "result":
671
      if key not in rdict:
672
        raise errors.OpExecError("Can't parse iallocator results:"
673
                                 " missing key '%s'" % key)
674
      setattr(self, key, rdict[key])
675

    
676
    self.req.ValidateResult(self, self.result)
677
    self.out_data = rdict