Revision a3ad611d

b/snf-common/synnefo/util/rapi.py
1 1
#
2 2
#
3 3

  
4
# Copyright (C) 2010 Google Inc.
4
# Copyright (C) 2010, 2011 Google Inc.
5 5
#
6 6
# This program is free software; you can redistribute it and/or modify
7 7
# it under the terms of the GNU General Public License as published by
......
34 34
# be standalone.
35 35

  
36 36
import logging
37
import simplejson
37 38
import socket
38 39
import urllib
39 40
import threading
40 41
import pycurl
41

  
42
try:
43
  import simplejson as json
44
except ImportError:
45
  import json
42
import time
46 43

  
47 44
try:
48 45
  from cStringIO import StringIO
......
66 63
REPLACE_DISK_CHG = "replace_new_secondary"
67 64
REPLACE_DISK_AUTO = "replace_auto"
68 65

  
66
NODE_EVAC_PRI = "primary-only"
67
NODE_EVAC_SEC = "secondary-only"
68
NODE_EVAC_ALL = "all"
69

  
69 70
NODE_ROLE_DRAINED = "drained"
70 71
NODE_ROLE_MASTER_CANDIATE = "master-candidate"
71 72
NODE_ROLE_MASTER = "master"
72 73
NODE_ROLE_OFFLINE = "offline"
73 74
NODE_ROLE_REGULAR = "regular"
74 75

  
76
JOB_STATUS_QUEUED = "queued"
77
JOB_STATUS_WAITING = "waiting"
78
JOB_STATUS_CANCELING = "canceling"
79
JOB_STATUS_RUNNING = "running"
80
JOB_STATUS_CANCELED = "canceled"
81
JOB_STATUS_SUCCESS = "success"
82
JOB_STATUS_ERROR = "error"
83
JOB_STATUS_FINALIZED = frozenset([
84
  JOB_STATUS_CANCELED,
85
  JOB_STATUS_SUCCESS,
86
  JOB_STATUS_ERROR,
87
  ])
88
JOB_STATUS_ALL = frozenset([
89
  JOB_STATUS_QUEUED,
90
  JOB_STATUS_WAITING,
91
  JOB_STATUS_CANCELING,
92
  JOB_STATUS_RUNNING,
93
  ]) | JOB_STATUS_FINALIZED
94

  
95
# Legacy name
96
JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
97

  
75 98
# Internal constants
76 99
_REQ_DATA_VERSION_FIELD = "__version__"
77
_INST_CREATE_REQV1 = "instance-create-reqv1"
78
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
79
_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
80
_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
81
_INST_CREATE_V0_PARAMS = frozenset([
82
  "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
83
  "hypervisor", "file_storage_dir", "file_driver", "dry_run",
84
  ])
85
_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
100
_QPARAM_DRY_RUN = "dry-run"
101
_QPARAM_FORCE = "force"
102

  
103
# Feature strings
104
INST_CREATE_REQV1 = "instance-create-reqv1"
105
INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
106
NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
107
NODE_EVAC_RES1 = "node-evac-res1"
108

  
109
# Old feature constant names in case they're references by users of this module
110
_INST_CREATE_REQV1 = INST_CREATE_REQV1
111
_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
112
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
113
_NODE_EVAC_RES1 = NODE_EVAC_RES1
86 114

  
87 115
# Older pycURL versions don't have all error constants
88 116
try:
......
105 133
  pass
106 134

  
107 135

  
108
class CertificateError(Error):
136
class GanetiApiError(Error):
137
  """Generic error raised from Ganeti API.
138

  
139
  """
140
  def __init__(self, msg, code=None):
141
    Error.__init__(self, msg)
142
    self.code = code
143

  
144

  
145
class CertificateError(GanetiApiError):
109 146
  """Raised when a problem is found with the SSL certificate.
110 147

  
111 148
  """
112 149
  pass
113 150

  
114 151

  
115
class GanetiApiError(Error):
116
  """Generic error raised from Ganeti API.
152
def _AppendIf(container, condition, value):
153
  """Appends to a list if a condition evaluates to truth.
117 154

  
118 155
  """
119
  def __init__(self, msg, code=None):
120
    Error.__init__(self, msg)
121
    self.code = code
156
  if condition:
157
    container.append(value)
158

  
159
  return condition
160

  
161

  
162
def _AppendDryRunIf(container, condition):
163
  """Appends a "dry-run" parameter if a condition evaluates to truth.
164

  
165
  """
166
  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
167

  
168

  
169
def _AppendForceIf(container, condition):
170
  """Appends a "force" parameter if a condition evaluates to truth.
171

  
172
  """
173
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
174

  
175

  
176
def _SetItemIf(container, condition, item, value):
177
  """Sets an item if a condition evaluates to truth.
178

  
179
  """
180
  if condition:
181
    container[item] = value
182

  
183
  return condition
122 184

  
123 185

  
124 186
def UsesRapiClient(fn):
......
240 302
  return _ConfigCurl
241 303

  
242 304

  
243
class GanetiRapiClient(object): # pylint: disable-msg=R0904
305
class GanetiRapiClient(object): # pylint: disable=R0904
244 306
  """Ganeti RAPI client.
245 307

  
246 308
  """
247 309
  USER_AGENT = "Ganeti RAPI Client"
248
  _json_encoder = json.JSONEncoder(sort_keys=True)
310
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
249 311

  
250 312
  def __init__(self, host, port=GANETI_RAPI_PORT,
251
               username=None, password=None,
252
               logger=logging.getLogger('synnefo.util'),
313
               username=None, password=None, logger=logging,
253 314
               curl_config_fn=None, curl_factory=None):
254 315
    """Initializes this class.
255 316

  
......
409 470
        curl.perform()
410 471
      except pycurl.error, err:
411 472
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
412
          raise CertificateError("SSL certificate error %s" % err)
473
          raise CertificateError("SSL certificate error %s" % err,
474
                                 code=err.args[0])
413 475

  
414
        raise GanetiApiError(str(err))
476
        raise GanetiApiError(str(err), code=err.args[0])
415 477
    finally:
416 478
      # Reset settings to not keep references to large objects in memory
