Add reason parameter to RAPI client functions
[ganeti-github.git] / 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):
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 _AppendDryRunIf(query, dry_run)
898 _AppendReason(query, reason)
899
900 return self._SendRequest(HTTP_DELETE,
901 ("/%s/instances/%s" %
902 (GANETI_RAPI_VERSION, instance)), query, None)
903
904 def ModifyInstance(self, instance, reason=None, **kwargs):
905 """Modifies an instance.
906
907 More details for parameters can be found in the RAPI documentation.
908
909 @type instance: string
910 @param instance: Instance name
911 @type reason: string
912 @param reason: the reason for executing this operation
913 @rtype: string
914 @return: job id
915
916 """
917 body = kwargs
918 query = []
919 _AppendReason(query, reason)
920
921 return self._SendRequest(HTTP_PUT,
922 ("/%s/instances/%s/modify" %
923 (GANETI_RAPI_VERSION, instance)), query, body)
924
925 def ActivateInstanceDisks(self, instance, ignore_size=None, reason=None):
926 """Activates an instance's disks.
927
928 @type instance: string
929 @param instance: Instance name
930 @type ignore_size: bool
931 @param ignore_size: Whether to ignore recorded size
932 @type reason: string
933 @param reason: the reason for executing this operation
934 @rtype: string
935 @return: job id
936
937 """
938 query = []
939 _AppendIf(query, ignore_size, ("ignore_size", 1))
940 _AppendReason(query, reason)
941
942 return self._SendRequest(HTTP_PUT,
943 ("/%s/instances/%s/activate-disks" %
944 (GANETI_RAPI_VERSION, instance)), query, None)
945
946 def DeactivateInstanceDisks(self, instance, reason=None):
947 """Deactivates an instance's disks.
948
949 @type instance: string
950 @param instance: Instance name
951 @type reason: string
952 @param reason: the reason for executing this operation
953 @rtype: string
954 @return: job id
955
956 """
957 query = []
958 _AppendReason(query, reason)
959 return self._SendRequest(HTTP_PUT,
960 ("/%s/instances/%s/deactivate-disks" %
961 (GANETI_RAPI_VERSION, instance)), query, None)
962
963 def RecreateInstanceDisks(self, instance, disks=None, nodes=None,
964 reason=None):
965 """Recreate an instance's disks.
966
967 @type instance: string
968 @param instance: Instance name
969 @type disks: list of int
970 @param disks: List of disk indexes
971 @type nodes: list of string
972 @param nodes: New instance nodes, if relocation is desired
973 @type reason: string
974 @param reason: the reason for executing this operation
975 @rtype: string
976 @return: job id
977
978 """
979 body = {}
980 _SetItemIf(body, disks is not None, "disks", disks)
981 _SetItemIf(body, nodes is not None, "nodes", nodes)
982
983 query = []
984 _AppendReason(query, reason)
985
986 return self._SendRequest(HTTP_POST,
987 ("/%s/instances/%s/recreate-disks" %
988 (GANETI_RAPI_VERSION, instance)), query, body)
989
990 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
991 reason=None):
992 """Grows a disk of an instance.
993
994 More details for parameters can be found in the RAPI documentation.
995
996 @type instance: string
997 @param instance: Instance name
998 @type disk: integer
999 @param disk: Disk index
1000 @type amount: integer
1001 @param amount: Grow disk by this amount (MiB)
1002 @type wait_for_sync: bool
1003 @param wait_for_sync: Wait for disk to synchronize
1004 @type reason: string
1005 @param reason: the reason for executing this operation
1006 @rtype: string
1007 @return: job id
1008
1009 """
1010 body = {
1011 "amount": amount,
1012 }
1013
1014 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1015
1016 query = []
1017 _AppendReason(query, reason)
1018
1019 return self._SendRequest(HTTP_POST,
1020 ("/%s/instances/%s/disk/%s/grow" %
1021 (GANETI_RAPI_VERSION, instance, disk)),
1022 query, body)
1023
1024 def GetInstanceTags(self, instance, reason=None):
1025 """Gets tags for an instance.
1026
1027 @type instance: str
1028 @param instance: instance whose tags to return
1029 @type reason: string
1030 @param reason: the reason for executing this operation
1031
1032 @rtype: list of str
1033 @return: tags for the instance
1034
1035 """
1036 query = []
1037 _AppendReason(query, reason)
1038 return self._SendRequest(HTTP_GET,
1039 ("/%s/instances/%s/tags" %
1040 (GANETI_RAPI_VERSION, instance)), query, None)
1041
1042 def AddInstanceTags(self, instance, tags, dry_run=False, reason=None):
1043 """Adds tags to an instance.
1044
1045 @type instance: str
1046 @param instance: instance to add tags to
1047 @type tags: list of str
1048 @param tags: tags to add to the instance
1049 @type dry_run: bool
1050 @param dry_run: whether to perform a dry run
1051 @type reason: string
1052 @param reason: the reason for executing this operation
1053
1054 @rtype: string
1055 @return: job id
1056
1057 """
1058 query = [("tag", t) for t in tags]
1059 _AppendDryRunIf(query, dry_run)
1060 _AppendReason(query, reason)
1061
1062 return self._SendRequest(HTTP_PUT,
1063 ("/%s/instances/%s/tags" %
1064 (GANETI_RAPI_VERSION, instance)), query, None)
1065
1066 def DeleteInstanceTags(self, instance, tags, dry_run=False, reason=None):
1067 """Deletes tags from an instance.
1068
1069 @type instance: str
1070 @param instance: instance to delete tags from
1071 @type tags: list of str
1072 @param tags: tags to delete
1073 @type dry_run: bool
1074 @param dry_run: whether to perform a dry run
1075 @type reason: string
1076 @param reason: the reason for executing this operation
1077 @rtype: string
1078 @return: job id
1079
1080 """
1081 query = [("tag", t) for t in tags]
1082 _AppendDryRunIf(query, dry_run)
1083 _AppendReason(query, reason)
1084
1085 return self._SendRequest(HTTP_DELETE,
1086 ("/%s/instances/%s/tags" %
1087 (GANETI_RAPI_VERSION, instance)), query, None)
1088
1089 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1090 dry_run=False, reason=None):
1091 """Reboots an instance.
1092
1093 @type instance: str
1094 @param instance: instance to reboot
1095 @type reboot_type: str
1096 @param reboot_type: one of: hard, soft, full
1097 @type ignore_secondaries: bool
1098 @param ignore_secondaries: if True, ignores errors for the secondary node
1099 while re-assembling disks (in hard-reboot mode only)
1100 @type dry_run: bool
1101 @param dry_run: whether to perform a dry run
1102 @type reason: string
1103 @param reason: the reason for the reboot
1104 @rtype: string
1105 @return: job id
1106
1107 """
1108 query = []
1109 _AppendDryRunIf(query, dry_run)
1110 _AppendIf(query, reboot_type, ("type", reboot_type))
1111 _AppendIf(query, ignore_secondaries is not None,
1112 ("ignore_secondaries", ignore_secondaries))
1113 _AppendReason(query, reason)
1114
1115 return self._SendRequest(HTTP_POST,
1116 ("/%s/instances/%s/reboot" %
1117 (GANETI_RAPI_VERSION, instance)), query, None)
1118
1119 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1120 reason=None, **kwargs):
1121 """Shuts down an instance.
1122
1123 @type instance: str
1124 @param instance: the instance to shut down
1125 @type dry_run: bool
1126 @param dry_run: whether to perform a dry run
1127 @type no_remember: bool
1128 @param no_remember: if true, will not record the state change
1129 @type reason: string
1130 @param reason: the reason for the shutdown
1131 @rtype: string
1132 @return: job id
1133
1134 """
1135 query = []
1136 body = kwargs
1137
1138 _AppendDryRunIf(query, dry_run)
1139 _AppendIf(query, no_remember, ("no_remember", 1))
1140 _AppendReason(query, reason)
1141
1142 return self._SendRequest(HTTP_PUT,
1143 ("/%s/instances/%s/shutdown" %
1144 (GANETI_RAPI_VERSION, instance)), query, body)
1145
1146 def StartupInstance(self, instance, dry_run=False, no_remember=False,
1147 reason=None):
1148 """Starts up an instance.
1149
1150 @type instance: str
1151 @param instance: the instance to start up
1152 @type dry_run: bool
1153 @param dry_run: whether to perform a dry run
1154 @type no_remember: bool
1155 @param no_remember: if true, will not record the state change
1156 @type reason: string
1157 @param reason: the reason for the startup
1158 @rtype: string
1159 @return: job id
1160
1161 """
1162 query = []
1163 _AppendDryRunIf(query, dry_run)
1164 _AppendIf(query, no_remember, ("no_remember", 1))
1165 _AppendReason(query, reason)
1166
1167 return self._SendRequest(HTTP_PUT,
1168 ("/%s/instances/%s/startup" %
1169 (GANETI_RAPI_VERSION, instance)), query, None)
1170
1171 def ReinstallInstance(self, instance, os=None, no_startup=False,
1172 osparams=None, reason=None):
1173 """Reinstalls an instance.
1174
1175 @type instance: str
1176 @param instance: The instance to reinstall
1177 @type os: str or None
1178 @param os: The operating system to reinstall. If None, the instance's
1179 current operating system will be installed again
1180 @type no_startup: bool
1181 @param no_startup: Whether to start the instance automatically
1182 @type reason: string
1183 @param reason: the reason for executing this operation
1184 @rtype: string
1185 @return: job id
1186
1187 """
1188 query = []
1189 _AppendReason(query, reason)
1190
1191 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1192 body = {
1193 "start": not no_startup,
1194 }
1195 _SetItemIf(body, os is not None, "os", os)
1196 _SetItemIf(body, osparams is not None, "osparams", osparams)
1197 return self._SendRequest(HTTP_POST,
1198 ("/%s/instances/%s/reinstall" %
1199 (GANETI_RAPI_VERSION, instance)), query, body)
1200
1201 # Use old request format
1202 if osparams:
1203 raise GanetiApiError("Server does not support specifying OS parameters"
1204 " for instance reinstallation")
1205
1206 query = []
1207 _AppendIf(query, os, ("os", os))
1208 _AppendIf(query, no_startup, ("nostartup", 1))
1209
1210 return self._SendRequest(HTTP_POST,
1211 ("/%s/instances/%s/reinstall" %
1212 (GANETI_RAPI_VERSION, instance)), query, None)
1213
1214 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1215 remote_node=None, iallocator=None, reason=None):
1216 """Replaces disks on an instance.
1217
1218 @type instance: str
1219 @param instance: instance whose disks to replace
1220 @type disks: list of ints
1221 @param disks: Indexes of disks to replace
1222 @type mode: str
1223 @param mode: replacement mode to use (defaults to replace_auto)
1224 @type remote_node: str or None
1225 @param remote_node: new secondary node to use (for use with
1226 replace_new_secondary mode)
1227 @type iallocator: str or None
1228 @param iallocator: instance allocator plugin to use (for use with
1229 replace_auto mode)
1230 @type reason: string
1231 @param reason: the reason for executing this operation
1232
1233 @rtype: string
1234 @return: job id
1235
1236 """
1237 query = [
1238 ("mode", mode),
1239 ]
1240
1241 # TODO: Convert to body parameters
1242
1243 if disks is not None:
1244 _AppendIf(query, True,
1245 ("disks", ",".join(str(idx) for idx in disks)))
1246
1247 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1248 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1249 _AppendReason(query, reason)
1250
1251 return self._SendRequest(HTTP_POST,
1252 ("/%s/instances/%s/replace-disks" %
1253 (GANETI_RAPI_VERSION, instance)), query, None)
1254
1255 def PrepareExport(self, instance, mode, reason=None):
1256 """Prepares an instance for an export.
1257
1258 @type instance: string
1259 @param instance: Instance name
1260 @type mode: string
1261 @param mode: Export mode
1262 @type reason: string
1263 @param reason: the reason for executing this operation
1264 @rtype: string
1265 @return: Job ID
1266
1267 """
1268 query = [("mode", mode)]
1269 _AppendReason(query, reason)
1270 return self._SendRequest(HTTP_PUT,
1271 ("/%s/instances/%s/prepare-export" %
1272 (GANETI_RAPI_VERSION, instance)), query, None)
1273
1274 def ExportInstance(self, instance, mode, destination, shutdown=None,
1275 remove_instance=None,
1276 x509_key_name=None, destination_x509_ca=None, reason=None):
1277 """Exports an instance.
1278
1279 @type instance: string
1280 @param instance: Instance name
1281 @type mode: string
1282 @param mode: Export mode
1283 @type reason: string
1284 @param reason: the reason for executing this operation
1285 @rtype: string
1286 @return: Job ID
1287
1288 """
1289 body = {
1290 "destination": destination,
1291 "mode": mode,
1292 }
1293
1294 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1295 _SetItemIf(body, remove_instance is not None,
1296 "remove_instance", remove_instance)
1297 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1298 _SetItemIf(body, destination_x509_ca is not None,
1299 "destination_x509_ca", destination_x509_ca)
1300
1301 query = []
1302 _AppendReason(query, reason)
1303
1304 return self._SendRequest(HTTP_PUT,
1305 ("/%s/instances/%s/export" %
1306 (GANETI_RAPI_VERSION, instance)), query, body)
1307
1308 def MigrateInstance(self, instance, mode=None, cleanup=None,
1309 target_node=None, reason=None):
1310 """Migrates an instance.
1311
1312 @type instance: string
1313 @param instance: Instance name
1314 @type mode: string
1315 @param mode: Migration mode
1316 @type cleanup: bool
1317 @param cleanup: Whether to clean up a previously failed migration
1318 @type target_node: string
1319 @param target_node: Target Node for externally mirrored instances
1320 @type reason: string
1321 @param reason: the reason for executing this operation
1322 @rtype: string
1323 @return: job id
1324
1325 """
1326 body = {}
1327 _SetItemIf(body, mode is not None, "mode", mode)
1328 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1329 _SetItemIf(body, target_node is not None, "target_node", target_node)
1330
1331 query = []
1332 _AppendReason(query, reason)
1333
1334 return self._SendRequest(HTTP_PUT,
1335 ("/%s/instances/%s/migrate" %
1336 (GANETI_RAPI_VERSION, instance)), query, body)
1337
1338 def FailoverInstance(self, instance, iallocator=None,
1339 ignore_consistency=None, target_node=None, reason=None):
1340 """Does a failover of an instance.
1341
1342 @type instance: string
1343 @param instance: Instance name
1344 @type iallocator: string
1345 @param iallocator: Iallocator for deciding the target node for
1346 shared-storage instances
1347 @type ignore_consistency: bool
1348 @param ignore_consistency: Whether to ignore disk consistency
1349 @type target_node: string
1350 @param target_node: Target node for shared-storage instances
1351 @type reason: string
1352 @param reason: the reason for executing this operation
1353 @rtype: string
1354 @return: job id
1355
1356 """
1357 body = {}
1358 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1359 _SetItemIf(body, ignore_consistency is not None,
1360 "ignore_consistency", ignore_consistency)
1361 _SetItemIf(body, target_node is not None, "target_node", target_node)
1362
1363 query = []
1364 _AppendReason(query, reason)
1365
1366 return self._SendRequest(HTTP_PUT,
1367 ("/%s/instances/%s/failover" %
1368 (GANETI_RAPI_VERSION, instance)), query, body)
1369
1370 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1371 reason=None):
1372 """Changes the name of an instance.
1373
1374 @type instance: string
1375 @param instance: Instance name
1376 @type new_name: string
1377 @param new_name: New instance name
1378 @type ip_check: bool
1379 @param ip_check: Whether to ensure instance's IP address is inactive
1380 @type name_check: bool
1381 @param name_check: Whether to ensure instance's name is resolvable
1382 @type reason: string
1383 @param reason: the reason for executing this operation
1384 @rtype: string
1385 @return: job id
1386
1387 """
1388 body = {
1389 "new_name": new_name,
1390 }
1391
1392 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1393 _SetItemIf(body, name_check is not None, "name_check", name_check)
1394
1395 query = []
1396 _AppendReason(query, reason)
1397
1398 return self._SendRequest(HTTP_PUT,
1399 ("/%s/instances/%s/rename" %
1400 (GANETI_RAPI_VERSION, instance)), query, body)
1401
1402 def GetInstanceConsole(self, instance, reason=None):
1403 """Request information for connecting to instance's console.
1404
1405 @type instance: string
1406 @param instance: Instance name
1407 @type reason: string
1408 @param reason: the reason for executing this operation
1409 @rtype: dict
1410 @return: dictionary containing information about instance's console
1411
1412 """
1413 query = []
1414 _AppendReason(query, reason)
1415 return self._SendRequest(HTTP_GET,
1416 ("/%s/instances/%s/console" %
1417 (GANETI_RAPI_VERSION, instance)), query, None)
1418
1419 def GetJobs(self, bulk=False):
1420 """Gets all jobs for the cluster.
1421
1422 @type bulk: bool
1423 @param bulk: Whether to return detailed information about jobs.
1424 @rtype: list of int
1425 @return: List of job ids for the cluster or list of dicts with detailed
1426 information about the jobs if bulk parameter was true.
1427
1428 """
1429 query = []
1430 _AppendIf(query, bulk, ("bulk", 1))
1431
1432 if bulk:
1433 return self._SendRequest(HTTP_GET,
1434 "/%s/jobs" % GANETI_RAPI_VERSION,
1435 query, None)
1436 else:
1437 return [int(j["id"])
1438 for j in self._SendRequest(HTTP_GET,
1439 "/%s/jobs" % GANETI_RAPI_VERSION,
1440 None, None)]
1441
1442 def GetJobStatus(self, job_id):
1443 """Gets the status of a job.
1444
1445 @type job_id: string
1446 @param job_id: job id whose status to query
1447
1448 @rtype: dict
1449 @return: job status
1450
1451 """
1452 return self._SendRequest(HTTP_GET,
1453 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1454 None, None)
1455
1456 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1457 """Polls cluster for job status until completion.
1458
1459 Completion is defined as any of the following states listed in
1460 L{JOB_STATUS_FINALIZED}.
1461
1462 @type job_id: string
1463 @param job_id: job id to watch
1464 @type period: int
1465 @param period: how often to poll for status (optional, default 5s)
1466 @type retries: int
1467 @param retries: how many time to poll before giving up
1468 (optional, default -1 means unlimited)
1469
1470 @rtype: bool
1471 @return: C{True} if job succeeded or C{False} if failed/status timeout
1472 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1473 possible; L{WaitForJobChange} returns immediately after a job changed and
1474 does not use polling
1475
1476 """
1477 while retries != 0:
1478 job_result = self.GetJobStatus(job_id)
1479
1480 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1481 return True
1482 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1483 return False
1484
1485 if period:
1486 time.sleep(period)
1487
1488 if retries > 0:
1489 retries -= 1
1490
1491 return False
1492
1493 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1494 """Waits for job changes.
1495
1496 @type job_id: string
1497 @param job_id: Job ID for which to wait
1498 @return: C{None} if no changes have been detected and a dict with two keys,
1499 C{job_info} and C{log_entries} otherwise.
1500 @rtype: dict
1501
1502 """
1503 body = {
1504 "fields": fields,
1505 "previous_job_info": prev_job_info,
1506 "previous_log_serial": prev_log_serial,
1507 }
1508
1509 return self._SendRequest(HTTP_GET,
1510 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1511 None, body)
1512
1513 def CancelJob(self, job_id, dry_run=False):
1514 """Cancels a job.
1515
1516 @type job_id: string
1517 @param job_id: id of the job to delete
1518 @type dry_run: bool
1519 @param dry_run: whether to perform a dry run
1520 @rtype: tuple
1521 @return: tuple containing the result, and a message (bool, string)
1522
1523 """
1524 query = []
1525 _AppendDryRunIf(query, dry_run)
1526
1527 return self._SendRequest(HTTP_DELETE,
1528 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1529 query, None)
1530
1531 def GetNodes(self, bulk=False, reason=None):
1532 """Gets all nodes in the cluster.
1533
1534 @type bulk: bool
1535 @param bulk: whether to return all information about all instances
1536 @type reason: string
1537 @param reason: the reason for executing this operation
1538
1539 @rtype: list of dict or str
1540 @return: if bulk is true, info about nodes in the cluster,
1541 else list of nodes in the cluster
1542
1543 """
1544 query = []
1545 _AppendIf(query, bulk, ("bulk", 1))
1546 _AppendReason(query, reason)
1547
1548 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1549 query, None)
1550 if bulk:
1551 return nodes
1552 else:
1553 return [n["id"] for n in nodes]
1554
1555 def GetNode(self, node, reason=None):
1556 """Gets information about a node.
1557
1558 @type node: str
1559 @param node: node whose info to return
1560 @type reason: string
1561 @param reason: the reason for executing this operation
1562
1563 @rtype: dict
1564 @return: info about the node
1565
1566 """
1567 query = []
1568 _AppendReason(query, reason)
1569
1570 return self._SendRequest(HTTP_GET,
1571 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1572 query, None)
1573
1574 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1575 dry_run=False, early_release=None,
1576 mode=None, accept_old=False, reason=None):
1577 """Evacuates instances from a Ganeti node.
1578
1579 @type node: str
1580 @param node: node to evacuate
1581 @type iallocator: str or None
1582 @param iallocator: instance allocator to use
1583 @type remote_node: str
1584 @param remote_node: node to evaucate to
1585 @type dry_run: bool
1586 @param dry_run: whether to perform a dry run
1587 @type early_release: bool
1588 @param early_release: whether to enable parallelization
1589 @type mode: string
1590 @param mode: Node evacuation mode
1591 @type accept_old: bool
1592 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1593 results
1594 @type reason: string
1595 @param reason: the reason for executing this operation
1596
1597 @rtype: string, or a list for pre-2.5 results
1598 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1599 list of (job ID, instance name, new secondary node); if dry_run was
1600 specified, then the actual move jobs were not submitted and the job IDs
1601 will be C{None}
1602
1603 @raises GanetiApiError: if an iallocator and remote_node are both
1604 specified
1605
1606 """
1607 if iallocator and remote_node:
1608 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1609
1610 query = []
1611 _AppendDryRunIf(query, dry_run)
1612 _AppendReason(query, reason)
1613
1614 if _NODE_EVAC_RES1 in self.GetFeatures():
1615 # Server supports body parameters
1616 body = {}
1617
1618 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1619 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1620 _SetItemIf(body, early_release is not None,
1621 "early_release", early_release)
1622 _SetItemIf(body, mode is not None, "mode", mode)
1623 else:
1624 # Pre-2.5 request format
1625 body = None
1626
1627 if not accept_old:
1628 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1629 " not accept old-style results (parameter"
1630 " accept_old)")
1631
1632 # Pre-2.5 servers can only evacuate secondaries
1633 if mode is not None and mode != NODE_EVAC_SEC:
1634 raise GanetiApiError("Server can only evacuate secondary instances")
1635
1636 _AppendIf(query, iallocator, ("iallocator", iallocator))
1637 _AppendIf(query, remote_node, ("remote_node", remote_node))
1638 _AppendIf(query, early_release, ("early_release", 1))
1639
1640 return self._SendRequest(HTTP_POST,
1641 ("/%s/nodes/%s/evacuate" %
1642 (GANETI_RAPI_VERSION, node)), query, body)
1643
1644 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1645 target_node=None, reason=None):
1646 """Migrates all primary instances from a node.
1647
1648 @type node: str
1649 @param node: node to migrate
1650 @type mode: string
1651 @param mode: if passed, it will overwrite the live migration type,
1652 otherwise the hypervisor default will be used
1653 @type dry_run: bool
1654 @param dry_run: whether to perform a dry run
1655 @type iallocator: string
1656 @param iallocator: instance allocator to use
1657 @type target_node: string
1658 @param target_node: Target node for shared-storage instances
1659 @type reason: string
1660 @param reason: the reason for executing this operation
1661
1662 @rtype: string
1663 @return: job id
1664
1665 """
1666 query = []
1667 _AppendDryRunIf(query, dry_run)
1668 _AppendReason(query, reason)
1669
1670 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1671 body = {}
1672
1673 _SetItemIf(body, mode is not None, "mode", mode)
1674 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1675 _SetItemIf(body, target_node is not None, "target_node", target_node)
1676
1677 assert len(query) <= 1
1678
1679 return self._SendRequest(HTTP_POST,
1680 ("/%s/nodes/%s/migrate" %
1681 (GANETI_RAPI_VERSION, node)), query, body)
1682 else:
1683 # Use old request format
1684 if target_node is not None:
1685 raise GanetiApiError("Server does not support specifying target node"
1686 " for node migration")
1687
1688 _AppendIf(query, mode is not None, ("mode", mode))
1689
1690 return self._SendRequest(HTTP_POST,
1691 ("/%s/nodes/%s/migrate" %
1692 (GANETI_RAPI_VERSION, node)), query, None)
1693
1694 def GetNodeRole(self, node, reason=None):
1695 """Gets the current role for a node.
1696
1697 @type node: str
1698 @param node: node whose role to return
1699 @type reason: string
1700 @param reason: the reason for executing this operation
1701
1702 @rtype: str
1703 @return: the current role for a node
1704
1705 """
1706 query = []
1707 _AppendReason(query, reason)
1708
1709 return self._SendRequest(HTTP_GET,
1710 ("/%s/nodes/%s/role" %
1711 (GANETI_RAPI_VERSION, node)), query, None)
1712
1713 def SetNodeRole(self, node, role, force=False, auto_promote=None,
1714 reason=None):
1715 """Sets the role for a node.
1716
1717 @type node: str
1718 @param node: the node whose role to set
1719 @type role: str
1720 @param role: the role to set for the node
1721 @type force: bool
1722 @param force: whether to force the role change
1723 @type auto_promote: bool
1724 @param auto_promote: Whether node(s) should be promoted to master candidate
1725 if necessary
1726 @type reason: string
1727 @param reason: the reason for executing this operation
1728
1729 @rtype: string
1730 @return: job id
1731
1732 """
1733 query = []
1734 _AppendForceIf(query, force)
1735 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1736 _AppendReason(query, reason)
1737
1738 return self._SendRequest(HTTP_PUT,
1739 ("/%s/nodes/%s/role" %
1740 (GANETI_RAPI_VERSION, node)), query, role)
1741
1742 def PowercycleNode(self, node, force=False, reason=None):
1743 """Powercycles a node.
1744
1745 @type node: string
1746 @param node: Node name
1747 @type force: bool
1748 @param force: Whether to force the operation
1749 @type reason: string
1750 @param reason: the reason for executing this operation
1751 @rtype: string
1752 @return: job id
1753
1754 """
1755 query = []
1756 _AppendForceIf(query, force)
1757 _AppendReason(query, reason)
1758
1759 return self._SendRequest(HTTP_POST,
1760 ("/%s/nodes/%s/powercycle" %
1761 (GANETI_RAPI_VERSION, node)), query, None)
1762
1763 def ModifyNode(self, node, reason=None, **kwargs):
1764 """Modifies a node.
1765
1766 More details for parameters can be found in the RAPI documentation.
1767
1768 @type node: string
1769 @param node: Node name
1770 @type reason: string
1771 @param reason: the reason for executing this operation
1772 @rtype: string
1773 @return: job id
1774
1775 """
1776 query = []
1777 _AppendReason(query, reason)
1778
1779 return self._SendRequest(HTTP_POST,
1780 ("/%s/nodes/%s/modify" %
1781 (GANETI_RAPI_VERSION, node)), query, kwargs)
1782
1783 def GetNodeStorageUnits(self, node, storage_type, output_fields, reason=None):
1784 """Gets the storage units for a node.
1785
1786 @type node: str
1787 @param node: the node whose storage units to return
1788 @type storage_type: str
1789 @param storage_type: storage type whose units to return
1790 @type output_fields: str
1791 @param output_fields: storage type fields to return
1792 @type reason: string
1793 @param reason: the reason for executing this operation
1794
1795 @rtype: string
1796 @return: job id where results can be retrieved
1797
1798 """
1799 query = [
1800 ("storage_type", storage_type),
1801 ("output_fields", output_fields),
1802 ]
1803 _AppendReason(query, reason)
1804
1805 return self._SendRequest(HTTP_GET,
1806 ("/%s/nodes/%s/storage" %
1807 (GANETI_RAPI_VERSION, node)), query, None)
1808
1809 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None,
1810 reason=None):
1811 """Modifies parameters of storage units on the node.
1812
1813 @type node: str
1814 @param node: node whose storage units to modify
1815 @type storage_type: str
1816 @param storage_type: storage type whose units to modify
1817 @type name: str
1818 @param name: name of the storage unit
1819 @type allocatable: bool or None
1820 @param allocatable: Whether to set the "allocatable" flag on the storage
1821 unit (None=no modification, True=set, False=unset)
1822 @type reason: string
1823 @param reason: the reason for executing this operation
1824
1825 @rtype: string
1826 @return: job id
1827
1828 """
1829 query = [
1830 ("storage_type", storage_type),
1831 ("name", name),
1832 ]
1833
1834 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1835 _AppendReason(query, reason)
1836
1837 return self._SendRequest(HTTP_PUT,
1838 ("/%s/nodes/%s/storage/modify" %
1839 (GANETI_RAPI_VERSION, node)), query, None)
1840
1841 def RepairNodeStorageUnits(self, node, storage_type, name, reason=None):
1842 """Repairs a storage unit on the node.
1843
1844 @type node: str
1845 @param node: node whose storage units to repair
1846 @type storage_type: str
1847 @param storage_type: storage type to repair
1848 @type name: str
1849 @param name: name of the storage unit to repair
1850 @type reason: string
1851 @param reason: the reason for executing this operation
1852
1853 @rtype: string
1854 @return: job id
1855
1856 """
1857 query = [
1858 ("storage_type", storage_type),
1859 ("name", name),
1860 ]
1861 _AppendReason(query, reason)
1862
1863 return self._SendRequest(HTTP_PUT,
1864 ("/%s/nodes/%s/storage/repair" %
1865 (GANETI_RAPI_VERSION, node)), query, None)
1866
1867 def GetNodeTags(self, node, reason=None):
1868 """Gets the tags for a node.
1869
1870 @type node: str
1871 @param node: node whose tags to return
1872 @type reason: string
1873 @param reason: the reason for executing this operation
1874
1875 @rtype: list of str
1876 @return: tags for the node
1877
1878 """
1879 query = []
1880 _AppendReason(query, reason)
1881
1882 return self._SendRequest(HTTP_GET,
1883 ("/%s/nodes/%s/tags" %
1884 (GANETI_RAPI_VERSION, node)), query, None)
1885
1886 def AddNodeTags(self, node, tags, dry_run=False, reason=None):
1887 """Adds tags to a node.
1888
1889 @type node: str
1890 @param node: node to add tags to
1891 @type tags: list of str
1892 @param tags: tags to add to the node
1893 @type dry_run: bool
1894 @param dry_run: whether to perform a dry run
1895 @type reason: string
1896 @param reason: the reason for executing this operation
1897
1898 @rtype: string
1899 @return: job id
1900
1901 """
1902 query = [("tag", t) for t in tags]
1903 _AppendDryRunIf(query, dry_run)
1904 _AppendReason(query, reason)
1905
1906 return self._SendRequest(HTTP_PUT,
1907 ("/%s/nodes/%s/tags" %
1908 (GANETI_RAPI_VERSION, node)), query, tags)
1909
1910 def DeleteNodeTags(self, node, tags, dry_run=False, reason=None):
1911 """Delete tags from a node.
1912
1913 @type node: str
1914 @param node: node to remove tags from
1915 @type tags: list of str
1916 @param tags: tags to remove from the node
1917 @type dry_run: bool
1918 @param dry_run: whether to perform a dry run
1919 @type reason: string
1920 @param reason: the reason for executing this operation
1921
1922 @rtype: string
1923 @return: job id
1924
1925 """
1926 query = [("tag", t) for t in tags]
1927 _AppendDryRunIf(query, dry_run)
1928 _AppendReason(query, reason)
1929
1930 return self._SendRequest(HTTP_DELETE,
1931 ("/%s/nodes/%s/tags" %
1932 (GANETI_RAPI_VERSION, node)), query, None)
1933
1934 def GetNetworks(self, bulk=False, reason=None):
1935 """Gets all networks in the cluster.
1936
1937 @type bulk: bool
1938 @param bulk: whether to return all information about the networks
1939
1940 @rtype: list of dict or str
1941 @return: if bulk is true, a list of dictionaries with info about all
1942 networks in the cluster, else a list of names of those networks
1943
1944 """
1945 query = []
1946 _AppendIf(query, bulk, ("bulk", 1))
1947 _AppendReason(query, reason)
1948
1949 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1950 query, None)
1951 if bulk:
1952 return networks
1953 else:
1954 return [n["name"] for n in networks]
1955
1956 def GetNetwork(self, network, reason=None):
1957 """Gets information about a network.
1958
1959 @type network: str
1960 @param network: name of the network whose info to return
1961 @type reason: string
1962 @param reason: the reason for executing this operation
1963
1964 @rtype: dict
1965 @return: info about the network
1966
1967 """
1968 query = []
1969 _AppendReason(query, reason)
1970
1971 return self._SendRequest(HTTP_GET,
1972 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1973 query, None)
1974
1975 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1976 gateway6=None, mac_prefix=None,
1977 add_reserved_ips=None, tags=None, dry_run=False,
1978 reason=None):
1979 """Creates a new network.
1980
1981 @type network_name: str
1982 @param network_name: the name of network to create
1983 @type dry_run: bool
1984 @param dry_run: whether to peform a dry run
1985 @type reason: string
1986 @param reason: the reason for executing this operation
1987
1988 @rtype: string
1989 @return: job id
1990
1991 """
1992 query = []
1993 _AppendDryRunIf(query, dry_run)
1994 _AppendReason(query, reason)
1995
1996 if add_reserved_ips:
1997 add_reserved_ips = add_reserved_ips.split(",")
1998
1999 if tags:
2000 tags = tags.split(",")
2001
2002 body = {
2003 "network_name": network_name,
2004 "gateway": gateway,
2005 "network": network,
2006 "gateway6": gateway6,
2007 "network6": network6,
2008 "mac_prefix": mac_prefix,
2009 "add_reserved_ips": add_reserved_ips,
2010 "tags": tags,
2011 }
2012
2013 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2014 query, body)
2015
2016 def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False,
2017 reason=None):
2018 """Connects a Network to a NodeGroup with the given netparams
2019
2020 """
2021 body = {
2022 "group_name": group_name,
2023 "network_mode": mode,
2024 "network_link": link,
2025 }
2026
2027 query = []
2028 _AppendDryRunIf(query, dry_run)
2029 _AppendReason(query, reason)
2030
2031 return self._SendRequest(HTTP_PUT,
2032 ("/%s/networks/%s/connect" %
2033 (GANETI_RAPI_VERSION, network_name)), query, body)
2034
2035 def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2036 reason=None):
2037 """Connects a Network to a NodeGroup with the given netparams
2038
2039 """
2040 body = {
2041 "group_name": group_name,
2042 }
2043
2044 query = []
2045 _AppendDryRunIf(query, dry_run)
2046 _AppendReason(query, reason)
2047
2048 return self._SendRequest(HTTP_PUT,
2049 ("/%s/networks/%s/disconnect" %
2050 (GANETI_RAPI_VERSION, network_name)), query, body)
2051
2052 def ModifyNetwork(self, network, reason=None, **kwargs):
2053 """Modifies a network.
2054
2055 More details for parameters can be found in the RAPI documentation.
2056
2057 @type network: string
2058 @param network: Network name
2059 @type reason: string
2060 @param reason: the reason for executing this operation
2061 @rtype: string
2062 @return: job id
2063
2064 """
2065 query = []
2066 _AppendReason(query, reason)
2067
2068 return self._SendRequest(HTTP_PUT,
2069 ("/%s/networks/%s/modify" %
2070 (GANETI_RAPI_VERSION, network)), None, kwargs)
2071
2072 def DeleteNetwork(self, network, dry_run=False, reason=None):
2073 """Deletes a network.
2074
2075 @type network: str
2076 @param network: the network to delete
2077 @type dry_run: bool
2078 @param dry_run: whether to peform a dry run
2079 @type reason: string
2080 @param reason: the reason for executing this operation
2081
2082 @rtype: string
2083 @return: job id
2084
2085 """
2086 query = []
2087 _AppendDryRunIf(query, dry_run)
2088 _AppendReason(query, reason)
2089
2090 return self._SendRequest(HTTP_DELETE,
2091 ("/%s/networks/%s" %
2092 (GANETI_RAPI_VERSION, network)), query, None)
2093
2094 def GetNetworkTags(self, network, reason=None):
2095 """Gets tags for a network.
2096
2097 @type network: string
2098 @param network: Node group whose tags to return
2099 @type reason: string
2100 @param reason: the reason for executing this operation
2101
2102 @rtype: list of strings
2103 @return: tags for the network
2104
2105 """
2106 query = []
2107 _AppendReason(query, reason)
2108
2109 return self._SendRequest(HTTP_GET,
2110 ("/%s/networks/%s/tags" %
2111 (GANETI_RAPI_VERSION, network)), query, None)
2112
2113 def AddNetworkTags(self, network, tags, dry_run=False, reason=None):
2114 """Adds tags to a network.
2115
2116 @type network: str
2117 @param network: network to add tags to
2118 @type tags: list of string
2119 @param tags: tags to add to the network
2120 @type dry_run: bool
2121 @param dry_run: whether to perform a dry run
2122 @type reason: string
2123 @param reason: the reason for executing this operation
2124
2125 @rtype: string
2126 @return: job id
2127
2128 """
2129 query = [("tag", t) for t in tags]
2130 _AppendDryRunIf(query, dry_run)
2131 _AppendReason(query, reason)
2132
2133 return self._SendRequest(HTTP_PUT,
2134 ("/%s/networks/%s/tags" %
2135 (GANETI_RAPI_VERSION, network)), query, None)
2136
2137 def DeleteNetworkTags(self, network, tags, dry_run=False, reason=None):
2138 """Deletes tags from a network.
2139
2140 @type network: str
2141 @param network: network to delete tags from
2142 @type tags: list of string
2143 @param tags: tags to delete
2144 @type dry_run: bool
2145 @param dry_run: whether to perform a dry run
2146 @type reason: string
2147 @param reason: the reason for executing this operation
2148 @rtype: string
2149 @return: job id
2150
2151 """
2152 query = [("tag", t) for t in tags]
2153 _AppendDryRunIf(query, dry_run)
2154 _AppendReason(query, reason)
2155
2156 return self._SendRequest(HTTP_DELETE,
2157 ("/%s/networks/%s/tags" %
2158 (GANETI_RAPI_VERSION, network)), query, None)
2159
2160 def GetGroups(self, bulk=False, reason=None):
2161 """Gets all node groups in the cluster.
2162
2163 @type bulk: bool
2164 @param bulk: whether to return all information about the groups
2165 @type reason: string
2166 @param reason: the reason for executing this operation
2167
2168 @rtype: list of dict or str
2169 @return: if bulk is true, a list of dictionaries with info about all node
2170 groups in the cluster, else a list of names of those node groups
2171
2172 """
2173 query = []
2174 _AppendIf(query, bulk, ("bulk", 1))
2175 _AppendReason(query, reason)
2176
2177 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2178 query, None)
2179 if bulk:
2180 return groups
2181 else:
2182 return [g["name"] for g in groups]
2183
2184 def GetGroup(self, group, reason=None):
2185 """Gets information about a node group.
2186
2187 @type group: str
2188 @param group: name of the node group whose info to return
2189 @type reason: string
2190 @param reason: the reason for executing this operation
2191
2192 @rtype: dict
2193 @return: info about the node group
2194
2195 """
2196 query = []
2197 _AppendReason(query, reason)
2198
2199 return self._SendRequest(HTTP_GET,
2200 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2201 query, None)
2202
2203 def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2204 """Creates a new node group.
2205
2206 @type name: str
2207 @param name: the name of node group to create
2208 @type alloc_policy: str
2209 @param alloc_policy: the desired allocation policy for the group, if any
2210 @type dry_run: bool
2211 @param dry_run: whether to peform a dry run
2212 @type reason: string
2213 @param reason: the reason for executing this operation
2214
2215 @rtype: string
2216 @return: job id
2217
2218 """
2219 query = []
2220 _AppendDryRunIf(query, dry_run)
2221 _AppendReason(query, reason)
2222
2223 body = {
2224 "name": name,
2225 "alloc_policy": alloc_policy,
2226 }
2227
2228 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2229 query, body)
2230
2231 def ModifyGroup(self, group, reason=None, **kwargs):
2232 """Modifies a node group.
2233
2234 More details for parameters can be found in the RAPI documentation.
2235
2236 @type group: string
2237 @param group: Node group name
2238 @type reason: string
2239 @param reason: the reason for executing this operation
2240 @rtype: string
2241 @return: job id
2242
2243 """
2244 query = []
2245 _AppendReason(query, reason)
2246
2247 return self._SendRequest(HTTP_PUT,
2248 ("/%s/groups/%s/modify" %
2249 (GANETI_RAPI_VERSION, group)), query, kwargs)
2250
2251 def DeleteGroup(self, group, dry_run=False, reason=None):
2252 """Deletes a node group.
2253
2254 @type group: str
2255 @param group: the node group to delete
2256 @type dry_run: bool
2257 @param dry_run: whether to peform a dry run
2258 @type reason: string
2259 @param reason: the reason for executing this operation
2260
2261 @rtype: string
2262 @return: job id
2263
2264 """
2265 query = []
2266 _AppendDryRunIf(query, dry_run)
2267 _AppendReason(query, reason)
2268
2269 return self._SendRequest(HTTP_DELETE,
2270 ("/%s/groups/%s" %
2271 (GANETI_RAPI_VERSION, group)), query, None)
2272
2273 def RenameGroup(self, group, new_name, reason=None):
2274 """Changes the name of a node group.
2275
2276 @type group: string
2277 @param group: Node group name
2278 @type new_name: string
2279 @param new_name: New node group name
2280 @type reason: string
2281 @param reason: the reason for executing this operation
2282
2283 @rtype: string
2284 @return: job id
2285
2286 """
2287 body = {
2288 "new_name": new_name,
2289 }
2290
2291 query = []
2292 _AppendReason(query, reason)
2293
2294 return self._SendRequest(HTTP_PUT,
2295 ("/%s/groups/%s/rename" %
2296 (GANETI_RAPI_VERSION, group)), query, body)
2297
2298 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2299 reason=None):
2300 """Assigns nodes to a group.
2301
2302 @type group: string
2303 @param group: Node group name
2304 @type nodes: list of strings
2305 @param nodes: List of nodes to assign to the group
2306 @type reason: string
2307 @param reason: the reason for executing this operation
2308
2309 @rtype: string
2310 @return: job id
2311
2312 """
2313 query = []
2314 _AppendForceIf(query, force)
2315 _AppendDryRunIf(query, dry_run)
2316 _AppendReason(query, reason)
2317
2318 body = {
2319 "nodes": nodes,
2320 }
2321
2322 return self._SendRequest(HTTP_PUT,
2323 ("/%s/groups/%s/assign-nodes" %
2324 (GANETI_RAPI_VERSION, group)), query, body)
2325
2326 def GetGroupTags(self, group, reason=None):
2327 """Gets tags for a node group.
2328
2329 @type group: string
2330 @param group: Node group whose tags to return
2331 @type reason: string
2332 @param reason: the reason for executing this operation
2333
2334 @rtype: list of strings
2335 @return: tags for the group
2336
2337 """
2338 query = []
2339 _AppendReason(query, reason)
2340
2341 return self._SendRequest(HTTP_GET,
2342 ("/%s/groups/%s/tags" %
2343 (GANETI_RAPI_VERSION, group)), query, None)
2344
2345 def AddGroupTags(self, group, tags, dry_run=False, reason=None):
2346 """Adds tags to a node group.
2347
2348 @type group: str
2349 @param group: group to add tags to
2350 @type tags: list of string
2351 @param tags: tags to add to the group
2352 @type dry_run: bool
2353 @param dry_run: whether to perform a dry run
2354 @type reason: string
2355 @param reason: the reason for executing this operation
2356
2357 @rtype: string
2358 @return: job id
2359
2360 """
2361 query = [("tag", t) for t in tags]
2362 _AppendDryRunIf(query, dry_run)
2363 _AppendReason(query, reason)
2364
2365 return self._SendRequest(HTTP_PUT,
2366 ("/%s/groups/%s/tags" %
2367 (GANETI_RAPI_VERSION, group)), query, None)
2368
2369 def DeleteGroupTags(self, group, tags, dry_run=False, reason=None):
2370 """Deletes tags from a node group.
2371
2372 @type group: str
2373 @param group: group to delete tags from
2374 @type tags: list of string
2375 @param tags: tags to delete
2376 @type dry_run: bool
2377 @param dry_run: whether to perform a dry run
2378 @type reason: string
2379 @param reason: the reason for executing this operation
2380 @rtype: string
2381 @return: job id
2382
2383 """
2384 query = [("tag", t) for t in tags]
2385 _AppendDryRunIf(query, dry_run)
2386 _AppendReason(query, reason)
2387
2388 return self._SendRequest(HTTP_DELETE,
2389 ("/%s/groups/%s/tags" %
2390 (GANETI_RAPI_VERSION, group)), query, None)
2391
2392 def Query(self, what, fields, qfilter=None, reason=None):
2393 """Retrieves information about resources.
2394
2395 @type what: string
2396 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2397 @type fields: list of string
2398 @param fields: Requested fields
2399 @type qfilter: None or list
2400 @param qfilter: Query filter
2401 @type reason: string
2402 @param reason: the reason for executing this operation
2403
2404 @rtype: string
2405 @return: job id
2406
2407 """
2408 query = []
2409 _AppendReason(query, reason)
2410
2411 body = {
2412 "fields": fields,
2413 }
2414
2415 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2416 # TODO: remove "filter" after 2.7
2417 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2418
2419 return self._SendRequest(HTTP_PUT,
2420 ("/%s/query/%s" %
2421 (GANETI_RAPI_VERSION, what)), query, body)
2422
2423 def QueryFields(self, what, fields=None, reason=None):
2424 """Retrieves available fields for a resource.
2425
2426 @type what: string
2427 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2428 @type fields: list of string
2429 @param fields: Requested fields
2430 @type reason: string
2431 @param reason: the reason for executing this operation
2432
2433 @rtype: string
2434 @return: job id
2435
2436 """
2437 query = []
2438 _AppendReason(query, reason)
2439
2440 if fields is not None:
2441 _AppendIf(query, True, ("fields", ",".join(fields)))
2442
2443 return self._SendRequest(HTTP_GET,
2444 ("/%s/query/%s/fields" %
2445 (GANETI_RAPI_VERSION, what)), query, None)