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