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