417 479
      # between requests
......
423 485

  
424 486
    # Was anything written to the response buffer?
425 487
    if encoded_resp_body.tell():
426
      response_content = json.loads(encoded_resp_body.getvalue())
488
      response_content = simplejson.loads(encoded_resp_body.getvalue())
427 489
    else:
428 490
      response_content = None
429 491

  
......
489 551
  def RedistributeConfig(self):
490 552
    """Tells the cluster to redistribute its configuration files.
491 553

  
554
    @rtype: string
492 555
    @return: job id
493 556

  
494 557
    """
......
501 564

  
502 565
    More details for parameters can be found in the RAPI documentation.
503 566

  
504
    @rtype: int
567
    @rtype: string
505 568
    @return: job id
506 569

  
507 570
    """
......
528 591
    @type dry_run: bool
529 592
    @param dry_run: whether to perform a dry run
530 593

  
531
    @rtype: int
594
    @rtype: string
532 595
    @return: job id
533 596

  
534 597
    """
535 598
    query = [("tag", t) for t in tags]
536
    if dry_run:
537
      query.append(("dry-run", 1))
599
    _AppendDryRunIf(query, dry_run)
538 600

  
539 601
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
540 602
                             query, None)
......
546 608
    @param tags: tags to delete
547 609
    @type dry_run: bool
548 610
    @param dry_run: whether to perform a dry run
611
    @rtype: string
612
    @return: job id
549 613

  
550 614
    """
551 615
    query = [("tag", t) for t in tags]
552
    if dry_run:
553
      query.append(("dry-run", 1))
616
    _AppendDryRunIf(query, dry_run)
554 617

  
555 618
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
556 619
                             query, None)
......
566 629

  
567 630
    """
568 631
    query = []
569
    if bulk:
570
      query.append(("bulk", 1))
632
    _AppendIf(query, bulk, ("bulk", 1))
