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