rapi client: add target_node to migrate instance
[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):
1005     """Reboots an instance.
1006
1007     @type instance: str
1008     @param instance: instance to rebot
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     @rtype: string
1017     @return: job id
1018
1019     """
1020     query = []
1021     _AppendDryRunIf(query, dry_run)
1022     _AppendIf(query, reboot_type, ("type", reboot_type))
1023     _AppendIf(query, ignore_secondaries is not None,
1024               ("ignore_secondaries", ignore_secondaries))
1025
1026     return self._SendRequest(HTTP_POST,
1027                              ("/%s/instances/%s/reboot" %
1028                               (GANETI_RAPI_VERSION, instance)), query, None)
1029
1030   def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1031                        **kwargs):
1032     """Shuts down an instance.
1033
1034     @type instance: str
1035     @param instance: the instance to shut down
1036     @type dry_run: bool
1037     @param dry_run: whether to perform a dry run
1038     @type no_remember: bool
1039     @param no_remember: if true, will not record the state change
1040     @rtype: string
1041     @return: job id
1042
1043     """
1044     query = []
1045     body = kwargs
1046
1047     _AppendDryRunIf(query, dry_run)
1048     _AppendIf(query, no_remember, ("no_remember", 1))
1049
1050     return self._SendRequest(HTTP_PUT,
1051                              ("/%s/instances/%s/shutdown" %
1052                               (GANETI_RAPI_VERSION, instance)), query, body)
1053
1054   def StartupInstance(self, instance, dry_run=False, no_remember=False):
1055     """Starts up an instance.
1056
1057     @type instance: str
1058     @param instance: the instance to start up
1059     @type dry_run: bool
1060     @param dry_run: whether to perform a dry run
1061     @type no_remember: bool
1062     @param no_remember: if true, will not record the state change
1063     @rtype: string
1064     @return: job id
1065
1066     """
1067     query = []
1068     _AppendDryRunIf(query, dry_run)
1069     _AppendIf(query, no_remember, ("no_remember", 1))
1070
1071     return self._SendRequest(HTTP_PUT,
1072                              ("/%s/instances/%s/startup" %
1073                               (GANETI_RAPI_VERSION, instance)), query, None)
1074
1075   def ReinstallInstance(self, instance, os=None, no_startup=False,
1076                         osparams=None):
1077     """Reinstalls an instance.
1078
1079     @type instance: str
1080     @param instance: The instance to reinstall
1081     @type os: str or None
1082     @param os: The operating system to reinstall. If None, the instance's
1083         current operating system will be installed again
1084     @type no_startup: bool
1085     @param no_startup: Whether to start the instance automatically
1086     @rtype: string
1087     @return: job id
1088
1089     """
1090     if _INST_REINSTALL_REQV1 in self.GetFeatures():
1091       body = {
1092         "start": not no_startup,
1093         }
1094       _SetItemIf(body, os is not None, "os", os)
1095       _SetItemIf(body, osparams is not None, "osparams", osparams)
1096       return self._SendRequest(HTTP_POST,
1097                                ("/%s/instances/%s/reinstall" %
1098                                 (GANETI_RAPI_VERSION, instance)), None, body)
1099
1100     # Use old request format
1101     if osparams:
1102       raise GanetiApiError("Server does not support specifying OS parameters"
1103                            " for instance reinstallation")
1104
1105     query = []
1106     _AppendIf(query, os, ("os", os))
1107     _AppendIf(query, no_startup, ("nostartup", 1))
1108
1109     return self._SendRequest(HTTP_POST,
1110                              ("/%s/instances/%s/reinstall" %
1111                               (GANETI_RAPI_VERSION, instance)), query, None)
1112
1113   def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1114                            remote_node=None, iallocator=None):
1115     """Replaces disks on an instance.
1116
1117     @type instance: str
1118     @param instance: instance whose disks to replace
1119     @type disks: list of ints
1120     @param disks: Indexes of disks to replace
1121     @type mode: str
1122     @param mode: replacement mode to use (defaults to replace_auto)
1123     @type remote_node: str or None
1124     @param remote_node: new secondary node to use (for use with
1125         replace_new_secondary mode)
1126     @type iallocator: str or None
1127     @param iallocator: instance allocator plugin to use (for use with
1128                        replace_auto mode)
1129
1130     @rtype: string
1131     @return: job id
1132
1133     """
1134     query = [
1135       ("mode", mode),
1136       ]
1137
1138     # TODO: Convert to body parameters
1139
1140     if disks is not None:
1141       _AppendIf(query, True,
1142                 ("disks", ",".join(str(idx) for idx in disks)))
1143
1144     _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1145     _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1146
1147     return self._SendRequest(HTTP_POST,
1148                              ("/%s/instances/%s/replace-disks" %
1149                               (GANETI_RAPI_VERSION, instance)), query, None)
1150
1151   def PrepareExport(self, instance, mode):
1152     """Prepares an instance for an export.
1153
1154     @type instance: string
1155     @param instance: Instance name
1156     @type mode: string
1157     @param mode: Export mode
1158     @rtype: string
1159     @return: Job ID
1160
1161     """
1162     query = [("mode", mode)]
1163     return self._SendRequest(HTTP_PUT,
1164                              ("/%s/instances/%s/prepare-export" %
1165                               (GANETI_RAPI_VERSION, instance)), query, None)
1166
1167   def ExportInstance(self, instance, mode, destination, shutdown=None,
1168                      remove_instance=None,
1169                      x509_key_name=None, destination_x509_ca=None):
1170     """Exports an instance.
1171
1172     @type instance: string
1173     @param instance: Instance name
1174     @type mode: string
1175     @param mode: Export mode
1176     @rtype: string
1177     @return: Job ID
1178
1179     """
1180     body = {
1181       "destination": destination,
1182       "mode": mode,
1183       }
1184
1185     _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1186     _SetItemIf(body, remove_instance is not None,
1187                "remove_instance", remove_instance)
1188     _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1189     _SetItemIf(body, destination_x509_ca is not None,
1190                "destination_x509_ca", destination_x509_ca)
1191
1192     return self._SendRequest(HTTP_PUT,
1193                              ("/%s/instances/%s/export" %
1194                               (GANETI_RAPI_VERSION, instance)), None, body)
1195
1196   def MigrateInstance(self, instance, mode=None, cleanup=None,
1197                       target_node=None):
1198     """Migrates an instance.
1199
1200     @type instance: string
1201     @param instance: Instance name
1202     @type mode: string
1203     @param mode: Migration mode
1204     @type cleanup: bool
1205     @param cleanup: Whether to clean up a previously failed migration
1206     @type target_node: string
1207     @param target_node: Target Node for externally mirrored instances
1208     @rtype: string
1209     @return: job id
1210
1211     """
1212     body = {}
1213     _SetItemIf(body, mode is not None, "mode", mode)
1214     _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1215     _SetItemIf(body, target_node is not None, "target_node", target_node)
1216
1217     return self._SendRequest(HTTP_PUT,
1218                              ("/%s/instances/%s/migrate" %
1219                               (GANETI_RAPI_VERSION, instance)), None, body)
1220
1221   def FailoverInstance(self, instance, iallocator=None,
1222                        ignore_consistency=None, target_node=None):
1223     """Does a failover of an instance.
1224
1225     @type instance: string
1226     @param instance: Instance name
1227     @type iallocator: string
1228     @param iallocator: Iallocator for deciding the target node for
1229       shared-storage instances
1230     @type ignore_consistency: bool
1231     @param ignore_consistency: Whether to ignore disk consistency
1232     @type target_node: string
1233     @param target_node: Target node for shared-storage instances
1234     @rtype: string
1235     @return: job id
1236
1237     """
1238     body = {}
1239     _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1240     _SetItemIf(body, ignore_consistency is not None,
1241                "ignore_consistency", ignore_consistency)
1242     _SetItemIf(body, target_node is not None, "target_node", target_node)
1243
1244     return self._SendRequest(HTTP_PUT,
1245                              ("/%s/instances/%s/failover" %
1246                               (GANETI_RAPI_VERSION, instance)), None, body)
1247
1248   def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1249     """Changes the name of an instance.
1250
1251     @type instance: string
1252     @param instance: Instance name
1253     @type new_name: string
1254     @param new_name: New instance name
1255     @type ip_check: bool
1256     @param ip_check: Whether to ensure instance's IP address is inactive
1257     @type name_check: bool
1258     @param name_check: Whether to ensure instance's name is resolvable
1259     @rtype: string
1260     @return: job id
1261
1262     """
1263     body = {
1264       "new_name": new_name,
1265       }
1266
1267     _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1268     _SetItemIf(body, name_check is not None, "name_check", name_check)
1269
1270     return self._SendRequest(HTTP_PUT,
1271                              ("/%s/instances/%s/rename" %
1272                               (GANETI_RAPI_VERSION, instance)), None, body)
1273
1274   def GetInstanceConsole(self, instance):
1275     """Request information for connecting to instance's console.
1276
1277     @type instance: string
1278     @param instance: Instance name
1279     @rtype: dict
1280     @return: dictionary containing information about instance's console
1281
1282     """
1283     return self._SendRequest(HTTP_GET,
1284                              ("/%s/instances/%s/console" %
1285                               (GANETI_RAPI_VERSION, instance)), None, None)
1286
1287   def GetJobs(self):
1288     """Gets all jobs for the cluster.
1289
1290     @rtype: list of int
1291     @return: job ids for the cluster
1292
1293     """
1294     return [int(j["id"])
1295             for j in self._SendRequest(HTTP_GET,
1296                                        "/%s/jobs" % GANETI_RAPI_VERSION,
1297                                        None, None)]
1298
1299   def GetJobStatus(self, job_id):
1300     """Gets the status of a job.
1301
1302     @type job_id: string
1303     @param job_id: job id whose status to query
1304
1305     @rtype: dict
1306     @return: job status
1307
1308     """
1309     return self._SendRequest(HTTP_GET,
1310                              "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1311                              None, None)
1312
1313   def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1314     """Polls cluster for job status until completion.
1315
1316     Completion is defined as any of the following states listed in
1317     L{JOB_STATUS_FINALIZED}.
1318
1319     @type job_id: string
1320     @param job_id: job id to watch
1321     @type period: int
1322     @param period: how often to poll for status (optional, default 5s)
1323     @type retries: int
1324     @param retries: how many time to poll before giving up
1325                     (optional, default -1 means unlimited)
1326
1327     @rtype: bool
1328     @return: C{True} if job succeeded or C{False} if failed/status timeout
1329     @deprecated: It is recommended to use L{WaitForJobChange} wherever
1330       possible; L{WaitForJobChange} returns immediately after a job changed and
1331       does not use polling
1332
1333     """
1334     while retries != 0:
1335       job_result = self.GetJobStatus(job_id)
1336
1337       if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1338         return True
1339       elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1340         return False
1341
1342       if period:
1343         time.sleep(period)
1344
1345       if retries > 0:
1346         retries -= 1
1347
1348     return False
1349
1350   def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1351     """Waits for job changes.
1352
1353     @type job_id: string
1354     @param job_id: Job ID for which to wait
1355     @return: C{None} if no changes have been detected and a dict with two keys,
1356       C{job_info} and C{log_entries} otherwise.
1357     @rtype: dict
1358
1359     """
1360     body = {
1361       "fields": fields,
1362       "previous_job_info": prev_job_info,
1363       "previous_log_serial": prev_log_serial,
1364       }
1365
1366     return self._SendRequest(HTTP_GET,
1367                              "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1368                              None, body)
1369
1370   def CancelJob(self, job_id, dry_run=False):
1371     """Cancels a job.
1372
1373     @type job_id: string
1374     @param job_id: id of the job to delete
1375     @type dry_run: bool
1376     @param dry_run: whether to perform a dry run
1377     @rtype: tuple
1378     @return: tuple containing the result, and a message (bool, string)
1379
1380     """
1381     query = []
1382     _AppendDryRunIf(query, dry_run)
1383
1384     return self._SendRequest(HTTP_DELETE,
1385                              "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1386                              query, None)
1387
1388   def GetNodes(self, bulk=False):
1389     """Gets all nodes in the cluster.
1390
1391     @type bulk: bool
1392     @param bulk: whether to return all information about all instances
1393
1394     @rtype: list of dict or str
1395     @return: if bulk is true, info about nodes in the cluster,
1396         else list of nodes in the cluster
1397
1398     """
1399     query = []
1400     _AppendIf(query, bulk, ("bulk", 1))
1401
1402     nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1403                               query, None)
1404     if bulk:
1405       return nodes
1406     else:
1407       return [n["id"] for n in nodes]
1408
1409   def GetNode(self, node):
1410     """Gets information about a node.
1411
1412     @type node: str
1413     @param node: node whose info to return
1414
1415     @rtype: dict
1416     @return: info about the node
1417
1418     """
1419     return self._SendRequest(HTTP_GET,
1420                              "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1421                              None, None)
1422
1423   def EvacuateNode(self, node, iallocator=None, remote_node=None,
1424                    dry_run=False, early_release=None,
1425                    mode=None, accept_old=False):
1426     """Evacuates instances from a Ganeti node.
1427
1428     @type node: str
1429     @param node: node to evacuate
1430     @type iallocator: str or None
1431     @param iallocator: instance allocator to use
1432     @type remote_node: str
1433     @param remote_node: node to evaucate to
1434     @type dry_run: bool
1435     @param dry_run: whether to perform a dry run
1436     @type early_release: bool
1437     @param early_release: whether to enable parallelization
1438     @type mode: string
1439     @param mode: Node evacuation mode
1440     @type accept_old: bool
1441     @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1442         results
1443
1444     @rtype: string, or a list for pre-2.5 results
1445     @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1446       list of (job ID, instance name, new secondary node); if dry_run was
1447       specified, then the actual move jobs were not submitted and the job IDs
1448       will be C{None}
1449
1450     @raises GanetiApiError: if an iallocator and remote_node are both
1451         specified
1452
1453     """
1454     if iallocator and remote_node:
1455       raise GanetiApiError("Only one of iallocator or remote_node can be used")
1456
1457     query = []
1458     _AppendDryRunIf(query, dry_run)
1459
1460     if _NODE_EVAC_RES1 in self.GetFeatures():
1461       # Server supports body parameters
1462       body = {}
1463
1464       _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1465       _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1466       _SetItemIf(body, early_release is not None,
1467                  "early_release", early_release)
1468       _SetItemIf(body, mode is not None, "mode", mode)
1469     else:
1470       # Pre-2.5 request format
1471       body = None
1472
1473       if not accept_old:
1474         raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1475                              " not accept old-style results (parameter"
1476                              " accept_old)")
1477
1478       # Pre-2.5 servers can only evacuate secondaries
1479       if mode is not None and mode != NODE_EVAC_SEC:
1480         raise GanetiApiError("Server can only evacuate secondary instances")
1481
1482       _AppendIf(query, iallocator, ("iallocator", iallocator))
1483       _AppendIf(query, remote_node, ("remote_node", remote_node))
1484       _AppendIf(query, early_release, ("early_release", 1))
1485
1486     return self._SendRequest(HTTP_POST,
1487                              ("/%s/nodes/%s/evacuate" %
1488                               (GANETI_RAPI_VERSION, node)), query, body)
1489
1490   def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1491                   target_node=None):
1492     """Migrates all primary instances from a node.
1493
1494     @type node: str
1495     @param node: node to migrate
1496     @type mode: string
1497     @param mode: if passed, it will overwrite the live migration type,
1498         otherwise the hypervisor default will be used
1499     @type dry_run: bool
1500     @param dry_run: whether to perform a dry run
1501     @type iallocator: string
1502     @param iallocator: instance allocator to use
1503     @type target_node: string
1504     @param target_node: Target node for shared-storage instances
1505
1506     @rtype: string
1507     @return: job id
1508
1509     """
1510     query = []
1511     _AppendDryRunIf(query, dry_run)
1512
1513     if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1514       body = {}
1515
1516       _SetItemIf(body, mode is not None, "mode", mode)
1517       _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1518       _SetItemIf(body, target_node is not None, "target_node", target_node)
1519
1520       assert len(query) <= 1
1521
1522       return self._SendRequest(HTTP_POST,
1523                                ("/%s/nodes/%s/migrate" %
1524                                 (GANETI_RAPI_VERSION, node)), query, body)
1525     else:
1526       # Use old request format
1527       if target_node is not None:
1528         raise GanetiApiError("Server does not support specifying target node"
1529                              " for node migration")
1530
1531       _AppendIf(query, mode is not None, ("mode", mode))
1532
1533       return self._SendRequest(HTTP_POST,
1534                                ("/%s/nodes/%s/migrate" %
1535                                 (GANETI_RAPI_VERSION, node)), query, None)
1536
1537   def GetNodeRole(self, node):
1538     """Gets the current role for a node.
1539
1540     @type node: str
1541     @param node: node whose role to return
1542
1543     @rtype: str
1544     @return: the current role for a node
1545
1546     """
1547     return self._SendRequest(HTTP_GET,
1548                              ("/%s/nodes/%s/role" %
1549                               (GANETI_RAPI_VERSION, node)), None, None)
1550
1551   def SetNodeRole(self, node, role, force=False, auto_promote=None):
1552     """Sets the role for a node.
1553
1554     @type node: str
1555     @param node: the node whose role to set
1556     @type role: str
1557     @param role: the role to set for the node
1558     @type force: bool
1559     @param force: whether to force the role change
1560     @type auto_promote: bool
1561     @param auto_promote: Whether node(s) should be promoted to master candidate
1562                          if necessary
1563
1564     @rtype: string
1565     @return: job id
1566
1567     """
1568     query = []
1569     _AppendForceIf(query, force)
1570     _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1571
1572     return self._SendRequest(HTTP_PUT,
1573                              ("/%s/nodes/%s/role" %
1574                               (GANETI_RAPI_VERSION, node)), query, role)
1575
1576   def PowercycleNode(self, node, force=False):
1577     """Powercycles a node.
1578
1579     @type node: string
1580     @param node: Node name
1581     @type force: bool
1582     @param force: Whether to force the operation
1583     @rtype: string
1584     @return: job id
1585
1586     """
1587     query = []
1588     _AppendForceIf(query, force)
1589
1590     return self._SendRequest(HTTP_POST,
1591                              ("/%s/nodes/%s/powercycle" %
1592                               (GANETI_RAPI_VERSION, node)), query, None)
1593
1594   def ModifyNode(self, node, **kwargs):
1595     """Modifies a node.
1596
1597     More details for parameters can be found in the RAPI documentation.
1598
1599     @type node: string
1600     @param node: Node name
1601     @rtype: string
1602     @return: job id
1603
1604     """
1605     return self._SendRequest(HTTP_POST,
1606                              ("/%s/nodes/%s/modify" %
1607                               (GANETI_RAPI_VERSION, node)), None, kwargs)
1608
1609   def GetNodeStorageUnits(self, node, storage_type, output_fields):
1610     """Gets the storage units for a node.
1611
1612     @type node: str
1613     @param node: the node whose storage units to return
1614     @type storage_type: str
1615     @param storage_type: storage type whose units to return
1616     @type output_fields: str
1617     @param output_fields: storage type fields to return
1618
1619     @rtype: string
1620     @return: job id where results can be retrieved
1621
1622     """
1623     query = [
1624       ("storage_type", storage_type),
1625       ("output_fields", output_fields),
1626       ]
1627
1628     return self._SendRequest(HTTP_GET,
1629                              ("/%s/nodes/%s/storage" %
1630                               (GANETI_RAPI_VERSION, node)), query, None)
1631
1632   def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1633     """Modifies parameters of storage units on the node.
1634
1635     @type node: str
1636     @param node: node whose storage units to modify
1637     @type storage_type: str
1638     @param storage_type: storage type whose units to modify
1639     @type name: str
1640     @param name: name of the storage unit
1641     @type allocatable: bool or None
1642     @param allocatable: Whether to set the "allocatable" flag on the storage
1643                         unit (None=no modification, True=set, False=unset)
1644
1645     @rtype: string
1646     @return: job id
1647
1648     """
1649     query = [
1650       ("storage_type", storage_type),
1651       ("name", name),
1652       ]
1653
1654     _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1655
1656     return self._SendRequest(HTTP_PUT,
1657                              ("/%s/nodes/%s/storage/modify" %
1658                               (GANETI_RAPI_VERSION, node)), query, None)
1659
1660   def RepairNodeStorageUnits(self, node, storage_type, name):
1661     """Repairs a storage unit on the node.
1662
1663     @type node: str
1664     @param node: node whose storage units to repair
1665     @type storage_type: str
1666     @param storage_type: storage type to repair
1667     @type name: str
1668     @param name: name of the storage unit to repair
1669
1670     @rtype: string
1671     @return: job id
1672
1673     """
1674     query = [
1675       ("storage_type", storage_type),
1676       ("name", name),
1677       ]
1678
1679     return self._SendRequest(HTTP_PUT,
1680                              ("/%s/nodes/%s/storage/repair" %
1681                               (GANETI_RAPI_VERSION, node)), query, None)
1682
1683   def GetNodeTags(self, node):
1684     """Gets the tags for a node.
1685
1686     @type node: str
1687     @param node: node whose tags to return
1688
1689     @rtype: list of str
1690     @return: tags for the node
1691
1692     """
1693     return self._SendRequest(HTTP_GET,
1694                              ("/%s/nodes/%s/tags" %
1695                               (GANETI_RAPI_VERSION, node)), None, None)
1696
1697   def AddNodeTags(self, node, tags, dry_run=False):
1698     """Adds tags to a node.
1699
1700     @type node: str
1701     @param node: node to add tags to
1702     @type tags: list of str
1703     @param tags: tags to add to the node
1704     @type dry_run: bool
1705     @param dry_run: whether to perform a dry run
1706
1707     @rtype: string
1708     @return: job id
1709
1710     """
1711     query = [("tag", t) for t in tags]
1712     _AppendDryRunIf(query, dry_run)
1713
1714     return self._SendRequest(HTTP_PUT,
1715                              ("/%s/nodes/%s/tags" %
1716                               (GANETI_RAPI_VERSION, node)), query, tags)
1717
1718   def DeleteNodeTags(self, node, tags, dry_run=False):
1719     """Delete tags from a node.
1720
1721     @type node: str
1722     @param node: node to remove tags from
1723     @type tags: list of str
1724     @param tags: tags to remove from 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_DELETE,
1736                              ("/%s/nodes/%s/tags" %
1737                               (GANETI_RAPI_VERSION, node)), query, None)
1738
1739   def GetNetworks(self, bulk=False):
1740     """Gets all networks in the cluster.
1741
1742     @type bulk: bool
1743     @param bulk: whether to return all information about the networks
1744
1745     @rtype: list of dict or str
1746     @return: if bulk is true, a list of dictionaries with info about all
1747         networks in the cluster, else a list of names of those networks
1748
1749     """
1750     query = []
1751     _AppendIf(query, bulk, ("bulk", 1))
1752
1753     networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1754                                  query, None)
1755     if bulk:
1756       return networks
1757     else:
1758       return [n["name"] for n in networks]
1759
1760   def GetNetwork(self, network):
1761     """Gets information about a network.
1762
1763     @type network: str
1764     @param network: name of the network whose info to return
1765
1766     @rtype: dict
1767     @return: info about the network
1768
1769     """
1770     return self._SendRequest(HTTP_GET,
1771                              "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1772                              None, None)
1773
1774   def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1775                     gateway6=None, mac_prefix=None,
1776                     add_reserved_ips=None, tags=None, dry_run=False):
1777     """Creates a new network.
1778
1779     @type network_name: str
1780     @param network_name: the name of network to create
1781     @type dry_run: bool
1782     @param dry_run: whether to peform a dry run
1783
1784     @rtype: string
1785     @return: job id
1786
1787     """
1788     query = []
1789     _AppendDryRunIf(query, dry_run)
1790
1791     if add_reserved_ips:
1792       add_reserved_ips = add_reserved_ips.split(",")
1793
1794     if tags:
1795       tags = tags.split(",")
1796
1797     body = {
1798       "network_name": network_name,
1799       "gateway": gateway,
1800       "network": network,
1801       "gateway6": gateway6,
1802       "network6": network6,
1803       "mac_prefix": mac_prefix,
1804       "add_reserved_ips": add_reserved_ips,
1805       "tags": tags,
1806       }
1807
1808     return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1809                              query, body)
1810
1811   def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
1812     """Connects a Network to a NodeGroup with the given netparams
1813
1814     """
1815     body = {
1816       "group_name": group_name,
1817       "network_mode": mode,
1818       "network_link": link,
1819       }
1820
1821     query = []
1822     _AppendDryRunIf(query, dry_run)
1823
1824     return self._SendRequest(HTTP_PUT,
1825                              ("/%s/networks/%s/connect" %
1826                              (GANETI_RAPI_VERSION, network_name)), query, body)
1827
1828   def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1829     """Connects a Network to a NodeGroup with the given netparams
1830
1831     """
1832     body = {
1833       "group_name": group_name,
1834       }
1835
1836     query = []
1837     _AppendDryRunIf(query, dry_run)
1838
1839     return self._SendRequest(HTTP_PUT,
1840                              ("/%s/networks/%s/disconnect" %
1841                              (GANETI_RAPI_VERSION, network_name)), query, body)
1842
1843   def ModifyNetwork(self, network, **kwargs):
1844     """Modifies a network.
1845
1846     More details for parameters can be found in the RAPI documentation.
1847
1848     @type network: string
1849     @param network: Network name
1850     @rtype: string
1851     @return: job id
1852
1853     """
1854     return self._SendRequest(HTTP_PUT,
1855                              ("/%s/networks/%s/modify" %
1856                               (GANETI_RAPI_VERSION, network)), None, kwargs)
1857
1858   def DeleteNetwork(self, network, dry_run=False):
1859     """Deletes a network.
1860
1861     @type network: str
1862     @param network: the network to delete
1863     @type dry_run: bool
1864     @param dry_run: whether to peform a dry run
1865
1866     @rtype: string
1867     @return: job id
1868
1869     """
1870     query = []
1871     _AppendDryRunIf(query, dry_run)
1872
1873     return self._SendRequest(HTTP_DELETE,
1874                              ("/%s/networks/%s" %
1875                               (GANETI_RAPI_VERSION, network)), query, None)
1876
1877   def GetNetworkTags(self, network):
1878     """Gets tags for a network.
1879
1880     @type network: string
1881     @param network: Node group whose tags to return
1882
1883     @rtype: list of strings
1884     @return: tags for the network
1885
1886     """
1887     return self._SendRequest(HTTP_GET,
1888                              ("/%s/networks/%s/tags" %
1889                               (GANETI_RAPI_VERSION, network)), None, None)
1890
1891   def AddNetworkTags(self, network, tags, dry_run=False):
1892     """Adds tags to a network.
1893
1894     @type network: str
1895     @param network: network to add tags to
1896     @type tags: list of string
1897     @param tags: tags to add to the network
1898     @type dry_run: bool
1899     @param dry_run: whether to perform a dry run
1900
1901     @rtype: string
1902     @return: job id
1903
1904     """
1905     query = [("tag", t) for t in tags]
1906     _AppendDryRunIf(query, dry_run)
1907
1908     return self._SendRequest(HTTP_PUT,
1909                              ("/%s/networks/%s/tags" %
1910                               (GANETI_RAPI_VERSION, network)), query, None)
1911
1912   def DeleteNetworkTags(self, network, tags, dry_run=False):
1913     """Deletes tags from a network.
1914
1915     @type network: str
1916     @param network: network to delete tags from
1917     @type tags: list of string
1918     @param tags: tags to delete
1919     @type dry_run: bool
1920     @param dry_run: whether to perform a dry run
1921     @rtype: string
1922     @return: job id
1923
1924     """
1925     query = [("tag", t) for t in tags]
1926     _AppendDryRunIf(query, dry_run)
1927
1928     return self._SendRequest(HTTP_DELETE,
1929                              ("/%s/networks/%s/tags" %
1930                               (GANETI_RAPI_VERSION, network)), query, None)
1931
1932   def GetGroups(self, bulk=False):
1933     """Gets all node groups in the cluster.
1934
1935     @type bulk: bool
1936     @param bulk: whether to return all information about the groups
1937
1938     @rtype: list of dict or str
1939     @return: if bulk is true, a list of dictionaries with info about all node
1940         groups in the cluster, else a list of names of those node groups
1941
1942     """
1943     query = []
1944     _AppendIf(query, bulk, ("bulk", 1))
1945
1946     groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1947                                query, None)
1948     if bulk:
1949       return groups
1950     else:
1951       return [g["name"] for g in groups]
1952
1953   def GetGroup(self, group):
1954     """Gets information about a node group.
1955
1956     @type group: str
1957     @param group: name of the node group whose info to return
1958
1959     @rtype: dict
1960     @return: info about the node group
1961
1962     """
1963     return self._SendRequest(HTTP_GET,
1964                              "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1965                              None, None)
1966
1967   def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1968     """Creates a new node group.
1969
1970     @type name: str
1971     @param name: the name of node group to create
1972     @type alloc_policy: str
1973     @param alloc_policy: the desired allocation policy for the group, if any
1974     @type dry_run: bool
1975     @param dry_run: whether to peform a dry run
1976
1977     @rtype: string
1978     @return: job id
1979
1980     """
1981     query = []
1982     _AppendDryRunIf(query, dry_run)
1983
1984     body = {
1985       "name": name,
1986       "alloc_policy": alloc_policy,
1987       }
1988
1989     return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1990                              query, body)
1991
1992   def ModifyGroup(self, group, **kwargs):
1993     """Modifies a node group.
1994
1995     More details for parameters can be found in the RAPI documentation.
1996
1997     @type group: string
1998     @param group: Node group name
1999     @rtype: string
2000     @return: job id
2001
2002     """
2003     return self._SendRequest(HTTP_PUT,
2004                              ("/%s/groups/%s/modify" %
2005                               (GANETI_RAPI_VERSION, group)), None, kwargs)
2006
2007   def DeleteGroup(self, group, dry_run=False):
2008     """Deletes a node group.
2009
2010     @type group: str
2011     @param group: the node group to delete
2012     @type dry_run: bool
2013     @param dry_run: whether to peform a dry run
2014
2015     @rtype: string
2016     @return: job id
2017
2018     """
2019     query = []
2020     _AppendDryRunIf(query, dry_run)
2021
2022     return self._SendRequest(HTTP_DELETE,
2023                              ("/%s/groups/%s" %
2024                               (GANETI_RAPI_VERSION, group)), query, None)
2025
2026   def RenameGroup(self, group, new_name):
2027     """Changes the name of a node group.
2028
2029     @type group: string
2030     @param group: Node group name
2031     @type new_name: string
2032     @param new_name: New node group name
2033
2034     @rtype: string
2035     @return: job id
2036
2037     """
2038     body = {
2039       "new_name": new_name,
2040       }
2041
2042     return self._SendRequest(HTTP_PUT,
2043                              ("/%s/groups/%s/rename" %
2044                               (GANETI_RAPI_VERSION, group)), None, body)
2045
2046   def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2047     """Assigns nodes to a group.
2048
2049     @type group: string
2050     @param group: Node group name
2051     @type nodes: list of strings
2052     @param nodes: List of nodes to assign to the group
2053
2054     @rtype: string
2055     @return: job id
2056
2057     """
2058     query = []
2059     _AppendForceIf(query, force)
2060     _AppendDryRunIf(query, dry_run)
2061
2062     body = {
2063       "nodes": nodes,
2064       }
2065
2066     return self._SendRequest(HTTP_PUT,
2067                              ("/%s/groups/%s/assign-nodes" %
2068                              (GANETI_RAPI_VERSION, group)), query, body)
2069
2070   def GetGroupTags(self, group):
2071     """Gets tags for a node group.
2072
2073     @type group: string
2074     @param group: Node group whose tags to return
2075
2076     @rtype: list of strings
2077     @return: tags for the group
2078
2079     """
2080     return self._SendRequest(HTTP_GET,
2081                              ("/%s/groups/%s/tags" %
2082                               (GANETI_RAPI_VERSION, group)), None, None)
2083
2084   def AddGroupTags(self, group, tags, dry_run=False):
2085     """Adds tags to a node group.
2086
2087     @type group: str
2088     @param group: group to add tags to
2089     @type tags: list of string
2090     @param tags: tags to add to the group
2091     @type dry_run: bool
2092     @param dry_run: whether to perform a dry run
2093
2094     @rtype: string
2095     @return: job id
2096
2097     """
2098     query = [("tag", t) for t in tags]
2099     _AppendDryRunIf(query, dry_run)
2100
2101     return self._SendRequest(HTTP_PUT,
2102                              ("/%s/groups/%s/tags" %
2103                               (GANETI_RAPI_VERSION, group)), query, None)
2104
2105   def DeleteGroupTags(self, group, tags, dry_run=False):
2106     """Deletes tags from a node group.
2107
2108     @type group: str
2109     @param group: group to delete tags from
2110     @type tags: list of string
2111     @param tags: tags to delete
2112     @type dry_run: bool
2113     @param dry_run: whether to perform a dry run
2114     @rtype: string
2115     @return: job id
2116
2117     """
2118     query = [("tag", t) for t in tags]
2119     _AppendDryRunIf(query, dry_run)
2120
2121     return self._SendRequest(HTTP_DELETE,
2122                              ("/%s/groups/%s/tags" %
2123                               (GANETI_RAPI_VERSION, group)), query, None)
2124
2125   def Query(self, what, fields, qfilter=None):
2126     """Retrieves information about resources.
2127
2128     @type what: string
2129     @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2130     @type fields: list of string
2131     @param fields: Requested fields
2132     @type qfilter: None or list
2133     @param qfilter: Query filter
2134
2135     @rtype: string
2136     @return: job id
2137
2138     """
2139     body = {
2140       "fields": fields,
2141       }
2142
2143     _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2144     # TODO: remove "filter" after 2.7
2145     _SetItemIf(body, qfilter is not None, "filter", qfilter)
2146
2147     return self._SendRequest(HTTP_PUT,
2148                              ("/%s/query/%s" %
2149                               (GANETI_RAPI_VERSION, what)), None, body)
2150
2151   def QueryFields(self, what, fields=None):
2152     """Retrieves available fields for a resource.
2153
2154     @type what: string
2155     @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2156     @type fields: list of string
2157     @param fields: Requested fields
2158
2159     @rtype: string
2160     @return: job id
2161
2162     """
2163     query = []
2164
2165     if fields is not None:
2166       _AppendIf(query, True, ("fields", ",".join(fields)))
2167
2168     return self._SendRequest(HTTP_GET,
2169                              ("/%s/query/%s/fields" %
2170                               (GANETI_RAPI_VERSION, what)), query, None)