571 633

  
572 634
    instances = self._SendRequest(HTTP_GET,
573 635
                                  "/%s/instances" % GANETI_RAPI_VERSION,
......
629 691
    @type dry_run: bool
630 692
    @keyword dry_run: whether to perform a dry run
631 693

  
632
    @rtype: int
694
    @rtype: string
633 695
    @return: job id
634 696

  
635 697
    """
636 698
    query = []
637 699

  
638
    if kwargs.get("dry_run"):
639
      query.append(("dry-run", 1))
700
    _AppendDryRunIf(query, kwargs.get("dry_run"))
640 701

  
641 702
    if _INST_CREATE_REQV1 in self.GetFeatures():
642 703
      # All required fields for request data version 1
......
657 718
      body.update((key, value) for key, value in kwargs.iteritems()
658 719
                  if key != "dry_run")
659 720
    else:
660
      # Old request format (version 0)
661

  
662
      # The following code must make sure that an exception is raised when an
663
      # unsupported setting is requested by the caller. Otherwise this can lead
664
      # to bugs difficult to find. The interface of this function must stay
665
      # exactly the same for version 0 and 1 (e.g. they aren't allowed to
666
      # require different data types).
667

  
668
      # Validate disks
669
      for idx, disk in enumerate(disks):
670
        unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS
671
        if unsupported:
672
          raise GanetiApiError("Server supports request version 0 only, but"
673
                               " disk %s specifies the unsupported parameters"
674
                               " %s, allowed are %s" %
675
                               (idx, unsupported,
676
                                list(_INST_CREATE_V0_DISK_PARAMS)))
677

  
678
      assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and
679
              "size" in _INST_CREATE_V0_DISK_PARAMS)
680
      disk_sizes = [disk["size"] for disk in disks]
681

  
682
      # Validate NICs
683
      if not nics:
684
        raise GanetiApiError("Server supports request version 0 only, but"
685
                             " no NIC specified")
686
      elif len(nics) > 1:
687
        raise GanetiApiError("Server supports request version 0 only, but"
688
                             " more than one NIC specified")
689

  
690
      assert len(nics) == 1
691

  
692
      unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS
693
      if unsupported:
694
        raise GanetiApiError("Server supports request version 0 only, but"
695
                             " NIC 0 specifies the unsupported parameters %s,"
696
                             " allowed are %s" %
697
                             (unsupported, list(_INST_NIC_PARAMS)))
698

  
699
      # Validate other parameters
700
      unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS -
701
                     _INST_CREATE_V0_DPARAMS)
702
      if unsupported:
703
        allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS)
704
        raise GanetiApiError("Server supports request version 0 only, but"
705
                             " the following unsupported parameters are"
706
                             " specified: %s, allowed are %s" %
707
                             (unsupported, list(allowed)))
708

  
709
      # All required fields for request data version 0
710
      body = {
711
        _REQ_DATA_VERSION_FIELD: 0,
712
        "name": name,
713
        "disk_template": disk_template,
714
        "disks": disk_sizes,
715
        }
716

  
717
      # NIC fields
718
      assert len(nics) == 1
719
      assert not (set(body.keys()) & set(nics[0].keys()))
720
      body.update(nics[0])
721

  
722
      # Copy supported fields
723
      assert not (set(body.keys()) & set(kwargs.keys()))
724
      body.update(dict((key, value) for key, value in kwargs.items()
725
                       if key in _INST_CREATE_V0_PARAMS))
726

  
727
      # Merge dictionaries
728
      for i in (value for key, value in kwargs.items()
729
                if key in _INST_CREATE_V0_DPARAMS):
730
        assert not (set(body.keys()) & set(i.keys()))
731
        body.update(i)
732

  
733
      assert not (set(kwargs.keys()) -
734
                  (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS))
735
      assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS)
721
      raise GanetiApiError("Server does not support new-style (version 1)"
722
                           " instance creation requests")
736 723

  
737 724
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
738 725
                             query, body)
......
743 730
    @type instance: str
744 731
    @param instance: the instance to delete
745 732

  
746
    @rtype: int
733
    @rtype: string
747 734
    @return: job id
748 735

  
749 736
    """
750 737
    query = []
751
    if dry_run:
752
      query.append(("dry-run", 1))
738
    _AppendDryRunIf(query, dry_run)
753 739

  
754 740
    return self._SendRequest(HTTP_DELETE,
755 741
                             ("/%s/instances/%s" %
......
762 748

  
763 749
    @type instance: string
764 750
    @param instance: Instance name
765
    @rtype: int
751
    @rtype: string
766 752
    @return: job id
767 753

  
768 754
    """
......
779 765
    @param instance: Instance name
780 766
    @type ignore_size: bool
781 767
    @param ignore_size: Whether to ignore recorded size
768
    @rtype: string
782 769
    @return: job id
783 770

  
784 771
    """
785 772
    query = []
786
    if ignore_size:
787
      query.append(("ignore_size", 1))
773
    _AppendIf(query, ignore_size, ("ignore_size", 1))
788 774

  
789 775
    return self._SendRequest(HTTP_PUT,
790 776
                             ("/%s/instances/%s/activate-disks" %
......
795 781

  
796 782
    @type instance: string
797 783
    @param instance: Instance name
784
    @rtype: string
798 785
    @return: job id
799 786

  
800 787
    """
......
802 789
                             ("/%s/instances/%s/deactivate-disks" %
803 790
                              (GANETI_RAPI_VERSION, instance)), None, None)
804 791

  
792
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
793
    """Recreate an instance's disks.
794

  
795
    @type instance: string
796
    @param instance: Instance name
797
    @type disks: list of int
798
    @param disks: List of disk indexes
799
    @type nodes: list of string
800
    @param nodes: New instance nodes, if relocation is desired
801
    @rtype: string
802
    @return: job id
803

  
804
    """
805
    body = {}
806
    _SetItemIf(body, disks is not None, "disks", disks)
807
    _SetItemIf(body, nodes is not None, "nodes", nodes)
808

  
809
    return self._SendRequest(HTTP_POST,
810
                             ("/%s/instances/%s/recreate-disks" %
811
                              (GANETI_RAPI_VERSION, instance)), None, body)
812

  
805 813
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
806 814
    """Grows a disk of an instance.
807 815

  
......
815 823
    @param amount: Grow disk by this amount (MiB)
816 824
    @type wait_for_sync: bool
817 825
    @param wait_for_sync: Wait for disk to synchronize
818
    @rtype: int
826
    @rtype: string
819 827
    @return: job id
820 828

  
821 829
    """
......
823 831
      "amount": amount,
824 832
      }
825 833

  
826
    if wait_for_sync is not None:
827
      body["wait_for_sync"] = wait_for_sync
834
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
828 835

  
829 836
    return self._SendRequest(HTTP_POST,
830 837
                             ("/%s/instances/%s/disk/%s/grow" %
......
855 862
    @type dry_run: bool
856 863
    @param dry_run: whether to perform a dry run
857 864

  
858
    @rtype: int
865
    @rtype: string
859 866
    @return: job id
860 867

  
861 868
    """
862 869
    query = [("tag", t) for t in tags]
863
    if dry_run:
864
      query.append(("dry-run", 1))
870
    _AppendDryRunIf(query, dry_run)
865 871

  
866 872
    return self._SendRequest(HTTP_PUT,
867 873
                             ("/%s/instances/%s/tags" %
......
876 882
    @param tags: tags to delete
877 883
    @type dry_run: bool
878 884
    @param dry_run: whether to perform a dry run
885
    @rtype: string
886
    @return: job id
879 887

  
880 888
    """
881 889
    query = [("tag", t) for t in tags]
882
    if dry_run:
883
      query.append(("dry-run", 1))
890
    _AppendDryRunIf(query, dry_run)
884 891

  
885 892
    return self._SendRequest(HTTP_DELETE,
886 893
                             ("/%s/instances/%s/tags" %
......
899 906
        while re-assembling disks (in hard-reboot mode only)
900 907
    @type dry_run: bool
901 908
    @param dry_run: whether to perform a dry run
909
    @rtype: string
910
    @return: job id
902 911

  
903 912
    """
904 913
    query = []
905
    if reboot_type:
906
      query.append(("type", reboot_type))
907
    if ignore_secondaries is not None:
908
      query.append(("ignore_secondaries", ignore_secondaries))
909
    if dry_run:
910
      query.append(("dry-run", 1))
914
    _AppendDryRunIf(query, dry_run)
915
    _AppendIf(query, reboot_type, ("type", reboot_type))
916
    _AppendIf(query, ignore_secondaries is not None,
917
              ("ignore_secondaries", ignore_secondaries))
911 918

  
912 919
    return self._SendRequest(HTTP_POST,
913 920
                             ("/%s/instances/%s/reboot" %
914 921
                              (GANETI_RAPI_VERSION, instance)), query, None)
915 922

  
916
  def ShutdownInstance(self, instance, dry_run=False):
923
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
917 924
    """Shuts down an instance.
918 925

  
919 926
    @type instance: str
920 927
    @param instance: the instance to shut down
921 928
    @type dry_run: bool
922 929
    @param dry_run: whether to perform a dry run
930
    @type no_remember: bool
931
    @param no_remember: if true, will not record the state change
932
    @rtype: string
933
    @return: job id
923 934

  
924 935
    """
925 936
    query = []
926
    if dry_run:
927
      query.append(("dry-run", 1))
937
    _AppendDryRunIf(query, dry_run)
938
    _AppendIf(query, no_remember, ("no-remember", 1))
928 939

  
929 940
    return self._SendRequest(HTTP_PUT,
930 941
                             ("/%s/instances/%s/shutdown" %
931 942
                              (GANETI_RAPI_VERSION, instance)), query, None)
932 943

  
933
  def StartupInstance(self, instance, dry_run=False):
944
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
934 945
    """Starts up an instance.
935 946

  
936 947
    @type instance: str
937 948
    @param instance: the instance to start up
938 949
    @type dry_run: bool
939 950
    @param dry_run: whether to perform a dry run
951
    @type no_remember: bool
952
    @param no_remember: if true, will not record the state change
953
    @rtype: string
954
    @return: job id
940 955

  
941 956
    """
942 957
    query = []
943
    if dry_run:
944
      query.append(("dry-run", 1))
958
    _AppendDryRunIf(query, dry_run)
959
    _AppendIf(query, no_remember, ("no-remember", 1))
945 960

  
946 961
    return self._SendRequest(HTTP_PUT,
947 962
                             ("/%s/instances/%s/startup" %
......
958 973
        current operating system will be installed again
959 974
    @type no_startup: bool
960 975
    @param no_startup: Whether to start the instance automatically
976
    @rtype: string
977
    @return: job id
961 978

  
962 979
    """
963 980
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
964 981
      body = {
965 982
        "start": not no_startup,
966 983
        }
967
      if os is not None:
968
        body["os"] = os
969
      if osparams is not None:
970
        body["osparams"] = osparams
984
      _SetItemIf(body, os is not None, "os", os)
985
      _SetItemIf(body, osparams is not None, "osparams", osparams)
971 986
      return self._SendRequest(HTTP_POST,
972 987
                               ("/%s/instances/%s/reinstall" %
973 988
                                (GANETI_RAPI_VERSION, instance)), None, body)
......
978 993
                           " for instance reinstallation")
979 994

  
980 995
    query = []
981
    if os:
982
      query.append(("os", os))
983
    if no_startup:
984
      query.append(("nostartup", 1))
996
    _AppendIf(query, os, ("os", os))
997
    _AppendIf(query, no_startup, ("nostartup", 1))
998

  
985 999
    return self._SendRequest(HTTP_POST,
986 1000
                             ("/%s/instances/%s/reinstall" %
987 1001
                              (GANETI_RAPI_VERSION, instance)), query, None)
988 1002

  
989 1003
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
990
                           remote_node=None, iallocator=None, dry_run=False):
1004
                           remote_node=None, iallocator=None):
991 1005
    """Replaces disks on an instance.
992 1006

  
993 1007
    @type instance: str
......
1002 1016
    @type iallocator: str or None
1003 1017
    @param iallocator: instance allocator plugin to use (for use with
1004 1018
                       replace_auto mode)
1005
    @type dry_run: bool
1006
    @param dry_run: whether to perform a dry run
1007 1019

  
1008
    @rtype: int
1020
    @rtype: string
1009 1021
    @return: job id
1010 1022

  
1011 1023
    """
......
1013 1025
      ("mode", mode),
1014 1026
      ]
1015 1027

  
1016
    if disks:
1017
      query.append(("disks", ",".join(str(idx) for idx in disks)))
1018

  
1019
    if remote_node:
1020
      query.append(("remote_node", remote_node))
1028
    # TODO: Convert to body parameters
1021 1029

  
1022
    if iallocator:
1023
      query.append(("iallocator", iallocator))
1030
    if disks is not None:
1031
      _AppendIf(query, True,
1032
                ("disks", ",".join(str(idx) for idx in disks)))
1024 1033

  
1025
    if dry_run:
1026
      query.append(("dry-run", 1))
1034
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1035
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1027 1036

  
1028 1037
    return self._SendRequest(HTTP_POST,
1029 1038
                             ("/%s/instances/%s/replace-disks" %
......
1063 1072
      "mode": mode,
1064 1073
      }
1065 1074

  
1066
    if shutdown is not None:
1067
      body["shutdown"] = shutdown
1068

  
1069
    if remove_instance is not None:
1070
      body["remove_instance"] = remove_instance
1071

  
1072
    if x509_key_name is not None:
1073
      body["x509_key_name"] = x509_key_name
1074

  
1075
    if destination_x509_ca is not None:
1076
      body["destination_x509_ca"] = destination_x509_ca
1075
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1076
    _SetItemIf(body, remove_instance is not None,
1077
               "remove_instance", remove_instance)
1078
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1079
    _SetItemIf(body, destination_x509_ca is not None,
1080
               "destination_x509_ca", destination_x509_ca)
1077 1081

  
1078 1082
    return self._SendRequest(HTTP_PUT,
1079 1083
                             ("/%s/instances/%s/export" %
......
1088 1092
    @param mode: Migration mode
1089 1093
    @type cleanup: bool
1090 1094
    @param cleanup: Whether to clean up a previously failed migration
1095
    @rtype: string
1096
    @return: job id
1091 1097

  
1092 1098
    """
1093 1099
    body = {}
1100
    _SetItemIf(body, mode is not None, "mode", mode)
1101
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1094 1102

  
1095
    if mode is not None:
1096
      body["mode"] = mode
1103
    return self._SendRequest(HTTP_PUT,
1104
                             ("/%s/instances/%s/migrate" %
1105
                              (GANETI_RAPI_VERSION, instance)), None, body)
1097 1106

  
1098
    if cleanup is not None:
1099
      body["cleanup"] = cleanup
1107
  def FailoverInstance(self, instance, iallocator=None,
1108
                       ignore_consistency=None, target_node=None):
1109
    """Does a failover of an instance.
1110

  
1111
    @type instance: string
1112
    @param instance: Instance name
1113
    @type iallocator: string
1114
    @param iallocator: Iallocator for deciding the target node for
1115
      shared-storage instances
1116
    @type ignore_consistency: bool
1117
    @param ignore_consistency: Whether to ignore disk consistency
1118
    @type target_node: string
1119
    @param target_node: Target node for shared-storage instances
1120
    @rtype: string
1121
    @return: job id
1122

  
1123
    """
1124
    body = {}
1125
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1126
    _SetItemIf(body, ignore_consistency is not None,
1127
               "ignore_consistency", ignore_consistency)
1128
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1100 1129

  
1101 1130
    return self._SendRequest(HTTP_PUT,
1102
                             ("/%s/instances/%s/migrate" %
1131
                             ("/%s/instances/%s/failover" %
1103 1132
                              (GANETI_RAPI_VERSION, instance)), None, body)
1104 1133

  
1105 1134
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
......
1113 1142
    @param ip_check: Whether to ensure instance's IP address is inactive
1114 1143
    @type name_check: bool
1115 1144
    @param name_check: Whether to ensure instance's name is resolvable
1145
    @rtype: string
1146
    @return: job id
1116 1147

  
1117 1148
    """
1118 1149
    body = {
1119 1150
      "new_name": new_name,
1120 1151
      }
1121 1152

  
1122
    if ip_check is not None:
1123
      body["ip_check"] = ip_check
1124

  
1125
    if name_check is not None:
1126
      body["name_check"] = name_check
1153
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1154
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1127 1155

  
1128 1156
    return self._SendRequest(HTTP_PUT,
1129 1157
                             ("/%s/instances/%s/rename" %
......
1134 1162

  
1135 1163
    @type instance: string
1136 1164
    @param instance: Instance name
1165
    @rtype: dict
1166
    @return: dictionary containing information about instance's console
1137 1167

  
1138 1168
    """
1139 1169
    return self._SendRequest(HTTP_GET,
......
1155 1185
  def GetJobStatus(self, job_id):
1156 1186
    """Gets the status of a job.
1157 1187

  
1158
    @type job_id: int
1188
    @type job_id: string
1159 1189
    @param job_id: job id whose status to query
1160 1190

  
1161 1191
    @rtype: dict
......
1166 1196
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1167 1197
                             None, None)
1168 1198

  
1199
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1200
    """Polls cluster for job status until completion.
1201

  
1202
    Completion is defined as any of the following states listed in
1203
    L{JOB_STATUS_FINALIZED}.
1204

  
1205
    @type job_id: string
1206
    @param job_id: job id to watch
1207
    @type period: int
1208
    @param period: how often to poll for status (optional, default 5s)
1209
    @type retries: int
1210
    @param retries: how many time to poll before giving up
1211
                    (optional, default -1 means unlimited)
1212

  
1213
    @rtype: bool
1214
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1215
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1216
      possible; L{WaitForJobChange} returns immediately after a job changed and
1217
      does not use polling
1218

  
1219
    """
1220
    while retries != 0:
1221
      job_result = self.GetJobStatus(job_id)
1222

  
1223
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1224
        return True
1225
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1226
        return False
1227

  
1228
      if period:
1229
        time.sleep(period)
1230

  
1231
      if retries > 0:
1232
        retries -= 1
1233

  
1234
    return False
1235

  
1169 1236
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1170 1237
    """Waits for job changes.
1171 1238

  
1172
    @type job_id: int
1239
    @type job_id: string
1173 1240
    @param job_id: Job ID for which to wait
1241
    @return: C{None} if no changes have been detected and a dict with two keys,
1242
      C{job_info} and C{log_entries} otherwise.
1243
    @rtype: dict
1174 1244

  
1175 1245
    """
1176 1246
    body = {
......
1186 1256
  def CancelJob(self, job_id, dry_run=False):
1187 1257
    """Cancels a job.
1188 1258

  
1189
    @type job_id: int
1259
    @type job_id: string
1190 1260
    @param job_id: id of the job to delete
1191 1261
    @type dry_run: bool
1192 1262
    @param dry_run: whether to perform a dry run
1263
    @rtype: tuple
1264
    @return: tuple containing the result, and a message (bool, string)
1193 1265

  
1194 1266
    """
1195 1267
    query = []
1196
    if dry_run:
1197
      query.append(("dry-run", 1))
1268
    _AppendDryRunIf(query, dry_run)
1198 1269

  
1199 1270
    return self._SendRequest(HTTP_DELETE,
1200 1271
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
......
1212 1283

  
1213 1284
    """
1214 1285
    query = []
1215
    if bulk:
1216
      query.append(("bulk", 1))
1286
    _AppendIf(query, bulk, ("bulk", 1))
1217 1287

  
1218 1288
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1219 1289
                              query, None)
......
1237 1307
                             None, None)
1238 1308

  
1239 1309
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1240
                   dry_run=False, early_release=False):
1310
                   dry_run=False, early_release=None,
1311
                   mode=None, accept_old=False):
1241 1312
    """Evacuates instances from a Ganeti node.
1242 1313

  
1243 1314
    @type node: str
......
1250 1321
    @param dry_run: whether to perform a dry run
1251 1322
    @type early_release: bool
1252 1323
    @param early_release: whether to enable parallelization
1324
    @type mode: string
1325
    @param mode: Node evacuation mode
1326
    @type accept_old: bool
1327
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1328
        results
1253 1329

  
1254
    @rtype: list
1255
    @return: list of (job ID, instance name, new secondary node); if
1256
        dry_run was specified, then the actual move jobs were not
1257
        submitted and the job IDs will be C{None}
1330
    @rtype: string, or a list for pre-2.5 results
1331
    @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1332
      list of (job ID, instance name, new secondary node); if dry_run was
1333
      specified, then the actual move jobs were not submitted and the job IDs
1334
      will be C{None}
1258 1335

  
1259 1336
    @raises GanetiApiError: if an iallocator and remote_node are both
1260 1337
        specified
......
1264 1341
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1265 1342

  
1266 1343
    query = []
1267
    if iallocator:
1268
      query.append(("iallocator", iallocator))
1269
    if remote_node:
1270
      query.append(("remote_node", remote_node))
1271
    if dry_run:
1272
      query.append(("dry-run", 1))
1273
    if early_release:
1274
      query.append(("early_release", 1))
1344
    _AppendDryRunIf(query, dry_run)
1345

  
1346
    if _NODE_EVAC_RES1 in self.GetFeatures():
1347
      # Server supports body parameters
1348
      body = {}
1349

  
1350
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1351
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1352
      _SetItemIf(body, early_release is not None,
1353
                 "early_release", early_release)
1354
      _SetItemIf(body, mode is not None, "mode", mode)
1355
    else:
1356
      # Pre-2.5 request format
1357
      body = None
1358

  
1359
      if not accept_old:
1360
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1361
                             " not accept old-style results (parameter"
1362
                             " accept_old)")
1363

  
1364
      # Pre-2.5 servers can only evacuate secondaries
1365
      if mode is not None and mode != NODE_EVAC_SEC:
1366
        raise GanetiApiError("Server can only evacuate secondary instances")
1367

  
1368
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1369
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1370
      _AppendIf(query, early_release, ("early_release", 1))
1275 1371

  
1276 1372
    return self._SendRequest(HTTP_POST,
1277 1373
                             ("/%s/nodes/%s/evacuate" %
1278
                              (GANETI_RAPI_VERSION, node)), query, None)
1374
                              (GANETI_RAPI_VERSION, node)), query, body)
1279 1375

  
1280
  def MigrateNode(self, node, mode=None, dry_run=False):
1376
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1377
                  target_node=None):
1281 1378
    """Migrates all primary instances from a node.
1282 1379

  
1283 1380
    @type node: str
......
1287 1384
        otherwise the hypervisor default will be used
1288 1385
    @type dry_run: bool
1289 1386
    @param dry_run: whether to perform a dry run
1387
    @type iallocator: string
1388
    @param iallocator: instance allocator to use
1389
    @type target_node: string
1390
    @param target_node: Target node for shared-storage instances
1290 1391

  
1291
    @rtype: int
1392
    @rtype: string
1292 1393
    @return: job id
1293 1394

  
1294 1395
    """
1295 1396
    query = []
1296
    if mode is not None:
1297
      query.append(("mode", mode))
1298
    if dry_run:
1299
      query.append(("dry-run", 1))
1397
    _AppendDryRunIf(query, dry_run)
1300 1398

  
1301
    return self._SendRequest(HTTP_POST,
1302
                             ("/%s/nodes/%s/migrate" %
1303
                              (GANETI_RAPI_VERSION, node)), query, None)
1399
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1400
      body = {}
1401

  
1402
      _SetItemIf(body, mode is not None, "mode", mode)
1403
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1404
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1405

  
1406
      assert len(query) <= 1
1407

  
1408
      return self._SendRequest(HTTP_POST,
1409
                               ("/%s/nodes/%s/migrate" %
1410
                                (GANETI_RAPI_VERSION, node)), query, body)
1411
    else:
1412
      # Use old request format
1413
      if target_node is not None:
1414
        raise GanetiApiError("Server does not support specifying target node"
1415
                             " for node migration")
1416

  
1417
      _AppendIf(query, mode is not None, ("mode", mode))
1418

  
1419
      return self._SendRequest(HTTP_POST,
1420
                               ("/%s/nodes/%s/migrate" %
1421
                                (GANETI_RAPI_VERSION, node)), query, None)
1304 1422

  
1305 1423
  def GetNodeRole(self, node):
1306 1424
    """Gets the current role for a node.
......
1316 1434
                             ("/%s/nodes/%s/role" %
1317 1435
                              (GANETI_RAPI_VERSION, node)), None, None)
1318 1436

  
1319
  def SetNodeRole(self, node, role, force=False):
1437
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1320 1438
    """Sets the role for a node.
1321 1439

  
1322 1440
    @type node: str
......
1325 1443
    @param role: the role to set for the node
1326 1444
    @type force: bool
1327 1445
    @param force: whether to force the role change
1446
    @type auto_promote: bool
1447
    @param auto_promote: Whether node(s) should be promoted to master candidate
1448
                         if necessary
1328 1449

  
1329
    @rtype: int
1450
    @rtype: string
1330 1451
    @return: job id
1331 1452

  
1332 1453
    """
1333
    query = [
1334
      ("force", force),
1335
      ]
1454
    query = []
1455
    _AppendForceIf(query, force)
1456
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1336 1457

  
1337 1458
    return self._SendRequest(HTTP_PUT,
1338 1459
                             ("/%s/nodes/%s/role" %
1339 1460
                              (GANETI_RAPI_VERSION, node)), query, role)
1340 1461

  
1462
  def PowercycleNode(self, node, force=False):
1463
    """Powercycles a node.
1464

  
1465
    @type node: string
1466
    @param node: Node name
1467
    @type force: bool
1468
    @param force: Whether to force the operation
1469
    @rtype: string
1470
    @return: job id
1471

  
1472
    """
1473
    query = []
1474
    _AppendForceIf(query, force)
1475

  
1476
    return self._SendRequest(HTTP_POST,
1477
                             ("/%s/nodes/%s/powercycle" %
1478
                              (GANETI_RAPI_VERSION, node)), query, None)
1479

  
1480
  def ModifyNode(self, node, **kwargs):
1481
    """Modifies a node.
1482

  
1483
    More details for parameters can be found in the RAPI documentation.
1484

  
1485
    @type node: string
1486
    @param node: Node name
1487
    @rtype: string
1488
    @return: job id
1489

  
1490
    """
1491
    return self._SendRequest(HTTP_POST,
1492
                             ("/%s/nodes/%s/modify" %
1493
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1494

  
1341 1495
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1342 1496
    """Gets the storage units for a node.
1343 1497

  
......
1348 1502
    @type output_fields: str
1349 1503
    @param output_fields: storage type fields to return
1350 1504

  
1351
    @rtype: int
1505
    @rtype: string
1352 1506
    @return: job id where results can be retrieved
1353 1507

  
1354 1508
    """
......
1374 1528
    @param allocatable: Whether to set the "allocatable" flag on the storage
1375 1529
                        unit (None=no modification, True=set, False=unset)
1376 1530

  
1377
    @rtype: int
1531
    @rtype: string
1378 1532
    @return: job id
1379 1533

  
1380 1534
    """
......
1383 1537
      ("name", name),
1384 1538
      ]
1385 1539

  
1386
    if allocatable is not None:
1387
      query.append(("allocatable", allocatable))
1540
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1388 1541

  
1389 1542
    return self._SendRequest(HTTP_PUT,
1390 1543
                             ("/%s/nodes/%s/storage/modify" %
......
1400 1553
    @type name: str
1401 1554
    @param name: name of the storage unit to repair
1402 1555

  
1403
    @rtype: int
1556
    @rtype: string
1404 1557
    @return: job id
1405 1558

  
1406 1559
    """
......
1437 1590
    @type dry_run: bool
1438 1591
    @param dry_run: whether to perform a dry run
1439 1592

  
1440
    @rtype: int
1593
    @rtype: string
1441 1594
    @return: job id
1442 1595

  
1443 1596
    """
1444 1597
    query = [("tag", t) for t in tags]
1445
    if dry_run:
1446
      query.append(("dry-run", 1))
1598
    _AppendDryRunIf(query, dry_run)
1447 1599

  
1448 1600
    return self._SendRequest(HTTP_PUT,
1449 1601
                             ("/%s/nodes/%s/tags" %
......
1459 1611
    @type dry_run: bool
1460 1612
    @param dry_run: whether to perform a dry run
1461 1613

  
1462
    @rtype: int
1614
    @rtype: string
1463 1615
    @return: job id
1464 1616

  
1465 1617
    """
1466 1618
    query = [("tag", t) for t in tags]
1467
    if dry_run:
1468
      query.append(("dry-run", 1))
1619
    _AppendDryRunIf(query, dry_run)
1469 1620

  
1470 1621
    return self._SendRequest(HTTP_DELETE,
1471 1622
                             ("/%s/nodes/%s/tags" %
1472 1623
                              (GANETI_RAPI_VERSION, node)), query, None)
1473 1624

  
1625
  def GetNetworks(self, bulk=False):
1626
    """Gets all networks in the cluster.
1627

  
1628
    @type bulk: bool
1629
    @param bulk: whether to return all information about the networks
1630

  
1631
    @rtype: list of dict or str
1632
    @return: if bulk is true, a list of dictionaries with info about all
1633
        networks in the cluster, else a list of names of those networks
1634

  
1635
    """
1636
    query = []
1637
    _AppendIf(query, bulk, ("bulk", 1))
1638

  
1639
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1640
                               query, None)
1641
    if bulk:
1642
      return networks
1643
    else:
1644
      return [n["name"] for n in networks]
1645

  
1646
  def GetNetwork(self, network):
1647
    """Gets information about a network.
1648

  
1649
    @type group: str
1650
    @param group: name of the network whose info to return
1651

  
1652
    @rtype: dict
1653
    @return: info about the network
1654

  
1655
    """
1656
    return self._SendRequest(HTTP_GET,
1657
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1658
                             None, None)
1659

  
1660
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1661
                    gateway6=None, mac_prefix=None, network_type="private",
1662
                    reserved_ips=None, dry_run=False):
1663
    """Creates a new network.
1664

  
1665
    @type name: str
1666
    @param name: the name of network to create
1667
    @type dry_run: bool
1668
    @param dry_run: whether to peform a dry run
1669

  
1670
    @rtype: string
1671
    @return: job id
1672

  
1673
    """
1674
    query = []
1675
    _AppendDryRunIf(query, dry_run)
1676

  
1677
    body = {
1678
      "network_name": network_name,
1679
      "gateway": gateway,
1680
      "network": network,
1681
      "gateway6": gateway6,
1682
      "network6": network6,
1683
      "mac_prefix": mac_prefix,
1684
      "network_type": network_type,
1685
      "reserved_ips": reserved_ips
1686
      }
1687

  
1688
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1689
                             query, body)
1690

  
1691
  def ConnectNetwork(self, network_name, group_name, mode, link, depends=None):
1692
    """Connects a Network to a NodeGroup with the given netparams
1693

  
1694
    """
1695
    body = {
1696
      "group_name": group_name,
1697
      "network_mode": mode,
1698
      "network_link": link
1699
      }
1700

  
1701
    if depends:
1702
      body['depends'] = []
1703
      for d in depends:
1704
        body['depends'].append([d, ["success"]])
1705

  
1706

  
1707
    return self._SendRequest(HTTP_PUT,
1708
                             ("/%s/networks/%s/connect" %
1709
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1710

  
1711
  def DisconnectNetwork(self, network_name, group_name, depends=None):
1712
    """Connects a Network to a NodeGroup with the given netparams
1713

  
1714
    """
1715
    body = {
1716
      "group_name": group_name
1717
      }
1718

  
1719
    if depends:
1720
      body['depends'] = []
1721
      for d in depends:
1722
        body['depends'].append([d, ["success"]])
1723

  
1724
    return self._SendRequest(HTTP_PUT,
1725
                             ("/%s/networks/%s/disconnect" %
1726
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1727

  
1728
  def ConnectNetworkAll(self, network_name, mode, link, depends=None):
1729
    """Connects a Network to a NodeGroup with the given netparams
1730

  
1731
    """
1732
    body = {
1733
      "network_mode": mode,
1734
      "network_link": link
1735
      }
1736

  
1737
    if depends:
1738
      body['depends'] = []
1739
      for d in depends:
1740
        body['depends'].append([d, ["success"]])
1741

  
1742
    return self._SendRequest(HTTP_PUT,
1743
                             ("/%s/networks/%s/connectall" %
1744
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1745

  
1746
  def DisconnectNetworkAll(self, network_name, depends=None):
1747
    """Connects a Network to a NodeGroup with the given netparams
1748

  
1749
    """
1750
    body = {}
1751
    if depends:
1752
      body['depends'] = []
1753
      for d in depends:
1754
        body['depends'].append([d, ["success"]])
1755

  
1756
    return self._SendRequest(HTTP_PUT,
1757
                             ("/%s/networks/%s/disconnectall" %
1758
                             (GANETI_RAPI_VERSION, network_name)), None, body)
1759

  
1760
  def DeleteNetwork(self, network, depends=None):
1761
    """Deletes a network.
1762

  
1763
    @type group: str
1764
    @param group: the network to delete
1765
    @type dry_run: bool
1766
    @param dry_run: whether to peform a dry run
1767

  
1768
    @rtype: string
1769
    @return: job id
1770

  
1771
    """
1772
    body = {}
1773
    if depends:
1774
      body['depends'] = []
1775
      for d in depends:
1776
        body['depends'].append([d, ["success"]])
1777

  
1778

  
1779
    return self._SendRequest(HTTP_DELETE,
1780
                             ("/%s/networks/%s" %
1781
                              (GANETI_RAPI_VERSION, network)), None, body)
1782

  
1474 1783
  def GetGroups(self, bulk=False):
1475 1784
    """Gets all node groups in the cluster.
1476 1785

  
......
1483 1792

  
1484 1793
    """
1485 1794
    query = []
1486
    if bulk:
1487
      query.append(("bulk", 1))
1795
    _AppendIf(query, bulk, ("bulk", 1))
1488 1796

  
1489 1797
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1490 1798
                               query, None)
......
1517 1825
    @type dry_run: bool
1518 1826
    @param dry_run: whether to peform a dry run
1519 1827

  
1520
    @rtype: int
1828
    @rtype: string
1521 1829
    @return: job id
1522 1830

  
1523 1831
    """
1524 1832
    query = []
1525
    if dry_run:
1526
      query.append(("dry-run", 1))
1833
    _AppendDryRunIf(query, dry_run)
1527 1834

  
1528 1835
    body = {
1529 1836
      "name": name,
......
1540 1847

  
1541 1848
    @type group: string
1542 1849
    @param group: Node group name
1543
    @rtype: int
1850
    @rtype: string
1544 1851
    @return: job id
1545 1852

  
1546 1853
    """
......
1556 1863
    @type dry_run: bool
1557 1864
    @param dry_run: whether to peform a dry run
1558 1865

  
1559
    @rtype: int
1866
    @rtype: string
1560 1867
    @return: job id
1561 1868

  
1562 1869
    """
1563 1870
    query = []
1564
    if dry_run:
1565
      query.append(("dry-run", 1))
1871
    _AppendDryRunIf(query, dry_run)
1566 1872

  
1567 1873
    return self._SendRequest(HTTP_DELETE,
1568 1874
                             ("/%s/groups/%s" %
......
1576 1882
    @type new_name: string
1577 1883
    @param new_name: New node group name
1578 1884

  
1579
    @rtype: int
1885
    @rtype: string
1580 1886
    @return: job id
1581 1887

  
1582 1888
    """
......
1588 1894
                             ("/%s/groups/%s/rename" %
1589 1895
                              (GANETI_RAPI_VERSION, group)), None, body)
1590 1896

  
1591

  
1592 1897
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1593 1898
    """Assigns nodes to a group.
1594 1899

  
......
1597 1902
    @type nodes: list of strings
1598 1903
    @param nodes: List of nodes to assign to the group
1599 1904

  
1600
    @rtype: int
1905
    @rtype: string
1601 1906
    @return: job id
1602 1907

  
1603 1908
    """
1604 1909
    query = []
1605

  
1606
    if force:
1607
      query.append(("force", 1))
1608

  
1609
    if dry_run:
1610
      query.append(("dry-run", 1))
1910
    _AppendForceIf(query, force)
1911
    _AppendDryRunIf(query, dry_run)
1611 1912

  
1612 1913
    body = {
1613 1914
      "nodes": nodes,
......
1616 1917
    return self._SendRequest(HTTP_PUT,
1617 1918
                             ("/%s/groups/%s/assign-nodes" %
1618 1919
                             (GANETI_RAPI_VERSION, group)), query, body)
1920

  
1921
  def GetGroupTags(self, group):
1922
    """Gets tags for a node group.
1923

  
1924
    @type group: string
1925
    @param group: Node group whose tags to return
1926

  
1927
    @rtype: list of strings
1928
    @return: tags for the group
1929

  
1930
    """
1931
    return self._SendRequest(HTTP_GET,
1932
                             ("/%s/groups/%s/tags" %
1933
                              (GANETI_RAPI_VERSION, group)), None, None)
1934

  
1935
  def AddGroupTags(self, group, tags, dry_run=False):
1936
    """Adds tags to a node group.
1937

  
1938
    @type group: str
1939
    @param group: group to add tags to
1940
    @type tags: list of string
1941
    @param tags: tags to add to the group
1942
    @type dry_run: bool
1943
    @param dry_run: whether to perform a dry run
1944

  
1945
    @rtype: string
1946
    @return: job id
1947

  
1948
    """
1949
    query = [("tag", t) for t in tags]
1950
    _AppendDryRunIf(query, dry_run)
1951

  
1952
    return self._SendRequest(HTTP_PUT,
1953
                             ("/%s/groups/%s/tags" %
1954
                              (GANETI_RAPI_VERSION, group)), query, None)
1955

  
1956
  def DeleteGroupTags(self, group, tags, dry_run=False):
1957
    """Deletes tags from a node group.
1958

  
1959
    @type group: str
1960
    @param group: group to delete tags from
1961
    @type tags: list of string
1962
    @param tags: tags to delete
1963
    @type dry_run: bool
1964
    @param dry_run: whether to perform a dry run
1965
    @rtype: string
1966
    @return: job id
1967

  
1968
    """
1969
    query = [("tag", t) for t in tags]
1970
    _AppendDryRunIf(query, dry_run)
1971

  
1972
    return self._SendRequest(HTTP_DELETE,
1973
                             ("/%s/groups/%s/tags" %
1974
                              (GANETI_RAPI_VERSION, group)), query, None)
1975

  
1976
  def Query(self, what, fields, qfilter=None):
1977
    """Retrieves information about resources.
1978

  
1979
    @type what: string
1980
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1981
    @type fields: list of string
1982
    @param fields: Requested fields
1983
    @type qfilter: None or list
1984
    @param qfilter: Query filter
1985

  
1986
    @rtype: string
1987
    @return: job id
1988

  
1989
    """
1990
    body = {
1991
      "fields": fields,
1992
      }
1993

  
1994
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
1995
    # TODO: remove "filter" after 2.7
1996
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
1997

  
1998
    return self._SendRequest(HTTP_PUT,
1999
                             ("/%s/query/%s" %
2000
                              (GANETI_RAPI_VERSION, what)), None, body)
2001

  
2002
  def QueryFields(self, what, fields=None):
2003
    """Retrieves available fields for a resource.
2004

  
2005
    @type what: string
2006
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2007
    @type fields: list of string
2008
    @param fields: Requested fields
2009

  
2010
    @rtype: string
2011
    @return: job id
2012

  
2013
    """
2014
    query = []
2015

  
2016
    if fields is not None:
2017
      _AppendIf(query, True, ("fields", ",".join(fields)))
2018

  
2019
    return self._SendRequest(HTTP_GET,
2020
                             ("/%s/query/%s/fields" %
2021
                              (GANETI_RAPI_VERSION, what)), query, None)

Also available in: Unified diff