Expose bulk parameter for GetJobs in RAPI client
[ganeti-local] / lib / rapi / client.py
1 #
2 #
3
4 # Copyright (C) 2010, 2011, 2012 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Ganeti RAPI client.
23
24 @attention: To use the RAPI client, the application B{must} call
25             C{pycurl.global_init} during initialization and
26             C{pycurl.global_cleanup} before exiting the process. This is very
27             important in multi-threaded programs. See curl_global_init(3) and
28             curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
29             can be used.
30
31 """
32
33 # No Ganeti-specific modules should be imported. The RAPI client is supposed to
34 # be standalone.
35
36 import logging
37 import simplejson
38 import socket
39 import urllib
40 import threading
41 import pycurl
42 import time
43
44 try:
45   from cStringIO import StringIO
46 except ImportError:
47   from StringIO import StringIO
48
49
50 GANETI_RAPI_PORT = 5080
51 GANETI_RAPI_VERSION = 2
52
53 HTTP_DELETE = "DELETE"
54 HTTP_GET = "GET"
55 HTTP_PUT = "PUT"
56 HTTP_POST = "POST"
57 HTTP_OK = 200
58 HTTP_NOT_FOUND = 404
59 HTTP_APP_JSON = "application/json"
60
61 REPLACE_DISK_PRI = "replace_on_primary"
62 REPLACE_DISK_SECONDARY = "replace_on_secondary"
63 REPLACE_DISK_CHG = "replace_new_secondary"
64 REPLACE_DISK_AUTO = "replace_auto"
65
66 NODE_EVAC_PRI = "primary-only"
67 NODE_EVAC_SEC = "secondary-only"
68 NODE_EVAC_ALL = "all"
69
70 NODE_ROLE_DRAINED = "drained"
71 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
72 NODE_ROLE_MASTER = "master"
73 NODE_ROLE_OFFLINE = "offline"
74 NODE_ROLE_REGULAR = "regular"
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_PENDING = frozenset([
84   JOB_STATUS_QUEUED,
85   JOB_STATUS_WAITING,
86   JOB_STATUS_CANCELING,
87   ])
88 JOB_STATUS_FINALIZED = frozenset([
89   JOB_STATUS_CANCELED,
90   JOB_STATUS_SUCCESS,
91   JOB_STATUS_ERROR,
92   ])
93 JOB_STATUS_ALL = frozenset([
94   JOB_STATUS_RUNNING,
95   ]) | JOB_STATUS_PENDING | JOB_STATUS_FINALIZED
96
97 # Legacy name
98 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
99
100 # Internal constants
101 _REQ_DATA_VERSION_FIELD = "__version__"
102 _QPARAM_DRY_RUN = "dry-run"
103 _QPARAM_FORCE = "force"
104
105 # Feature strings
106 INST_CREATE_REQV1 = "instance-create-reqv1"
107 INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
108 NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
109 NODE_EVAC_RES1 = "node-evac-res1"
110
111 # Old feature constant names in case they're references by users of this module
112 _INST_CREATE_REQV1 = INST_CREATE_REQV1
113 _INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
114 _NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
115 _NODE_EVAC_RES1 = NODE_EVAC_RES1
116
117 #: Resolver errors
118 ECODE_RESOLVER = "resolver_error"
119
120 #: Not enough resources (iallocator failure, disk space, memory, etc.)
121 ECODE_NORES = "insufficient_resources"
122
123 #: Temporarily out of resources; operation can be tried again
124 ECODE_TEMP_NORES = "temp_insufficient_resources"
125
126 #: Wrong arguments (at syntax level)
127 ECODE_INVAL = "wrong_input"
128
129 #: Wrong entity state
130 ECODE_STATE = "wrong_state"
131
132 #: Entity not found
133 ECODE_NOENT = "unknown_entity"
134
135 #: Entity already exists
136 ECODE_EXISTS = "already_exists"
137
138 #: Resource not unique (e.g. MAC or IP duplication)
139 ECODE_NOTUNIQUE = "resource_not_unique"
140
141 #: Internal cluster error
142 ECODE_FAULT = "internal_error"
143
144 #: Environment error (e.g. node disk error)
145 ECODE_ENVIRON = "environment_error"
146
147 #: List of all failure types
148 ECODE_ALL = frozenset([
149   ECODE_RESOLVER,
150   ECODE_NORES,
151   ECODE_TEMP_NORES,
152   ECODE_INVAL,
153   ECODE_STATE,
154   ECODE_NOENT,
155   ECODE_EXISTS,
156   ECODE_NOTUNIQUE,
157   ECODE_FAULT,
158   ECODE_ENVIRON,
159   ])
160
161 # Older pycURL versions don't have all error constants
162 try:
163   _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
164   _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
165 except AttributeError:
166   _CURLE_SSL_CACERT = 60
167   _CURLE_SSL_CACERT_BADFILE = 77
168
169 _CURL_SSL_CERT_ERRORS = frozenset([
170   _CURLE_SSL_CACERT,
171   _CURLE_SSL_CACERT_BADFILE,
172   ])
173
174
175 class Error(Exception):
176   """Base error class for this module.
177
178   """
179   pass
180
181
182 class GanetiApiError(Error):
183   """Generic error raised from Ganeti API.
184
185   """
186   def __init__(self, msg, code=None):
187     Error.__init__(self, msg)
188     self.code = code
189
190
191 class CertificateError(GanetiApiError):
192   """Raised when a problem is found with the SSL certificate.
193
194   """
195   pass
196
197
198 def _AppendIf(container, condition, value):
199   """Appends to a list if a condition evaluates to truth.
200
201   """
202   if condition:
203     container.append(value)
204
205   return condition
206
207
208 def _AppendDryRunIf(container, condition):
209   """Appends a "dry-run" parameter if a condition evaluates to truth.
210
211   """
212   return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
213
214
215 def _AppendForceIf(container, condition):
216   """Appends a "force" parameter if a condition evaluates to truth.
217
218   """
219   return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
220
221
222 def _SetItemIf(container, condition, item, value):
223   """Sets an item if a condition evaluates to truth.
224
225   """
226   if condition:
227     container[item] = value
228
229   return condition
230
231
232 def UsesRapiClient(fn):
233   """Decorator for code using RAPI client to initialize pycURL.
234
235   """
236   def wrapper(*args, **kwargs):
237     # curl_global_init(3) and curl_global_cleanup(3) must be called with only
238     # one thread running. This check is just a safety measure -- it doesn't
239     # cover all cases.
240     assert threading.activeCount() == 1, \
241            "Found active threads when initializing pycURL"
242
243     pycurl.global_init(pycurl.GLOBAL_ALL)
244     try:
245       return fn(*args, **kwargs)
246     finally:
247       pycurl.global_cleanup()
248
249   return wrapper
250
251
252 def GenericCurlConfig(verbose=False, use_signal=False,
253                       use_curl_cabundle=False, cafile=None, capath=None,
254                       proxy=None, verify_hostname=False,
255                       connect_timeout=None, timeout=None,
256                       _pycurl_version_fn=pycurl.version_info):
257   """Curl configuration function generator.
258
259   @type verbose: bool
260   @param verbose: Whether to set cURL to verbose mode
261   @type use_signal: bool
262   @param use_signal: Whether to allow cURL to use signals
263   @type use_curl_cabundle: bool
264   @param use_curl_cabundle: Whether to use cURL's default CA bundle
265   @type cafile: string
266   @param cafile: In which file we can find the certificates
267   @type capath: string
268   @param capath: In which directory we can find the certificates
269   @type proxy: string
270   @param proxy: Proxy to use, None for default behaviour and empty string for
271                 disabling proxies (see curl_easy_setopt(3))
272   @type verify_hostname: bool
273   @param verify_hostname: Whether to verify the remote peer certificate's
274                           commonName
275   @type connect_timeout: number
276   @param connect_timeout: Timeout for establishing connection in seconds
277   @type timeout: number
278   @param timeout: Timeout for complete transfer in seconds (see
279                   curl_easy_setopt(3)).
280
281   """
282   if use_curl_cabundle and (cafile or capath):
283     raise Error("Can not use default CA bundle when CA file or path is set")
284
285   def _ConfigCurl(curl, logger):
286     """Configures a cURL object
287
288     @type curl: pycurl.Curl
289     @param curl: cURL object
290
291     """
292     logger.debug("Using cURL version %s", pycurl.version)
293
294     # pycurl.version_info returns a tuple with information about the used
295     # version of libcurl. Item 5 is the SSL library linked to it.
296     # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
297     # 0, '1.2.3.3', ...)
298     sslver = _pycurl_version_fn()[5]
299     if not sslver:
300       raise Error("No SSL support in cURL")
301
302     lcsslver = sslver.lower()
303     if lcsslver.startswith("openssl/"):
304       pass
305     elif lcsslver.startswith("nss/"):
306       # TODO: investigate compatibility beyond a simple test
307       pass
308     elif lcsslver.startswith("gnutls/"):
309       if capath:
310         raise Error("cURL linked against GnuTLS has no support for a"
311                     " CA path (%s)" % (pycurl.version, ))
312     else:
313       raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
314                                 sslver)
315
316     curl.setopt(pycurl.VERBOSE, verbose)
317     curl.setopt(pycurl.NOSIGNAL, not use_signal)
318
319     # Whether to verify remote peer's CN
320     if verify_hostname:
321       # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
322       # certificate must indicate that the server is the server to which you
323       # meant to connect, or the connection fails. [...] When the value is 1,
324       # the certificate must contain a Common Name field, but it doesn't matter
325       # what name it says. [...]"
326       curl.setopt(pycurl.SSL_VERIFYHOST, 2)
327     else:
328       curl.setopt(pycurl.SSL_VERIFYHOST, 0)
329
330     if cafile or capath or use_curl_cabundle:
331       # Require certificates to be checked
332       curl.setopt(pycurl.SSL_VERIFYPEER, True)
333       if cafile:
334         curl.setopt(pycurl.CAINFO, str(cafile))
335       if capath:
336         curl.setopt(pycurl.CAPATH, str(capath))
337       # Not changing anything for using default CA bundle
338     else:
339       # Disable SSL certificate verification
340       curl.setopt(pycurl.SSL_VERIFYPEER, False)
341
342     if proxy is not None:
343       curl.setopt(pycurl.PROXY, str(proxy))
344
345     # Timeouts
346     if connect_timeout is not None:
347       curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
348     if timeout is not None:
349       curl.setopt(pycurl.TIMEOUT, timeout)
350
351   return _ConfigCurl
352
353
354 class GanetiRapiClient(object): # pylint: disable=R0904
355   """Ganeti RAPI client.
356
357   """
358   USER_AGENT = "Ganeti RAPI Client"
359   _json_encoder = simplejson.JSONEncoder(sort_keys=True)
360
361   def __init__(self, host, port=GANETI_RAPI_PORT,
362                username=None, password=None, logger=logging,
363                curl_config_fn=None, curl_factory=None):
364     """Initializes this class.
365
366     @type host: string
367     @param host: the ganeti cluster master to interact with
368     @type port: int
369     @param port: the port on which the RAPI is running (default is 5080)
370     @type username: string
371     @param username: the username to connect with
372     @type password: string
373     @param password: the password to connect with
374     @type curl_config_fn: callable
375     @param curl_config_fn: Function to configure C{pycurl.Curl} object
376     @param logger: Logging object
377
378     """
379     self._username = username
380     self._password = password
381     self._logger = logger
382     self._curl_config_fn = curl_config_fn
383     self._curl_factory = curl_factory
384
385     try:
386       socket.inet_pton(socket.AF_INET6, host)
387       address = "[%s]:%s" % (host, port)
388     except socket.error:
389       address = "%s:%s" % (host, port)
390
391     self._base_url = "https://%s" % address
392
393     if username is not None:
394       if password is None:
395         raise Error("Password not specified")
396     elif password:
397       raise Error("Specified password without username")
398
399   def _CreateCurl(self):
400     """Creates a cURL object.
401
402     """
403     # Create pycURL object if no factory is provided
404     if self._curl_factory:
405       curl = self._curl_factory()
406     else:
407       curl = pycurl.Curl()
408
409     # Default cURL settings
410     curl.setopt(pycurl.VERBOSE, False)
411     curl.setopt(pycurl.FOLLOWLOCATION, False)
412     curl.setopt(pycurl.MAXREDIRS, 5)
413     curl.setopt(pycurl.NOSIGNAL, True)
414     curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
415     curl.setopt(pycurl.SSL_VERIFYHOST, 0)
416     curl.setopt(pycurl.SSL_VERIFYPEER, False)
417     curl.setopt(pycurl.HTTPHEADER, [
418       "Accept: %s" % HTTP_APP_JSON,
419       "Content-type: %s" % HTTP_APP_JSON,
420       ])
421
422     assert ((self._username is None and self._password is None) ^
423             (self._username is not None and self._password is not None))
424
425     if self._username:
426       # Setup authentication
427       curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
428       curl.setopt(pycurl.USERPWD,
429                   str("%s:%s" % (self._username, self._password)))
430
431     # Call external configuration function
432     if self._curl_config_fn:
433       self._curl_config_fn(curl, self._logger)
434
435     return curl
436
437   @staticmethod
438   def _EncodeQuery(query):
439     """Encode query values for RAPI URL.
440
441     @type query: list of two-tuples
442     @param query: Query arguments
443     @rtype: list
444     @return: Query list with encoded values
445
446     """
447     result = []
448
449     for name, value in query:
450       if value is None:
451         result.append((name, ""))
452
453       elif isinstance(value, bool):
454         # Boolean values must be encoded as 0 or 1
455         result.append((name, int(value)))
456
457       elif isinstance(value, (list, tuple, dict)):
458         raise ValueError("Invalid query data type %r" % type(value).__name__)
459
460       else:
461         result.append((name, value))
462
463     return result
464
465   def _SendRequest(self, method, path, query, content):
466     """Sends an HTTP request.
467
468     This constructs a full URL, encodes and decodes HTTP bodies, and
469     handles invalid responses in a pythonic way.
470
471     @type method: string
472     @param method: HTTP method to use
473     @type path: string
474     @param path: HTTP URL path
475     @type query: list of two-tuples
476     @param query: query arguments to pass to urllib.urlencode
477     @type content: str or None
478     @param content: HTTP body content
479
480     @rtype: str
481     @return: JSON-Decoded response
482
483     @raises CertificateError: If an invalid SSL certificate is found
484     @raises GanetiApiError: If an invalid response is returned
485
486     """
487     assert path.startswith("/")
488
489     curl = self._CreateCurl()
490
491     if content is not None:
492       encoded_content = self._json_encoder.encode(content)
493     else:
494       encoded_content = ""
495
496     # Build URL
497     urlparts = [self._base_url, path]
498     if query:
499       urlparts.append("?")
500       urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
501
502     url = "".join(urlparts)
503
504     self._logger.debug("Sending request %s %s (content=%r)",
505                        method, url, encoded_content)
506
507     # Buffer for response
508     encoded_resp_body = StringIO()
509
510     # Configure cURL
511     curl.setopt(pycurl.CUSTOMREQUEST, str(method))
512     curl.setopt(pycurl.URL, str(url))
513     curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
514     curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
515
516     try:
517       # Send request and wait for response
518       try:
519         curl.perform()
520       except pycurl.error, err:
521         if err.args[0] in _CURL_SSL_CERT_ERRORS:
522           raise CertificateError("SSL certificate error %s" % err,
523                                  code=err.args[0])
524
525         raise GanetiApiError(str(err), code=err.args[0])
526     finally:
527       # Reset settings to not keep references to large objects in memory
528       # between requests
529       curl.setopt(pycurl.POSTFIELDS, "")
530       curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
531
532     # Get HTTP response code
533     http_code = curl.getinfo(pycurl.RESPONSE_CODE)
534
535     # Was anything written to the response buffer?
536     if encoded_resp_body.tell():
537       response_content = simplejson.loads(encoded_resp_body.getvalue())
538     else:
539       response_content = None
540
541     if http_code != HTTP_OK:
542       if isinstance(response_content, dict):
543         msg = ("%s %s: %s" %
544                (response_content["code"],
545                 response_content["message"],
546                 response_content["explain"]))
547       else:
548         msg = str(response_content)
549
550       raise GanetiApiError(msg, code=http_code)
551
552     return response_content
553
554   def GetVersion(self):
555     """Gets the Remote API version running on the cluster.
556
557     @rtype: int
558     @return: Ganeti Remote API version
559
560     """
561     return self._SendRequest(HTTP_GET, "/version", None, None)
562
563   def GetFeatures(self):
564     """Gets the list of optional features supported by RAPI server.
565
566     @rtype: list
567     @return: List of optional features
568
569     """
570     try:
571       return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
572                                None, None)
573     except GanetiApiError, err:
574       # Older RAPI servers don't support this resource
575       if err.code == HTTP_NOT_FOUND:
576         return []
577
578       raise
579
580   def GetOperatingSystems(self):
581     """Gets the Operating Systems running in the Ganeti cluster.
582
583     @rtype: list of str
584     @return: operating systems
585
586     """
587     return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
588                              None, None)
589
590   def GetInfo(self):
591     """Gets info about the cluster.
592
593     @rtype: dict
594     @return: information about the cluster
595
596     """
597     return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
598                              None, None)
599
600   def RedistributeConfig(self):
601     """Tells the cluster to redistribute its configuration files.
602
603     @rtype: string
604     @return: job id
605
606     """
607     return self._SendRequest(HTTP_PUT,
608                              "/%s/redistribute-config" % GANETI_RAPI_VERSION,
609                              None, None)
610
611   def ModifyCluster(self, **kwargs):
612     """Modifies cluster parameters.
613
614     More details for parameters can be found in the RAPI documentation.
615
616     @rtype: string
617     @return: job id
618
619     """
620     body = kwargs
621
622     return self._SendRequest(HTTP_PUT,
623                              "/%s/modify" % GANETI_RAPI_VERSION, None, body)
624
625   def GetClusterTags(self):
626     """Gets the cluster tags.
627
628     @rtype: list of str
629     @return: cluster tags
630
631     """
632     return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
633                              None, None)
634
635   def AddClusterTags(self, tags, dry_run=False):
636     """Adds tags to the cluster.
637
638     @type tags: list of str
639     @param tags: tags to add to the cluster
640     @type dry_run: bool
641     @param dry_run: whether to perform a dry run
642
643     @rtype: string
644     @return: job id
645
646     """
647     query = [("tag", t) for t in tags]
648     _AppendDryRunIf(query, dry_run)
649
650     return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
651                              query, None)
652
653   def DeleteClusterTags(self, tags, dry_run=False):
654     """Deletes tags from the cluster.
655
656     @type tags: list of str
657     @param tags: tags to delete
658     @type dry_run: bool
659     @param dry_run: whether to perform a dry run
660     @rtype: string
661     @return: job id
662
663     """
664     query = [("tag", t) for t in tags]
665     _AppendDryRunIf(query, dry_run)
666
667     return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
668                              query, None)
669
670   def GetInstances(self, bulk=False):
671     """Gets information about instances on the cluster.
672
673     @type bulk: bool
674     @param bulk: whether to return all information about all instances
675
676     @rtype: list of dict or list of str
677     @return: if bulk is True, info about the instances, else a list of instances
678
679     """
680     query = []
681     _AppendIf(query, bulk, ("bulk", 1))
682
683     instances = self._SendRequest(HTTP_GET,
684                                   "/%s/instances" % GANETI_RAPI_VERSION,
685                                   query, None)
686     if bulk:
687       return instances
688     else:
689       return [i["id"] for i in instances]
690
691   def GetInstance(self, instance):
692     """Gets information about an instance.
693
694     @type instance: str
695     @param instance: instance whose info to return
696
697     @rtype: dict
698     @return: info about the instance
699
700     """
701     return self._SendRequest(HTTP_GET,
702                              ("/%s/instances/%s" %
703                               (GANETI_RAPI_VERSION, instance)), None, None)
704
705   def GetInstanceInfo(self, instance, static=None):
706     """Gets information about an instance.
707
708     @type instance: string
709     @param instance: Instance name
710     @rtype: string
711     @return: Job ID
712
713     """
714     if static is not None:
715       query = [("static", static)]
716     else:
717       query = None
718
719     return self._SendRequest(HTTP_GET,
720                              ("/%s/instances/%s/info" %
721                               (GANETI_RAPI_VERSION, instance)), query, None)
722
723   @staticmethod
724   def _UpdateWithKwargs(base, **kwargs):
725     """Updates the base with params from kwargs.
726
727     @param base: The base dict, filled with required fields
728
729     @note: This is an inplace update of base
730
731     """
732     conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
733     if conflicts:
734       raise GanetiApiError("Required fields can not be specified as"
735                            " keywords: %s" % ", ".join(conflicts))
736
737     base.update((key, value) for key, value in kwargs.iteritems()
738                 if key != "dry_run")
739
740   def InstanceAllocation(self, mode, name, disk_template, disks, nics,
741                          **kwargs):
742     """Generates an instance allocation as used by multiallocate.
743
744     More details for parameters can be found in the RAPI documentation.
745     It is the same as used by CreateInstance.
746
747     @type mode: string
748     @param mode: Instance creation mode
749     @type name: string
750     @param name: Hostname of the instance to create
751     @type disk_template: string
752     @param disk_template: Disk template for instance (e.g. plain, diskless,
753                           file, or drbd)
754     @type disks: list of dicts
755     @param disks: List of disk definitions
756     @type nics: list of dicts
757     @param nics: List of NIC definitions
758
759     @return: A dict with the generated entry
760
761     """
762     # All required fields for request data version 1
763     alloc = {
764       "mode": mode,
765       "name": name,
766       "disk_template": disk_template,
767       "disks": disks,
768       "nics": nics,
769       }
770
771     self._UpdateWithKwargs(alloc, **kwargs)
772
773     return alloc
774
775   def InstancesMultiAlloc(self, instances, **kwargs):
776     """Tries to allocate multiple instances.
777
778     More details for parameters can be found in the RAPI documentation.
779
780     @param instances: A list of L{InstanceAllocation} results
781
782     """
783     query = []
784     body = {
785       "instances": instances,
786       }
787     self._UpdateWithKwargs(body, **kwargs)
788
789     _AppendDryRunIf(query, kwargs.get("dry_run"))
790
791     return self._SendRequest(HTTP_POST,
792                              "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
793                              query, body)
794
795   def CreateInstance(self, mode, name, disk_template, disks, nics,
796                      **kwargs):
797     """Creates a new instance.
798
799     More details for parameters can be found in the RAPI documentation.
800
801     @type mode: string
802     @param mode: Instance creation mode
803     @type name: string
804     @param name: Hostname of the instance to create
805     @type disk_template: string
806     @param disk_template: Disk template for instance (e.g. plain, diskless,
807                           file, or drbd)
808     @type disks: list of dicts
809     @param disks: List of disk definitions
810     @type nics: list of dicts
811     @param nics: List of NIC definitions
812     @type dry_run: bool
813     @keyword dry_run: whether to perform a dry run
814
815     @rtype: string
816     @return: job id
817
818     """
819     query = []
820
821     _AppendDryRunIf(query, kwargs.get("dry_run"))
822
823     if _INST_CREATE_REQV1 in self.GetFeatures():
824       body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
825                                      **kwargs)
826       body[_REQ_DATA_VERSION_FIELD] = 1
827     else:
828       raise GanetiApiError("Server does not support new-style (version 1)"
829                            " instance creation requests")
830
831     return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
832                              query, body)
833
834   def DeleteInstance(self, instance, dry_run=False):
835     """Deletes an instance.
836
837     @type instance: str
838     @param instance: the instance to delete
839
840     @rtype: string
841     @return: job id
842
843     """
844     query = []
845     _AppendDryRunIf(query, dry_run)
846
847     return self._SendRequest(HTTP_DELETE,
848                              ("/%s/instances/%s" %
849                               (GANETI_RAPI_VERSION, instance)), query, None)
850
851   def ModifyInstance(self, instance, **kwargs):
852     """Modifies an instance.
853
854     More details for parameters can be found in the RAPI documentation.
855
856     @type instance: string
857     @param instance: Instance name
858     @rtype: string
859     @return: job id
860
861     """
862     body = kwargs
863
864     return self._SendRequest(HTTP_PUT,
865                              ("/%s/instances/%s/modify" %
866                               (GANETI_RAPI_VERSION, instance)), None, body)
867
868   def ActivateInstanceDisks(self, instance, ignore_size=None):
869     """Activates an instance's disks.
870
871     @type instance: string
872     @param instance: Instance name
873     @type ignore_size: bool
874     @param ignore_size: Whether to ignore recorded size
875     @rtype: string
876     @return: job id
877
878     """
879     query = []
880     _AppendIf(query, ignore_size, ("ignore_size", 1))
881
882     return self._SendRequest(HTTP_PUT,
883                              ("/%s/instances/%s/activate-disks" %
884                               (GANETI_RAPI_VERSION, instance)), query, None)
885
886   def DeactivateInstanceDisks(self, instance):
887     """Deactivates an instance's disks.
888
889     @type instance: string
890     @param instance: Instance name
891     @rtype: string
892     @return: job id
893
894     """
895     return self._SendRequest(HTTP_PUT,
896                              ("/%s/instances/%s/deactivate-disks" %
897                               (GANETI_RAPI_VERSION, instance)), None, None)
898
899   def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
900     """Recreate an instance's disks.
901
902     @type instance: string
903     @param instance: Instance name
904     @type disks: list of int
905     @param disks: List of disk indexes
906     @type nodes: list of string
907     @param nodes: New instance nodes, if relocation is desired
908     @rtype: string
909     @return: job id
910
911     """
912     body = {}
913     _SetItemIf(body, disks is not None, "disks", disks)
914     _SetItemIf(body, nodes is not None, "nodes", nodes)
915
916     return self._SendRequest(HTTP_POST,
917                              ("/%s/instances/%s/recreate-disks" %
918                               (GANETI_RAPI_VERSION, instance)), None, body)
919
920   def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
921     """Grows a disk of an instance.
922
923     More details for parameters can be found in the RAPI documentation.
924
925     @type instance: string
926     @param instance: Instance name
927     @type disk: integer
928     @param disk: Disk index
929     @type amount: integer
930     @param amount: Grow disk by this amount (MiB)
931     @type wait_for_sync: bool
932     @param wait_for_sync: Wait for disk to synchronize
933     @rtype: string
934     @return: job id
935
936     """
937     body = {
938       "amount": amount,
939       }
940
941     _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
942
943     return self._SendRequest(HTTP_POST,
944                              ("/%s/instances/%s/disk/%s/grow" %
945                               (GANETI_RAPI_VERSION, instance, disk)),
946                              None, body)
947
948   def GetInstanceTags(self, instance):
949     """Gets tags for an instance.
950
951     @type instance: str
952     @param instance: instance whose tags to return
953
954     @rtype: list of str
955     @return: tags for the instance
956
957     """
958     return self._SendRequest(HTTP_GET,
959                              ("/%s/instances/%s/tags" %
960                               (GANETI_RAPI_VERSION, instance)), None, None)
961
962   def AddInstanceTags(self, instance, tags, dry_run=False):
963     """Adds tags to an instance.
964
965     @type instance: str
966     @param instance: instance to add tags to
967     @type tags: list of str
968     @param tags: tags to add to the instance
969     @type dry_run: bool
970     @param dry_run: whether to perform a dry run
971
972     @rtype: string
973     @return: job id
974
975     """
976     query = [("tag", t) for t in tags]
977     _AppendDryRunIf(query, dry_run)
978
979     return self._SendRequest(HTTP_PUT,
980                              ("/%s/instances/%s/tags" %
981                               (GANETI_RAPI_VERSION, instance)), query, None)
982
983   def DeleteInstanceTags(self, instance, tags, dry_run=False):
984     """Deletes tags from an instance.
985
986     @type instance: str
987     @param instance: instance to delete tags from
988     @type tags: list of str
989     @param tags: tags to delete
990     @type dry_run: bool
991     @param dry_run: whether to perform a dry run
992     @rtype: string
993     @return: job id
994
995     """
996     query = [("tag", t) for t in tags]
997     _AppendDryRunIf(query, dry_run)
998
999     return self._SendRequest(HTTP_DELETE,
1000                              ("/%s/instances/%s/tags" %
1001                               (GANETI_RAPI_VERSION, instance)), query, None)
1002
1003   def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1004                      dry_run=False, reason=None):
1005     """Reboots an instance.
1006
1007     @type instance: str
1008     @param instance: instance to reboot
1009     @type reboot_type: str
1010     @param reboot_type: one of: hard, soft, full
1011     @type ignore_secondaries: bool
1012     @param ignore_secondaries: if True, ignores errors for the secondary node
1013         while re-assembling disks (in hard-reboot mode only)
1014     @type dry_run: bool
1015     @param dry_run: whether to perform a dry run
1016     @type reason: string
1017     @param reason: the reason for the reboot
1018     @rtype: string
1019     @return: job id
1020
1021     """
1022     query = []
1023     _AppendDryRunIf(query, dry_run)
1024     _AppendIf(query, reboot_type, ("type", reboot_type))
1025     _AppendIf(query, ignore_secondaries is not None,
1026               ("ignore_secondaries", ignore_secondaries))
1027     _AppendIf(query, reason, ("reason", reason))
1028
1029     return self._SendRequest(HTTP_POST,
1030                              ("/%s/instances/%s/reboot" %
1031                               (GANETI_RAPI_VERSION, instance)), query, None)
1032
1033   def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1034                        reason=None, **kwargs):
1035     """Shuts down an instance.
1036
1037     @type instance: str
1038     @param instance: the instance to shut down
1039     @type dry_run: bool
1040     @param dry_run: whether to perform a dry run
1041     @type no_remember: bool
1042     @param no_remember: if true, will not record the state change
1043     @type reason: string
1044     @param reason: the reason for the shutdown
1045     @rtype: string
1046     @return: job id
1047
1048     """
1049     query = []
1050     body = kwargs
1051
1052     _AppendDryRunIf(query, dry_run)
1053     _AppendIf(query, no_remember, ("no_remember", 1))
1054     _AppendIf(query, reason, ("reason", reason))
1055
1056     return self._SendRequest(HTTP_PUT,
1057                              ("/%s/instances/%s/shutdown" %
1058                               (GANETI_RAPI_VERSION, instance)), query, body)
1059
1060   def StartupInstance(self, instance, dry_run=False, no_remember=False,
1061                       reason=None):
1062     """Starts up an instance.
1063
1064     @type instance: str
1065     @param instance: the instance to start up
1066     @type dry_run: bool
1067     @param dry_run: whether to perform a dry run
1068     @type no_remember: bool
1069     @param no_remember: if true, will not record the state change
1070     @type reason: string
1071     @param reason: the reason for the startup
1072     @rtype: string
1073     @return: job id
1074
1075     """
1076     query = []
1077     _AppendDryRunIf(query, dry_run)
1078     _AppendIf(query, no_remember, ("no_remember", 1))
1079     _AppendIf(query, reason, ("reason", reason))
1080
1081     return self._SendRequest(HTTP_PUT,
1082                              ("/%s/instances/%s/startup" %
1083                               (GANETI_RAPI_VERSION, instance)), query, None)
1084
1085   def ReinstallInstance(self, instance, os=None, no_startup=False,
1086                         osparams=None):
1087     """Reinstalls an instance.
1088
1089     @type instance: str
1090     @param instance: The instance to reinstall
1091     @type os: str or None
1092     @param os: The operating system to reinstall. If None, the instance's
1093         current operating system will be installed again
1094     @type no_startup: bool
1095     @param no_startup: Whether to start the instance automatically
1096     @rtype: string
1097     @return: job id
1098
1099     """
1100     if _INST_REINSTALL_REQV1 in self.GetFeatures():
1101       body = {
1102         "start": not no_startup,
1103         }
1104       _SetItemIf(body, os is not None, "os", os)
1105       _SetItemIf(body, osparams is not None, "osparams", osparams)
1106       return self._SendRequest(HTTP_POST,
1107                                ("/%s/instances/%s/reinstall" %
1108                                 (GANETI_RAPI_VERSION, instance)), None, body)
1109
1110     # Use old request format
1111     if osparams:
1112       raise GanetiApiError("Server does not support specifying OS parameters"
1113                            " for instance reinstallation")
1114
1115     query = []
1116     _AppendIf(query, os, ("os", os))
1117     _AppendIf(query, no_startup, ("nostartup", 1))
1118
1119     return self._SendRequest(HTTP_POST,
1120                              ("/%s/instances/%s/reinstall" %
1121                               (GANETI_RAPI_VERSION, instance)), query, None)
1122
1123   def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1124                            remote_node=None, iallocator=None):
1125     """Replaces disks on an instance.
1126
1127     @type instance: str
1128     @param instance: instance whose disks to replace
1129     @type disks: list of ints
1130     @param disks: Indexes of disks to replace
1131     @type mode: str
1132     @param mode: replacement mode to use (defaults to replace_auto)
1133     @type remote_node: str or None
1134     @param remote_node: new secondary node to use (for use with
1135         replace_new_secondary mode)
1136     @type iallocator: str or None
1137     @param iallocator: instance allocator plugin to use (for use with
1138                        replace_auto mode)
1139
1140     @rtype: string
1141     @return: job id
1142
1143     """
1144     query = [
1145       ("mode", mode),
1146       ]
1147
1148     # TODO: Convert to body parameters
1149
1150     if disks is not None:
1151       _AppendIf(query, True,
1152                 ("disks", ",".join(str(idx) for idx in disks)))
1153
1154     _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1155     _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1156
1157     return self._SendRequest(HTTP_POST,
1158                              ("/%s/instances/%s/replace-disks" %
1159                               (GANETI_RAPI_VERSION, instance)), query, None)
1160
1161   def PrepareExport(self, instance, mode):
1162     """Prepares an instance for an export.
1163
1164     @type instance: string
1165     @param instance: Instance name
1166     @type mode: string
1167     @param mode: Export mode
1168     @rtype: string
1169     @return: Job ID
1170
1171     """
1172     query = [("mode", mode)]
1173     return self._SendRequest(HTTP_PUT,
1174                              ("/%s/instances/%s/prepare-export" %
1175                               (GANETI_RAPI_VERSION, instance)), query, None)
1176
1177   def ExportInstance(self, instance, mode, destination, shutdown=None,
1178                      remove_instance=None,
1179                      x509_key_name=None, destination_x509_ca=None):
1180     """Exports an instance.
1181
1182     @type instance: string
1183     @param instance: Instance name
1184     @type mode: string
1185     @param mode: Export mode
1186     @rtype: string
1187     @return: Job ID
1188
1189     """
1190     body = {
1191       "destination": destination,
1192       "mode": mode,
1193       }
1194
1195     _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1196     _SetItemIf(body, remove_instance is not None,
1197                "remove_instance", remove_instance)
1198     _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1199     _SetItemIf(body, destination_x509_ca is not None,
1200                "destination_x509_ca", destination_x509_ca)
1201
1202     return self._SendRequest(HTTP_PUT,
1203                              ("/%s/instances/%s/export" %
1204                               (GANETI_RAPI_VERSION, instance)), None, body)
1205
1206   def MigrateInstance(self, instance, mode=None, cleanup=None,
1207                       target_node=None):
1208     """Migrates an instance.
1209
1210     @type instance: string
1211     @param instance: Instance name
1212     @type mode: string
1213     @param mode: Migration mode
1214     @type cleanup: bool
1215     @param cleanup: Whether to clean up a previously failed migration
1216     @type target_node: string
1217     @param target_node: Target Node for externally mirrored instances
1218     @rtype: string
1219     @return: job id
1220
1221     """
1222     body = {}
1223     _SetItemIf(body, mode is not None, "mode", mode)
1224     _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1225     _SetItemIf(body, target_node is not None, "target_node", target_node)
1226
1227     return self._SendRequest(HTTP_PUT,
1228                              ("/%s/instances/%s/migrate" %
1229                               (GANETI_RAPI_VERSION, instance)), None, body)
1230
1231   def FailoverInstance(self, instance, iallocator=None,
1232                        ignore_consistency=None, target_node=None):
1233     """Does a failover of an instance.
1234
1235     @type instance: string
1236     @param instance: Instance name
1237     @type iallocator: string
1238     @param iallocator: Iallocator for deciding the target node for
1239       shared-storage instances
1240     @type ignore_consistency: bool
1241     @param ignore_consistency: Whether to ignore disk consistency
1242     @type target_node: string
1243     @param target_node: Target node for shared-storage instances
1244     @rtype: string
1245     @return: job id
1246
1247     """
1248     body = {}
1249     _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1250     _SetItemIf(body, ignore_consistency is not None,
1251                "ignore_consistency", ignore_consistency)
1252     _SetItemIf(body, target_node is not None, "target_node", target_node)
1253
1254     return self._SendRequest(HTTP_PUT,
1255                              ("/%s/instances/%s/failover" %
1256                               (GANETI_RAPI_VERSION, instance)), None, body)
1257
1258   def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1259     """Changes the name of an instance.
1260
1261     @type instance: string
1262     @param instance: Instance name
1263     @type new_name: string
1264     @param new_name: New instance name
1265     @type ip_check: bool
1266     @param ip_check: Whether to ensure instance's IP address is inactive
1267     @type name_check: bool
1268     @param name_check: Whether to ensure instance's name is resolvable
1269     @rtype: string
1270     @return: job id
1271
1272     """
1273     body = {
1274       "new_name": new_name,
1275       }
1276
1277     _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1278     _SetItemIf(body, name_check is not None, "name_check", name_check)
1279
1280     return self._SendRequest(HTTP_PUT,
1281                              ("/%s/instances/%s/rename" %
1282                               (GANETI_RAPI_VERSION, instance)), None, body)
1283
1284   def GetInstanceConsole(self, instance):
1285     """Request information for connecting to instance's console.
1286
1287     @type instance: string
1288     @param instance: Instance name
1289     @rtype: dict
1290     @return: dictionary containing information about instance's console
1291
1292     """
1293     return self._SendRequest(HTTP_GET,
1294                              ("/%s/instances/%s/console" %
1295                               (GANETI_RAPI_VERSION, instance)), None, None)
1296
1297   def GetJobs(self, bulk=False):
1298     """Gets all jobs for the cluster.
1299
1300     @type bulk: bool
1301     @param bulk: Whether to return detailed information about jobs.
1302     @rtype: list of int
1303     @return: List of job ids for the cluster or list of dicts with detailed
1304              information about the jobs if bulk parameter was true.
1305
1306     """
1307     query = []
1308     _AppendIf(query, bulk, ("bulk", 1))
1309
1310     if bulk:
1311       return self._SendRequest(HTTP_GET,
1312                                "/%s/jobs" % GANETI_RAPI_VERSION,
1313                                query, None)
1314     else:
1315       return [int(j["id"])
1316               for j in self._SendRequest(HTTP_GET,
1317                                          "/%s/jobs" % GANETI_RAPI_VERSION,
1318                                          None, None)]
1319
1320   def GetJobStatus(self, job_id):
1321     """Gets the status of a job.
1322
1323     @type job_id: string
1324     @param job_id: job id whose status to query
1325
1326     @rtype: dict
1327     @return: job status
1328
1329     """
1330     return self._SendRequest(HTTP_GET,
1331                              "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1332                              None, None)
1333
1334   def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1335     """Polls cluster for job status until completion.
1336
1337     Completion is defined as any of the following states listed in
1338     L{JOB_STATUS_FINALIZED}.
1339
1340     @type job_id: string
1341     @param job_id: job id to watch
1342     @type period: int
1343     @param period: how often to poll for status (optional, default 5s)
1344     @type retries: int
1345     @param retries: how many time to poll before giving up
1346                     (optional, default -1 means unlimited)
1347
1348     @rtype: bool
1349     @return: C{True} if job succeeded or C{False} if failed/status timeout
1350     @deprecated: It is recommended to use L{WaitForJobChange} wherever
1351       possible; L{WaitForJobChange} returns immediately after a job changed and
1352       does not use polling
1353
1354     """
1355     while retries != 0:
1356       job_result = self.GetJobStatus(job_id)
1357
1358       if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1359         return True
1360       elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1361         return False
1362
1363       if period:
1364         time.sleep(period)
1365
1366       if retries > 0:
1367         retries -= 1
1368
1369     return False
1370
1371   def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1372     """Waits for job changes.
1373
1374     @type job_id: string
1375     @param job_id: Job ID for which to wait
1376     @return: C{None} if no changes have been detected and a dict with two keys,
1377       C{job_info} and C{log_entries} otherwise.
1378     @rtype: dict
1379
1380     """
1381     body = {
1382       "fields": fields,
1383       "previous_job_info": prev_job_info,
1384       "previous_log_serial": prev_log_serial,
1385       }
1386
1387     return self._SendRequest(HTTP_GET,
1388                              "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1389                              None, body)
1390
1391   def CancelJob(self, job_id, dry_run=False):
1392     """Cancels a job.
1393
1394     @type job_id: string
1395     @param job_id: id of the job to delete
1396     @type dry_run: bool
1397     @param dry_run: whether to perform a dry run
1398     @rtype: tuple
1399     @return: tuple containing the result, and a message (bool, string)
1400
1401     """
1402     query = []
1403     _AppendDryRunIf(query, dry_run)
1404
1405     return self._SendRequest(HTTP_DELETE,
1406                              "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1407                              query, None)
1408
1409   def GetNodes(self, bulk=False):
1410     """Gets all nodes in the cluster.
1411
1412     @type bulk: bool
1413     @param bulk: whether to return all information about all instances
1414
1415     @rtype: list of dict or str
1416     @return: if bulk is true, info about nodes in the cluster,
1417         else list of nodes in the cluster
1418
1419     """
1420     query = []
1421     _AppendIf(query, bulk, ("bulk", 1))
1422
1423     nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1424                               query, None)
1425     if bulk:
1426       return nodes
1427     else:
1428       return [n["id"] for n in nodes]
1429
1430   def GetNode(self, node):
1431     """Gets information about a node.
1432
1433     @type node: str
1434     @param node: node whose info to return
1435
1436     @rtype: dict
1437     @return: info about the node
1438
1439     """
1440     return self._SendRequest(HTTP_GET,
1441                              "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1442                              None, None)
1443
1444   def EvacuateNode(self, node, iallocator=None, remote_node=None,
1445                    dry_run=False, early_release=None,
1446                    mode=None, accept_old=False):
1447     """Evacuates instances from a Ganeti node.
1448
1449     @type node: str
1450     @param node: node to evacuate
1451     @type iallocator: str or None
1452     @param iallocator: instance allocator to use
1453     @type remote_node: str
1454     @param remote_node: node to evaucate to
1455     @type dry_run: bool
1456     @param dry_run: whether to perform a dry run
1457     @type early_release: bool
1458     @param early_release: whether to enable parallelization
1459     @type mode: string
1460     @param mode: Node evacuation mode
1461     @type accept_old: bool
1462     @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1463         results
1464
1465     @rtype: string, or a list for pre-2.5 results
1466     @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1467       list of (job ID, instance name, new secondary node); if dry_run was
1468       specified, then the actual move jobs were not submitted and the job IDs
1469       will be C{None}
1470
1471     @raises GanetiApiError: if an iallocator and remote_node are both
1472         specified
1473
1474     """
1475     if iallocator and remote_node:
1476       raise GanetiApiError("Only one of iallocator or remote_node can be used")
1477
1478     query = []
1479     _AppendDryRunIf(query, dry_run)
1480
1481     if _NODE_EVAC_RES1 in self.GetFeatures():
1482       # Server supports body parameters
1483       body = {}
1484
1485       _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1486       _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1487       _SetItemIf(body, early_release is not None,
1488                  "early_release", early_release)
1489       _SetItemIf(body, mode is not None, "mode", mode)
1490     else:
1491       # Pre-2.5 request format
1492       body = None
1493
1494       if not accept_old:
1495         raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1496                              " not accept old-style results (parameter"
1497                              " accept_old)")
1498
1499       # Pre-2.5 servers can only evacuate secondaries
1500       if mode is not None and mode != NODE_EVAC_SEC:
1501         raise GanetiApiError("Server can only evacuate secondary instances")
1502
1503       _AppendIf(query, iallocator, ("iallocator", iallocator))
1504       _AppendIf(query, remote_node, ("remote_node", remote_node))
1505       _AppendIf(query, early_release, ("early_release", 1))
1506
1507     return self._SendRequest(HTTP_POST,
1508                              ("/%s/nodes/%s/evacuate" %
1509                               (GANETI_RAPI_VERSION, node)), query, body)
1510
1511   def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1512                   target_node=None):
1513     """Migrates all primary instances from a node.
1514
1515     @type node: str
1516     @param node: node to migrate
1517     @type mode: string
1518     @param mode: if passed, it will overwrite the live migration type,
1519         otherwise the hypervisor default will be used
1520     @type dry_run: bool
1521     @param dry_run: whether to perform a dry run
1522     @type iallocator: string
1523     @param iallocator: instance allocator to use
1524     @type target_node: string
1525     @param target_node: Target node for shared-storage instances
1526
1527     @rtype: string
1528     @return: job id
1529
1530     """
1531     query = []
1532     _AppendDryRunIf(query, dry_run)
1533
1534     if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1535       body = {}
1536
1537       _SetItemIf(body, mode is not None, "mode", mode)
1538       _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1539       _SetItemIf(body, target_node is not None, "target_node", target_node)
1540
1541       assert len(query) <= 1
1542
1543       return self._SendRequest(HTTP_POST,
1544                                ("/%s/nodes/%s/migrate" %
1545                                 (GANETI_RAPI_VERSION, node)), query, body)
1546     else:
1547       # Use old request format
1548       if target_node is not None:
1549         raise GanetiApiError("Server does not support specifying target node"
1550                              " for node migration")
1551
1552       _AppendIf(query, mode is not None, ("mode", mode))
1553
1554       return self._SendRequest(HTTP_POST,
1555                                ("/%s/nodes/%s/migrate" %
1556                                 (GANETI_RAPI_VERSION, node)), query, None)
1557
1558   def GetNodeRole(self, node):
1559     """Gets the current role for a node.
1560
1561     @type node: str
1562     @param node: node whose role to return
1563
1564     @rtype: str
1565     @return: the current role for a node
1566
1567     """
1568     return self._SendRequest(HTTP_GET,
1569                              ("/%s/nodes/%s/role" %
1570                               (GANETI_RAPI_VERSION, node)), None, None)
1571
1572   def SetNodeRole(self, node, role, force=False, auto_promote=None):
1573     """Sets the role for a node.
1574
1575     @type node: str
1576     @param node: the node whose role to set
1577     @type role: str
1578     @param role: the role to set for the node
1579     @type force: bool
1580     @param force: whether to force the role change
1581     @type auto_promote: bool
1582     @param auto_promote: Whether node(s) should be promoted to master candidate
1583                          if necessary
1584
1585     @rtype: string
1586     @return: job id
1587
1588     """
1589     query = []
1590     _AppendForceIf(query, force)
1591     _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1592
1593     return self._SendRequest(HTTP_PUT,
1594                              ("/%s/nodes/%s/role" %
1595                               (GANETI_RAPI_VERSION, node)), query, role)
1596
1597   def PowercycleNode(self, node, force=False):
1598     """Powercycles a node.
1599
1600     @type node: string
1601     @param node: Node name
1602     @type force: bool
1603     @param force: Whether to force the operation
1604     @rtype: string
1605     @return: job id
1606
1607     """
1608     query = []
1609     _AppendForceIf(query, force)
1610
1611     return self._SendRequest(HTTP_POST,
1612                              ("/%s/nodes/%s/powercycle" %
1613                               (GANETI_RAPI_VERSION, node)), query, None)
1614
1615   def ModifyNode(self, node, **kwargs):
1616     """Modifies a node.
1617
1618     More details for parameters can be found in the RAPI documentation.
1619
1620     @type node: string
1621     @param node: Node name
1622     @rtype: string
1623     @return: job id
1624
1625     """
1626     return self._SendRequest(HTTP_POST,
1627                              ("/%s/nodes/%s/modify" %
1628                               (GANETI_RAPI_VERSION, node)), None, kwargs)
1629
1630   def GetNodeStorageUnits(self, node, storage_type, output_fields):
1631     """Gets the storage units for a node.
1632
1633     @type node: str
1634     @param node: the node whose storage units to return
1635     @type storage_type: str
1636     @param storage_type: storage type whose units to return
1637     @type output_fields: str
1638     @param output_fields: storage type fields to return
1639
1640     @rtype: string
1641     @return: job id where results can be retrieved
1642
1643     """
1644     query = [
1645       ("storage_type", storage_type),
1646       ("output_fields", output_fields),
1647       ]
1648
1649     return self._SendRequest(HTTP_GET,
1650                              ("/%s/nodes/%s/storage" %
1651                               (GANETI_RAPI_VERSION, node)), query, None)
1652
1653   def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1654     """Modifies parameters of storage units on the node.
1655
1656     @type node: str
1657     @param node: node whose storage units to modify
1658     @type storage_type: str
1659     @param storage_type: storage type whose units to modify
1660     @type name: str
1661     @param name: name of the storage unit
1662     @type allocatable: bool or None
1663     @param allocatable: Whether to set the "allocatable" flag on the storage
1664                         unit (None=no modification, True=set, False=unset)
1665
1666     @rtype: string
1667     @return: job id
1668
1669     """
1670     query = [
1671       ("storage_type", storage_type),
1672       ("name", name),
1673       ]
1674
1675     _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1676
1677     return self._SendRequest(HTTP_PUT,
1678                              ("/%s/nodes/%s/storage/modify" %
1679                               (GANETI_RAPI_VERSION, node)), query, None)
1680
1681   def RepairNodeStorageUnits(self, node, storage_type, name):
1682     """Repairs a storage unit on the node.
1683
1684     @type node: str
1685     @param node: node whose storage units to repair
1686     @type storage_type: str
1687     @param storage_type: storage type to repair
1688     @type name: str
1689     @param name: name of the storage unit to repair
1690
1691     @rtype: string
1692     @return: job id
1693
1694     """
1695     query = [
1696       ("storage_type", storage_type),
1697       ("name", name),
1698       ]
1699
1700     return self._SendRequest(HTTP_PUT,
1701                              ("/%s/nodes/%s/storage/repair" %
1702                               (GANETI_RAPI_VERSION, node)), query, None)
1703
1704   def GetNodeTags(self, node):
1705     """Gets the tags for a node.
1706
1707     @type node: str
1708     @param node: node whose tags to return
1709
1710     @rtype: list of str
1711     @return: tags for the node
1712
1713     """
1714     return self._SendRequest(HTTP_GET,
1715                              ("/%s/nodes/%s/tags" %
1716                               (GANETI_RAPI_VERSION, node)), None, None)
1717
1718   def AddNodeTags(self, node, tags, dry_run=False):
1719     """Adds tags to a node.
1720
1721     @type node: str
1722     @param node: node to add tags to
1723     @type tags: list of str
1724     @param tags: tags to add to the node
1725     @type dry_run: bool
1726     @param dry_run: whether to perform a dry run
1727
1728     @rtype: string
1729     @return: job id
1730
1731     """
1732     query = [("tag", t) for t in tags]
1733     _AppendDryRunIf(query, dry_run)
1734
1735     return self._SendRequest(HTTP_PUT,
1736                              ("/%s/nodes/%s/tags" %
1737                               (GANETI_RAPI_VERSION, node)), query, tags)
1738
1739   def DeleteNodeTags(self, node, tags, dry_run=False):
1740     """Delete tags from a node.
1741
1742     @type node: str
1743     @param node: node to remove tags from
1744     @type tags: list of str
1745     @param tags: tags to remove from the node
1746     @type dry_run: bool
1747     @param dry_run: whether to perform a dry run
1748
1749     @rtype: string
1750     @return: job id
1751
1752     """
1753     query = [("tag", t) for t in tags]
1754     _AppendDryRunIf(query, dry_run)
1755
1756     return self._SendRequest(HTTP_DELETE,
1757                              ("/%s/nodes/%s/tags" %
1758                               (GANETI_RAPI_VERSION, node)), query, None)
1759
1760   def GetNetworks(self, bulk=False):
1761     """Gets all networks in the cluster.
1762
1763     @type bulk: bool
1764     @param bulk: whether to return all information about the networks
1765
1766     @rtype: list of dict or str
1767     @return: if bulk is true, a list of dictionaries with info about all
1768         networks in the cluster, else a list of names of those networks
1769
1770     """
1771     query = []
1772     _AppendIf(query, bulk, ("bulk", 1))
1773
1774     networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1775                                  query, None)
1776     if bulk:
1777       return networks
1778     else:
1779       return [n["name"] for n in networks]
1780
1781   def GetNetwork(self, network):
1782     """Gets information about a network.
1783
1784     @type network: str
1785     @param network: name of the network whose info to return
1786
1787     @rtype: dict
1788     @return: info about the network
1789
1790     """
1791     return self._SendRequest(HTTP_GET,
1792                              "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1793                              None, None)
1794
1795   def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1796                     gateway6=None, mac_prefix=None,
1797                     add_reserved_ips=None, tags=None, dry_run=False):
1798     """Creates a new network.
1799
1800     @type network_name: str
1801     @param network_name: the name of network to create
1802     @type dry_run: bool
1803     @param dry_run: whether to peform a dry run
1804
1805     @rtype: string
1806     @return: job id
1807
1808     """
1809     query = []
1810     _AppendDryRunIf(query, dry_run)
1811
1812     if add_reserved_ips:
1813       add_reserved_ips = add_reserved_ips.split(",")
1814
1815     if tags:
1816       tags = tags.split(",")
1817
1818     body = {
1819       "network_name": network_name,
1820       "gateway": gateway,
1821       "network": network,
1822       "gateway6": gateway6,
1823       "network6": network6,
1824       "mac_prefix": mac_prefix,
1825       "add_reserved_ips": add_reserved_ips,
1826       "tags": tags,
1827       }
1828
1829     return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1830                              query, body)
1831
1832   def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
1833     """Connects a Network to a NodeGroup with the given netparams
1834
1835     """
1836     body = {
1837       "group_name": group_name,
1838       "network_mode": mode,
1839       "network_link": link,
1840       }
1841
1842     query = []
1843     _AppendDryRunIf(query, dry_run)
1844
1845     return self._SendRequest(HTTP_PUT,
1846                              ("/%s/networks/%s/connect" %
1847                              (GANETI_RAPI_VERSION, network_name)), query, body)
1848
1849   def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1850     """Connects a Network to a NodeGroup with the given netparams
1851
1852     """
1853     body = {
1854       "group_name": group_name,
1855       }
1856
1857     query = []
1858     _AppendDryRunIf(query, dry_run)
1859
1860     return self._SendRequest(HTTP_PUT,
1861                              ("/%s/networks/%s/disconnect" %
1862                              (GANETI_RAPI_VERSION, network_name)), query, body)
1863
1864   def ModifyNetwork(self, network, **kwargs):
1865     """Modifies a network.
1866
1867     More details for parameters can be found in the RAPI documentation.
1868
1869     @type network: string
1870     @param network: Network name
1871     @rtype: string
1872     @return: job id
1873
1874     """
1875     return self._SendRequest(HTTP_PUT,
1876                              ("/%s/networks/%s/modify" %
1877                               (GANETI_RAPI_VERSION, network)), None, kwargs)
1878
1879   def DeleteNetwork(self, network, dry_run=False):
1880     """Deletes a network.
1881
1882     @type network: str
1883     @param network: the network to delete
1884     @type dry_run: bool
1885     @param dry_run: whether to peform a dry run
1886
1887     @rtype: string
1888     @return: job id
1889
1890     """
1891     query = []
1892     _AppendDryRunIf(query, dry_run)
1893
1894     return self._SendRequest(HTTP_DELETE,
1895                              ("/%s/networks/%s" %
1896                               (GANETI_RAPI_VERSION, network)), query, None)
1897
1898   def GetNetworkTags(self, network):
1899     """Gets tags for a network.
1900
1901     @type network: string
1902     @param network: Node group whose tags to return
1903
1904     @rtype: list of strings
1905     @return: tags for the network
1906
1907     """
1908     return self._SendRequest(HTTP_GET,
1909                              ("/%s/networks/%s/tags" %
1910                               (GANETI_RAPI_VERSION, network)), None, None)
1911
1912   def AddNetworkTags(self, network, tags, dry_run=False):
1913     """Adds tags to a network.
1914
1915     @type network: str
1916     @param network: network to add tags to
1917     @type tags: list of string
1918     @param tags: tags to add to the network
1919     @type dry_run: bool
1920     @param dry_run: whether to perform a dry run
1921
1922     @rtype: string
1923     @return: job id
1924
1925     """
1926     query = [("tag", t) for t in tags]
1927     _AppendDryRunIf(query, dry_run)
1928
1929     return self._SendRequest(HTTP_PUT,
1930                              ("/%s/networks/%s/tags" %
1931                               (GANETI_RAPI_VERSION, network)), query, None)
1932
1933   def DeleteNetworkTags(self, network, tags, dry_run=False):
1934     """Deletes tags from a network.
1935
1936     @type network: str
1937     @param network: network to delete tags from
1938     @type tags: list of string
1939     @param tags: tags to delete
1940     @type dry_run: bool
1941     @param dry_run: whether to perform a dry run
1942     @rtype: string
1943     @return: job id
1944
1945     """
1946     query = [("tag", t) for t in tags]
1947     _AppendDryRunIf(query, dry_run)
1948
1949     return self._SendRequest(HTTP_DELETE,
1950                              ("/%s/networks/%s/tags" %
1951                               (GANETI_RAPI_VERSION, network)), query, None)
1952
1953   def GetGroups(self, bulk=False):
1954     """Gets all node groups in the cluster.
1955
1956     @type bulk: bool
1957     @param bulk: whether to return all information about the groups
1958
1959     @rtype: list of dict or str
1960     @return: if bulk is true, a list of dictionaries with info about all node
1961         groups in the cluster, else a list of names of those node groups
1962
1963     """
1964     query = []
1965     _AppendIf(query, bulk, ("bulk", 1))
1966
1967     groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1968                                query, None)
1969     if bulk:
1970       return groups
1971     else:
1972       return [g["name"] for g in groups]
1973
1974   def GetGroup(self, group):
1975     """Gets information about a node group.
1976
1977     @type group: str
1978     @param group: name of the node group whose info to return
1979
1980     @rtype: dict
1981     @return: info about the node group
1982
1983     """
1984     return self._SendRequest(HTTP_GET,
1985                              "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1986                              None, None)
1987
1988   def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1989     """Creates a new node group.
1990
1991     @type name: str
1992     @param name: the name of node group to create
1993     @type alloc_policy: str
1994     @param alloc_policy: the desired allocation policy for the group, if any
1995     @type dry_run: bool
1996     @param dry_run: whether to peform a dry run
1997
1998     @rtype: string
1999     @return: job id
2000
2001     """
2002     query = []
2003     _AppendDryRunIf(query, dry_run)
2004
2005     body = {
2006       "name": name,
2007       "alloc_policy": alloc_policy,
2008       }
2009
2010     return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2011                              query, body)
2012
2013   def ModifyGroup(self, group, **kwargs):
2014     """Modifies a node group.
2015
2016     More details for parameters can be found in the RAPI documentation.
2017
2018     @type group: string
2019     @param group: Node group name
2020     @rtype: string
2021     @return: job id
2022
2023     """
2024     return self._SendRequest(HTTP_PUT,
2025                              ("/%s/groups/%s/modify" %
2026                               (GANETI_RAPI_VERSION, group)), None, kwargs)
2027
2028   def DeleteGroup(self, group, dry_run=False):
2029     """Deletes a node group.
2030
2031     @type group: str
2032     @param group: the node group to delete
2033     @type dry_run: bool
2034     @param dry_run: whether to peform a dry run
2035
2036     @rtype: string
2037     @return: job id
2038
2039     """
2040     query = []
2041     _AppendDryRunIf(query, dry_run)
2042
2043     return self._SendRequest(HTTP_DELETE,
2044                              ("/%s/groups/%s" %
2045                               (GANETI_RAPI_VERSION, group)), query, None)
2046
2047   def RenameGroup(self, group, new_name):
2048     """Changes the name of a node group.
2049
2050     @type group: string
2051     @param group: Node group name
2052     @type new_name: string
2053     @param new_name: New node group name
2054
2055     @rtype: string
2056     @return: job id
2057
2058     """
2059     body = {
2060       "new_name": new_name,
2061       }
2062
2063     return self._SendRequest(HTTP_PUT,
2064                              ("/%s/groups/%s/rename" %
2065                               (GANETI_RAPI_VERSION, group)), None, body)
2066
2067   def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2068     """Assigns nodes to a group.
2069
2070     @type group: string
2071     @param group: Node group name
2072     @type nodes: list of strings
2073     @param nodes: List of nodes to assign to the group
2074
2075     @rtype: string
2076     @return: job id
2077
2078     """
2079     query = []
2080     _AppendForceIf(query, force)
2081     _AppendDryRunIf(query, dry_run)
2082
2083     body = {
2084       "nodes": nodes,
2085       }
2086
2087     return self._SendRequest(HTTP_PUT,
2088                              ("/%s/groups/%s/assign-nodes" %
2089                              (GANETI_RAPI_VERSION, group)), query, body)
2090
2091   def GetGroupTags(self, group):
2092     """Gets tags for a node group.
2093
2094     @type group: string
2095     @param group: Node group whose tags to return
2096
2097     @rtype: list of strings
2098     @return: tags for the group
2099
2100     """
2101     return self._SendRequest(HTTP_GET,
2102                              ("/%s/groups/%s/tags" %
2103                               (GANETI_RAPI_VERSION, group)), None, None)
2104
2105   def AddGroupTags(self, group, tags, dry_run=False):
2106     """Adds tags to a node group.
2107
2108     @type group: str
2109     @param group: group to add tags to
2110     @type tags: list of string
2111     @param tags: tags to add to the group
2112     @type dry_run: bool
2113     @param dry_run: whether to perform a dry run
2114
2115     @rtype: string
2116     @return: job id
2117
2118     """
2119     query = [("tag", t) for t in tags]
2120     _AppendDryRunIf(query, dry_run)
2121
2122     return self._SendRequest(HTTP_PUT,
2123                              ("/%s/groups/%s/tags" %
2124                               (GANETI_RAPI_VERSION, group)), query, None)
2125
2126   def DeleteGroupTags(self, group, tags, dry_run=False):
2127     """Deletes tags from a node group.
2128
2129     @type group: str
2130     @param group: group to delete tags from
2131     @type tags: list of string
2132     @param tags: tags to delete
2133     @type dry_run: bool
2134     @param dry_run: whether to perform a dry run
2135     @rtype: string
2136     @return: job id
2137
2138     """
2139     query = [("tag", t) for t in tags]
2140     _AppendDryRunIf(query, dry_run)
2141
2142     return self._SendRequest(HTTP_DELETE,
2143                              ("/%s/groups/%s/tags" %
2144                               (GANETI_RAPI_VERSION, group)), query, None)
2145
2146   def Query(self, what, fields, qfilter=None):
2147     """Retrieves information about resources.
2148
2149     @type what: string
2150     @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2151     @type fields: list of string
2152     @param fields: Requested fields
2153     @type qfilter: None or list
2154     @param qfilter: Query filter
2155
2156     @rtype: string
2157     @return: job id
2158
2159     """
2160     body = {
2161       "fields": fields,
2162       }
2163
2164     _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2165     # TODO: remove "filter" after 2.7
2166     _SetItemIf(body, qfilter is not None, "filter", qfilter)
2167
2168     return self._SendRequest(HTTP_PUT,
2169                              ("/%s/query/%s" %
2170                               (GANETI_RAPI_VERSION, what)), None, body)
2171
2172   def QueryFields(self, what, fields=None):
2173     """Retrieves available fields for a resource.
2174
2175     @type what: string
2176     @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2177     @type fields: list of string
2178     @param fields: Requested fields
2179
2180     @rtype: string
2181     @return: job id
2182
2183     """
2184     query = []
2185
2186     if fields is not None:
2187       _AppendIf(query, True, ("fields", ",".join(fields)))
2188
2189     return self._SendRequest(HTTP_GET,
2190                              ("/%s/query/%s/fields" %
2191                               (GANETI_RAPI_VERSION, what)), query, None)