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