4 # Copyright (C) 2010, 2011, 2012 Google Inc.
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.
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.
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
22 """Ganeti RAPI client.
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}
33 # No Ganeti-specific modules should be imported. The RAPI client is supposed to
45 from cStringIO import StringIO
47 from StringIO import StringIO
50 GANETI_RAPI_PORT = 5080
51 GANETI_RAPI_VERSION = 2
53 HTTP_DELETE = "DELETE"
59 HTTP_APP_JSON = "application/json"
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"
66 NODE_EVAC_PRI = "primary-only"
67 NODE_EVAC_SEC = "secondary-only"
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"
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([
88 JOB_STATUS_FINALIZED = frozenset([
93 JOB_STATUS_ALL = frozenset([
95 ]) | JOB_STATUS_PENDING | JOB_STATUS_FINALIZED
98 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
101 _REQ_DATA_VERSION_FIELD = "__version__"
102 _QPARAM_DRY_RUN = "dry-run"
103 _QPARAM_FORCE = "force"
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"
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
118 ECODE_RESOLVER = "resolver_error"
120 #: Not enough resources (iallocator failure, disk space, memory, etc.)
121 ECODE_NORES = "insufficient_resources"
123 #: Temporarily out of resources; operation can be tried again
124 ECODE_TEMP_NORES = "temp_insufficient_resources"
126 #: Wrong arguments (at syntax level)
127 ECODE_INVAL = "wrong_input"
129 #: Wrong entity state
130 ECODE_STATE = "wrong_state"
133 ECODE_NOENT = "unknown_entity"
135 #: Entity already exists
136 ECODE_EXISTS = "already_exists"
138 #: Resource not unique (e.g. MAC or IP duplication)
139 ECODE_NOTUNIQUE = "resource_not_unique"
141 #: Internal cluster error
142 ECODE_FAULT = "internal_error"
144 #: Environment error (e.g. node disk error)
145 ECODE_ENVIRON = "environment_error"
147 #: List of all failure types
148 ECODE_ALL = frozenset([
161 # Older pycURL versions don't have all error constants
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
169 _CURL_SSL_CERT_ERRORS = frozenset([
171 _CURLE_SSL_CACERT_BADFILE,
175 class Error(Exception):
176 """Base error class for this module.
182 class GanetiApiError(Error):
183 """Generic error raised from Ganeti API.
186 def __init__(self, msg, code=None):
187 Error.__init__(self, msg)
191 class CertificateError(GanetiApiError):
192 """Raised when a problem is found with the SSL certificate.
198 def _AppendIf(container, condition, value):
199 """Appends to a list if a condition evaluates to truth.
203 container.append(value)
208 def _AppendDryRunIf(container, condition):
209 """Appends a "dry-run" parameter if a condition evaluates to truth.
212 return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
215 def _AppendForceIf(container, condition):
216 """Appends a "force" parameter if a condition evaluates to truth.
219 return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
222 def _AppendReason(container, reason):
223 """Appends an element to the reason trail.
225 If the user provided a reason, it is added to the reason trail.
228 return _AppendIf(container, reason, ("reason", reason))
231 def _SetItemIf(container, condition, item, value):
232 """Sets an item if a condition evaluates to truth.
236 container[item] = value
241 def UsesRapiClient(fn):
242 """Decorator for code using RAPI client to initialize pycURL.
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
249 assert threading.activeCount() == 1, \
250 "Found active threads when initializing pycURL"
252 pycurl.global_init(pycurl.GLOBAL_ALL)
254 return fn(*args, **kwargs)
256 pycurl.global_cleanup()
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.
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
275 @param cafile: In which file we can find the certificates
277 @param capath: In which directory we can find the certificates
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
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)).
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")
294 def _ConfigCurl(curl, logger):
295 """Configures a cURL object
297 @type curl: pycurl.Curl
298 @param curl: cURL object
301 logger.debug("Using cURL version %s", pycurl.version)
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',
307 sslver = _pycurl_version_fn()[5]
309 raise Error("No SSL support in cURL")
311 lcsslver = sslver.lower()
312 if lcsslver.startswith("openssl/"):
314 elif lcsslver.startswith("nss/"):
315 # TODO: investigate compatibility beyond a simple test
317 elif lcsslver.startswith("gnutls/"):
319 raise Error("cURL linked against GnuTLS has no support for a"
320 " CA path (%s)" % (pycurl.version, ))
322 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
325 curl.setopt(pycurl.VERBOSE, verbose)
326 curl.setopt(pycurl.NOSIGNAL, not use_signal)
328 # Whether to verify remote peer's CN
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)
337 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
339 if cafile or capath or use_curl_cabundle:
340 # Require certificates to be checked
341 curl.setopt(pycurl.SSL_VERIFYPEER, True)
343 curl.setopt(pycurl.CAINFO, str(cafile))
345 curl.setopt(pycurl.CAPATH, str(capath))
346 # Not changing anything for using default CA bundle
348 # Disable SSL certificate verification
349 curl.setopt(pycurl.SSL_VERIFYPEER, False)
351 if proxy is not None:
352 curl.setopt(pycurl.PROXY, str(proxy))
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)
363 class GanetiRapiClient(object): # pylint: disable=R0904
364 """Ganeti RAPI client.
367 USER_AGENT = "Ganeti RAPI Client"
368 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
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.
376 @param host: the ganeti cluster master to interact with
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
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
395 socket.inet_pton(socket.AF_INET6, host)
396 address = "[%s]:%s" % (host, port)
398 address = "%s:%s" % (host, port)
400 self._base_url = "https://%s" % address
402 if username is not None:
404 raise Error("Password not specified")
406 raise Error("Specified password without username")
408 def _CreateCurl(self):
409 """Creates a cURL object.
412 # Create pycURL object if no factory is provided
413 if self._curl_factory:
414 curl = self._curl_factory()
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,
431 assert ((self._username is None and self._password is None) ^
432 (self._username is not None and self._password is not None))
435 # Setup authentication
436 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
437 curl.setopt(pycurl.USERPWD,
438 str("%s:%s" % (self._username, self._password)))
440 # Call external configuration function
441 if self._curl_config_fn:
442 self._curl_config_fn(curl, self._logger)
447 def _EncodeQuery(query):
448 """Encode query values for RAPI URL.
450 @type query: list of two-tuples
451 @param query: Query arguments
453 @return: Query list with encoded values
458 for name, value in query:
460 result.append((name, ""))
462 elif isinstance(value, bool):
463 # Boolean values must be encoded as 0 or 1
464 result.append((name, int(value)))
466 elif isinstance(value, (list, tuple, dict)):
467 raise ValueError("Invalid query data type %r" % type(value).__name__)
470 result.append((name, value))
474 def _SendRequest(self, method, path, query, content):
475 """Sends an HTTP request.
477 This constructs a full URL, encodes and decodes HTTP bodies, and
478 handles invalid responses in a pythonic way.
481 @param method: HTTP method to use
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
490 @return: JSON-Decoded response
492 @raises CertificateError: If an invalid SSL certificate is found
493 @raises GanetiApiError: If an invalid response is returned
496 assert path.startswith("/")
498 curl = self._CreateCurl()
500 if content is not None:
501 encoded_content = self._json_encoder.encode(content)
506 urlparts = [self._base_url, path]
509 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
511 url = "".join(urlparts)
513 self._logger.debug("Sending request %s %s (content=%r)",
514 method, url, encoded_content)
516 # Buffer for response
517 encoded_resp_body = StringIO()
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)
526 # Send request and wait for response
529 except pycurl.error, err:
530 if err.args[0] in _CURL_SSL_CERT_ERRORS:
531 raise CertificateError("SSL certificate error %s" % err,
534 raise GanetiApiError(str(err), code=err.args[0])
536 # Reset settings to not keep references to large objects in memory
538 curl.setopt(pycurl.POSTFIELDS, "")
539 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
541 # Get HTTP response code
542 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
544 # Was anything written to the response buffer?
545 if encoded_resp_body.tell():
546 response_content = simplejson.loads(encoded_resp_body.getvalue())
548 response_content = None
550 if http_code != HTTP_OK:
551 if isinstance(response_content, dict):
553 (response_content["code"],
554 response_content["message"],
555 response_content["explain"]))
557 msg = str(response_content)
559 raise GanetiApiError(msg, code=http_code)
561 return response_content
563 def GetVersion(self):
564 """Gets the Remote API version running on the cluster.
567 @return: Ganeti Remote API version
570 return self._SendRequest(HTTP_GET, "/version", None, None)
572 def GetFeatures(self):
573 """Gets the list of optional features supported by RAPI server.
576 @return: List of optional features
580 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
582 except GanetiApiError, err:
583 # Older RAPI servers don't support this resource
584 if err.code == HTTP_NOT_FOUND:
589 def GetOperatingSystems(self, reason=None):
590 """Gets the Operating Systems running in the Ganeti cluster.
593 @return: operating systems
595 @param reason: the reason for executing this operation
599 _AppendReason(query, reason)
600 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
603 def GetInfo(self, reason=None):
604 """Gets info about the cluster.
607 @param reason: the reason for executing this operation
609 @return: information about the cluster
613 _AppendReason(query, reason)
614 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
617 def RedistributeConfig(self, reason=None):
618 """Tells the cluster to redistribute its configuration files.
621 @param reason: the reason for executing this operation
627 _AppendReason(query, reason)
628 return self._SendRequest(HTTP_PUT,
629 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
632 def ModifyCluster(self, reason=None, **kwargs):
633 """Modifies cluster parameters.
635 More details for parameters can be found in the RAPI documentation.
638 @param reason: the reason for executing this operation
644 _AppendReason(query, reason)
648 return self._SendRequest(HTTP_PUT,
649 "/%s/modify" % GANETI_RAPI_VERSION, query, body)
651 def GetClusterTags(self, reason=None):
652 """Gets the cluster tags.
655 @param reason: the reason for executing this operation
657 @return: cluster tags
661 _AppendReason(query, reason)
662 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
665 def AddClusterTags(self, tags, dry_run=False, reason=None):
666 """Adds tags to the cluster.
668 @type tags: list of str
669 @param tags: tags to add to the cluster
671 @param dry_run: whether to perform a dry run
673 @param reason: the reason for executing this operation
679 query = [("tag", t) for t in tags]
680 _AppendDryRunIf(query, dry_run)
681 _AppendReason(query, reason)
683 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
686 def DeleteClusterTags(self, tags, dry_run=False, reason=None):
687 """Deletes tags from the cluster.
689 @type tags: list of str
690 @param tags: tags to delete
692 @param dry_run: whether to perform a dry run
694 @param reason: the reason for executing this operation
699 query = [("tag", t) for t in tags]
700 _AppendDryRunIf(query, dry_run)
701 _AppendReason(query, reason)
703 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
706 def GetInstances(self, bulk=False, reason=None):
707 """Gets information about instances on the cluster.
710 @param bulk: whether to return all information about all instances
712 @param reason: the reason for executing this operation
714 @rtype: list of dict or list of str
715 @return: if bulk is True, info about the instances, else a list of instances
719 _AppendIf(query, bulk, ("bulk", 1))
720 _AppendReason(query, reason)
722 instances = self._SendRequest(HTTP_GET,
723 "/%s/instances" % GANETI_RAPI_VERSION,
728 return [i["id"] for i in instances]
730 def GetInstance(self, instance, reason=None):
731 """Gets information about an instance.
734 @param instance: instance whose info to return
736 @param reason: the reason for executing this operation
739 @return: info about the instance
743 _AppendReason(query, reason)
745 return self._SendRequest(HTTP_GET,
746 ("/%s/instances/%s" %
747 (GANETI_RAPI_VERSION, instance)), query, None)
749 def GetInstanceInfo(self, instance, static=None, reason=None):
750 """Gets information about an instance.
752 @type instance: string
753 @param instance: Instance name
755 @param reason: the reason for executing this operation
761 if static is not None:
762 query.append(("static", static))
763 _AppendReason(query, reason)
765 return self._SendRequest(HTTP_GET,
766 ("/%s/instances/%s/info" %
767 (GANETI_RAPI_VERSION, instance)), query, None)
770 def _UpdateWithKwargs(base, **kwargs):
771 """Updates the base with params from kwargs.
773 @param base: The base dict, filled with required fields
775 @note: This is an inplace update of base
778 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
780 raise GanetiApiError("Required fields can not be specified as"
781 " keywords: %s" % ", ".join(conflicts))
783 base.update((key, value) for key, value in kwargs.iteritems()
786 def InstanceAllocation(self, mode, name, disk_template, disks, nics,
788 """Generates an instance allocation as used by multiallocate.
790 More details for parameters can be found in the RAPI documentation.
791 It is the same as used by CreateInstance.
794 @param mode: Instance creation mode
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,
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
805 @return: A dict with the generated entry
808 # All required fields for request data version 1
812 "disk_template": disk_template,
817 self._UpdateWithKwargs(alloc, **kwargs)
821 def InstancesMultiAlloc(self, instances, reason=None, **kwargs):
822 """Tries to allocate multiple instances.
824 More details for parameters can be found in the RAPI documentation.
826 @param instances: A list of L{InstanceAllocation} results
831 "instances": instances,
833 self._UpdateWithKwargs(body, **kwargs)
835 _AppendDryRunIf(query, kwargs.get("dry_run"))
836 _AppendReason(query, reason)
838 return self._SendRequest(HTTP_POST,
839 "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
842 def CreateInstance(self, mode, name, disk_template, disks, nics,
843 reason=None, **kwargs):
844 """Creates a new instance.
846 More details for parameters can be found in the RAPI documentation.
849 @param mode: Instance creation mode
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,
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
860 @keyword dry_run: whether to perform a dry run
862 @param reason: the reason for executing this operation
870 _AppendDryRunIf(query, kwargs.get("dry_run"))
871 _AppendReason(query, reason)
873 if _INST_CREATE_REQV1 in self.GetFeatures():
874 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
876 body[_REQ_DATA_VERSION_FIELD] = 1
878 raise GanetiApiError("Server does not support new-style (version 1)"
879 " instance creation requests")
881 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
884 def DeleteInstance(self, instance, dry_run=False, reason=None, **kwargs):
885 """Deletes an instance.
888 @param instance: the instance to delete
890 @param reason: the reason for executing this operation
899 _AppendDryRunIf(query, dry_run)
900 _AppendReason(query, reason)
902 return self._SendRequest(HTTP_DELETE,
903 ("/%s/instances/%s" %
904 (GANETI_RAPI_VERSION, instance)), query, body)
906 def ModifyInstance(self, instance, reason=None, **kwargs):
907 """Modifies an instance.
909 More details for parameters can be found in the RAPI documentation.
911 @type instance: string
912 @param instance: Instance name
914 @param reason: the reason for executing this operation
921 _AppendReason(query, reason)
923 return self._SendRequest(HTTP_PUT,
924 ("/%s/instances/%s/modify" %
925 (GANETI_RAPI_VERSION, instance)), query, body)
927 def ActivateInstanceDisks(self, instance, ignore_size=None, reason=None):
928 """Activates an instance's disks.
930 @type instance: string
931 @param instance: Instance name
932 @type ignore_size: bool
933 @param ignore_size: Whether to ignore recorded size
935 @param reason: the reason for executing this operation
941 _AppendIf(query, ignore_size, ("ignore_size", 1))
942 _AppendReason(query, reason)
944 return self._SendRequest(HTTP_PUT,
945 ("/%s/instances/%s/activate-disks" %
946 (GANETI_RAPI_VERSION, instance)), query, None)
948 def DeactivateInstanceDisks(self, instance, reason=None):
949 """Deactivates an instance's disks.
951 @type instance: string
952 @param instance: Instance name
954 @param reason: the reason for executing this operation
960 _AppendReason(query, reason)
961 return self._SendRequest(HTTP_PUT,
962 ("/%s/instances/%s/deactivate-disks" %
963 (GANETI_RAPI_VERSION, instance)), query, None)
965 def RecreateInstanceDisks(self, instance, disks=None, nodes=None,
967 """Recreate an instance's disks.
969 @type instance: string
970 @param instance: Instance name
971 @type disks: list of int
972 @param disks: List of disk indexes
973 @type nodes: list of string
974 @param nodes: New instance nodes, if relocation is desired
976 @param reason: the reason for executing this operation
982 _SetItemIf(body, disks is not None, "disks", disks)
983 _SetItemIf(body, nodes is not None, "nodes", nodes)
986 _AppendReason(query, reason)
988 return self._SendRequest(HTTP_POST,
989 ("/%s/instances/%s/recreate-disks" %
990 (GANETI_RAPI_VERSION, instance)), query, body)
992 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
994 """Grows a disk of an instance.
996 More details for parameters can be found in the RAPI documentation.
998 @type instance: string
999 @param instance: Instance name
1001 @param disk: Disk index
1002 @type amount: integer
1003 @param amount: Grow disk by this amount (MiB)
1004 @type wait_for_sync: bool
1005 @param wait_for_sync: Wait for disk to synchronize
1006 @type reason: string
1007 @param reason: the reason for executing this operation
1016 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1019 _AppendReason(query, reason)
1021 return self._SendRequest(HTTP_POST,
1022 ("/%s/instances/%s/disk/%s/grow" %
1023 (GANETI_RAPI_VERSION, instance, disk)),
1026 def GetInstanceTags(self, instance, reason=None):
1027 """Gets tags for an instance.
1030 @param instance: instance whose tags to return
1031 @type reason: string
1032 @param reason: the reason for executing this operation
1035 @return: tags for the instance
1039 _AppendReason(query, reason)
1040 return self._SendRequest(HTTP_GET,
1041 ("/%s/instances/%s/tags" %
1042 (GANETI_RAPI_VERSION, instance)), query, None)
1044 def AddInstanceTags(self, instance, tags, dry_run=False, reason=None):
1045 """Adds tags to an instance.
1048 @param instance: instance to add tags to
1049 @type tags: list of str
1050 @param tags: tags to add to the instance
1052 @param dry_run: whether to perform a dry run
1053 @type reason: string
1054 @param reason: the reason for executing this operation
1060 query = [("tag", t) for t in tags]
1061 _AppendDryRunIf(query, dry_run)
1062 _AppendReason(query, reason)
1064 return self._SendRequest(HTTP_PUT,
1065 ("/%s/instances/%s/tags" %
1066 (GANETI_RAPI_VERSION, instance)), query, None)
1068 def DeleteInstanceTags(self, instance, tags, dry_run=False, reason=None):
1069 """Deletes tags from an instance.
1072 @param instance: instance to delete tags from
1073 @type tags: list of str
1074 @param tags: tags to delete
1076 @param dry_run: whether to perform a dry run
1077 @type reason: string
1078 @param reason: the reason for executing this operation
1083 query = [("tag", t) for t in tags]
1084 _AppendDryRunIf(query, dry_run)
1085 _AppendReason(query, reason)
1087 return self._SendRequest(HTTP_DELETE,
1088 ("/%s/instances/%s/tags" %
1089 (GANETI_RAPI_VERSION, instance)), query, None)
1091 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1092 dry_run=False, reason=None, **kwargs):
1093 """Reboots an instance.
1096 @param instance: instance to reboot
1097 @type reboot_type: str
1098 @param reboot_type: one of: hard, soft, full
1099 @type ignore_secondaries: bool
1100 @param ignore_secondaries: if True, ignores errors for the secondary node
1101 while re-assembling disks (in hard-reboot mode only)
1103 @param dry_run: whether to perform a dry run
1104 @type reason: string
1105 @param reason: the reason for the reboot
1113 _AppendDryRunIf(query, dry_run)
1114 _AppendIf(query, reboot_type, ("type", reboot_type))
1115 _AppendIf(query, ignore_secondaries is not None,
1116 ("ignore_secondaries", ignore_secondaries))
1117 _AppendReason(query, reason)
1119 return self._SendRequest(HTTP_POST,
1120 ("/%s/instances/%s/reboot" %
1121 (GANETI_RAPI_VERSION, instance)), query, body)
1123 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1124 reason=None, **kwargs):
1125 """Shuts down an instance.
1128 @param instance: the instance to shut down
1130 @param dry_run: whether to perform a dry run
1131 @type no_remember: bool
1132 @param no_remember: if true, will not record the state change
1133 @type reason: string
1134 @param reason: the reason for the shutdown
1142 _AppendDryRunIf(query, dry_run)
1143 _AppendIf(query, no_remember, ("no_remember", 1))
1144 _AppendReason(query, reason)
1146 return self._SendRequest(HTTP_PUT,
1147 ("/%s/instances/%s/shutdown" %
1148 (GANETI_RAPI_VERSION, instance)), query, body)
1150 def StartupInstance(self, instance, dry_run=False, no_remember=False,
1152 """Starts up an instance.
1155 @param instance: the instance to start up
1157 @param dry_run: whether to perform a dry run
1158 @type no_remember: bool
1159 @param no_remember: if true, will not record the state change
1160 @type reason: string
1161 @param reason: the reason for the startup
1167 _AppendDryRunIf(query, dry_run)
1168 _AppendIf(query, no_remember, ("no_remember", 1))
1169 _AppendReason(query, reason)
1171 return self._SendRequest(HTTP_PUT,
1172 ("/%s/instances/%s/startup" %
1173 (GANETI_RAPI_VERSION, instance)), query, None)
1175 def ReinstallInstance(self, instance, os=None, no_startup=False,
1176 osparams=None, reason=None):
1177 """Reinstalls an instance.
1180 @param instance: The instance to reinstall
1181 @type os: str or None
1182 @param os: The operating system to reinstall. If None, the instance's
1183 current operating system will be installed again
1184 @type no_startup: bool
1185 @param no_startup: Whether to start the instance automatically
1186 @type reason: string
1187 @param reason: the reason for executing this operation
1193 _AppendReason(query, reason)
1195 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1197 "start": not no_startup,
1199 _SetItemIf(body, os is not None, "os", os)
1200 _SetItemIf(body, osparams is not None, "osparams", osparams)
1201 return self._SendRequest(HTTP_POST,
1202 ("/%s/instances/%s/reinstall" %
1203 (GANETI_RAPI_VERSION, instance)), query, body)
1205 # Use old request format
1207 raise GanetiApiError("Server does not support specifying OS parameters"
1208 " for instance reinstallation")
1211 _AppendIf(query, os, ("os", os))
1212 _AppendIf(query, no_startup, ("nostartup", 1))
1214 return self._SendRequest(HTTP_POST,
1215 ("/%s/instances/%s/reinstall" %
1216 (GANETI_RAPI_VERSION, instance)), query, None)
1218 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1219 remote_node=None, iallocator=None, reason=None):
1220 """Replaces disks on an instance.
1223 @param instance: instance whose disks to replace
1224 @type disks: list of ints
1225 @param disks: Indexes of disks to replace
1227 @param mode: replacement mode to use (defaults to replace_auto)
1228 @type remote_node: str or None
1229 @param remote_node: new secondary node to use (for use with
1230 replace_new_secondary mode)
1231 @type iallocator: str or None
1232 @param iallocator: instance allocator plugin to use (for use with
1234 @type reason: string
1235 @param reason: the reason for executing this operation
1245 # TODO: Convert to body parameters
1247 if disks is not None:
1248 _AppendIf(query, True,
1249 ("disks", ",".join(str(idx) for idx in disks)))
1251 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1252 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1253 _AppendReason(query, reason)
1255 return self._SendRequest(HTTP_POST,
1256 ("/%s/instances/%s/replace-disks" %
1257 (GANETI_RAPI_VERSION, instance)), query, None)
1259 def PrepareExport(self, instance, mode, reason=None):
1260 """Prepares an instance for an export.
1262 @type instance: string
1263 @param instance: Instance name
1265 @param mode: Export mode
1266 @type reason: string
1267 @param reason: the reason for executing this operation
1272 query = [("mode", mode)]
1273 _AppendReason(query, reason)
1274 return self._SendRequest(HTTP_PUT,
1275 ("/%s/instances/%s/prepare-export" %
1276 (GANETI_RAPI_VERSION, instance)), query, None)
1278 def ExportInstance(self, instance, mode, destination, shutdown=None,
1279 remove_instance=None,
1280 x509_key_name=None, destination_x509_ca=None, reason=None):
1281 """Exports an instance.
1283 @type instance: string
1284 @param instance: Instance name
1286 @param mode: Export mode
1287 @type reason: string
1288 @param reason: the reason for executing this operation
1294 "destination": destination,
1298 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1299 _SetItemIf(body, remove_instance is not None,
1300 "remove_instance", remove_instance)
1301 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1302 _SetItemIf(body, destination_x509_ca is not None,
1303 "destination_x509_ca", destination_x509_ca)
1306 _AppendReason(query, reason)
1308 return self._SendRequest(HTTP_PUT,
1309 ("/%s/instances/%s/export" %
1310 (GANETI_RAPI_VERSION, instance)), query, body)
1312 def MigrateInstance(self, instance, mode=None, cleanup=None,
1313 target_node=None, reason=None):
1314 """Migrates an instance.
1316 @type instance: string
1317 @param instance: Instance name
1319 @param mode: Migration mode
1321 @param cleanup: Whether to clean up a previously failed migration
1322 @type target_node: string
1323 @param target_node: Target Node for externally mirrored instances
1324 @type reason: string
1325 @param reason: the reason for executing this operation
1331 _SetItemIf(body, mode is not None, "mode", mode)
1332 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1333 _SetItemIf(body, target_node is not None, "target_node", target_node)
1336 _AppendReason(query, reason)
1338 return self._SendRequest(HTTP_PUT,
1339 ("/%s/instances/%s/migrate" %
1340 (GANETI_RAPI_VERSION, instance)), query, body)
1342 def FailoverInstance(self, instance, iallocator=None,
1343 ignore_consistency=None, target_node=None, reason=None):
1344 """Does a failover of an instance.
1346 @type instance: string
1347 @param instance: Instance name
1348 @type iallocator: string
1349 @param iallocator: Iallocator for deciding the target node for
1350 shared-storage instances
1351 @type ignore_consistency: bool
1352 @param ignore_consistency: Whether to ignore disk consistency
1353 @type target_node: string
1354 @param target_node: Target node for shared-storage instances
1355 @type reason: string
1356 @param reason: the reason for executing this operation
1362 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1363 _SetItemIf(body, ignore_consistency is not None,
1364 "ignore_consistency", ignore_consistency)
1365 _SetItemIf(body, target_node is not None, "target_node", target_node)
1368 _AppendReason(query, reason)
1370 return self._SendRequest(HTTP_PUT,
1371 ("/%s/instances/%s/failover" %
1372 (GANETI_RAPI_VERSION, instance)), query, body)
1374 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1376 """Changes the name of an instance.
1378 @type instance: string
1379 @param instance: Instance name
1380 @type new_name: string
1381 @param new_name: New instance name
1382 @type ip_check: bool
1383 @param ip_check: Whether to ensure instance's IP address is inactive
1384 @type name_check: bool
1385 @param name_check: Whether to ensure instance's name is resolvable
1386 @type reason: string
1387 @param reason: the reason for executing this operation
1393 "new_name": new_name,
1396 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1397 _SetItemIf(body, name_check is not None, "name_check", name_check)
1400 _AppendReason(query, reason)
1402 return self._SendRequest(HTTP_PUT,
1403 ("/%s/instances/%s/rename" %
1404 (GANETI_RAPI_VERSION, instance)), query, body)
1406 def GetInstanceConsole(self, instance, reason=None):
1407 """Request information for connecting to instance's console.
1409 @type instance: string
1410 @param instance: Instance name
1411 @type reason: string
1412 @param reason: the reason for executing this operation
1414 @return: dictionary containing information about instance's console
1418 _AppendReason(query, reason)
1419 return self._SendRequest(HTTP_GET,
1420 ("/%s/instances/%s/console" %
1421 (GANETI_RAPI_VERSION, instance)), query, None)
1423 def GetJobs(self, bulk=False):
1424 """Gets all jobs for the cluster.
1427 @param bulk: Whether to return detailed information about jobs.
1429 @return: List of job ids for the cluster or list of dicts with detailed
1430 information about the jobs if bulk parameter was true.
1434 _AppendIf(query, bulk, ("bulk", 1))
1437 return self._SendRequest(HTTP_GET,
1438 "/%s/jobs" % GANETI_RAPI_VERSION,
1441 return [int(j["id"])
1442 for j in self._SendRequest(HTTP_GET,
1443 "/%s/jobs" % GANETI_RAPI_VERSION,
1446 def GetJobStatus(self, job_id):
1447 """Gets the status of a job.
1449 @type job_id: string
1450 @param job_id: job id whose status to query
1456 return self._SendRequest(HTTP_GET,
1457 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1460 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1461 """Polls cluster for job status until completion.
1463 Completion is defined as any of the following states listed in
1464 L{JOB_STATUS_FINALIZED}.
1466 @type job_id: string
1467 @param job_id: job id to watch
1469 @param period: how often to poll for status (optional, default 5s)
1471 @param retries: how many time to poll before giving up
1472 (optional, default -1 means unlimited)
1475 @return: C{True} if job succeeded or C{False} if failed/status timeout
1476 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1477 possible; L{WaitForJobChange} returns immediately after a job changed and
1478 does not use polling
1482 job_result = self.GetJobStatus(job_id)
1484 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1486 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1497 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1498 """Waits for job changes.
1500 @type job_id: string
1501 @param job_id: Job ID for which to wait
1502 @return: C{None} if no changes have been detected and a dict with two keys,
1503 C{job_info} and C{log_entries} otherwise.
1509 "previous_job_info": prev_job_info,
1510 "previous_log_serial": prev_log_serial,
1513 return self._SendRequest(HTTP_GET,
1514 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1517 def CancelJob(self, job_id, dry_run=False):
1520 @type job_id: string
1521 @param job_id: id of the job to delete
1523 @param dry_run: whether to perform a dry run
1525 @return: tuple containing the result, and a message (bool, string)
1529 _AppendDryRunIf(query, dry_run)
1531 return self._SendRequest(HTTP_DELETE,
1532 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1535 def GetNodes(self, bulk=False, reason=None):
1536 """Gets all nodes in the cluster.
1539 @param bulk: whether to return all information about all instances
1540 @type reason: string
1541 @param reason: the reason for executing this operation
1543 @rtype: list of dict or str
1544 @return: if bulk is true, info about nodes in the cluster,
1545 else list of nodes in the cluster
1549 _AppendIf(query, bulk, ("bulk", 1))
1550 _AppendReason(query, reason)
1552 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1557 return [n["id"] for n in nodes]
1559 def GetNode(self, node, reason=None):
1560 """Gets information about a node.
1563 @param node: node whose info to return
1564 @type reason: string
1565 @param reason: the reason for executing this operation
1568 @return: info about the node
1572 _AppendReason(query, reason)
1574 return self._SendRequest(HTTP_GET,
1575 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1578 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1579 dry_run=False, early_release=None,
1580 mode=None, accept_old=False, reason=None):
1581 """Evacuates instances from a Ganeti node.
1584 @param node: node to evacuate
1585 @type iallocator: str or None
1586 @param iallocator: instance allocator to use
1587 @type remote_node: str
1588 @param remote_node: node to evaucate to
1590 @param dry_run: whether to perform a dry run
1591 @type early_release: bool
1592 @param early_release: whether to enable parallelization
1594 @param mode: Node evacuation mode
1595 @type accept_old: bool
1596 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1598 @type reason: string
1599 @param reason: the reason for executing this operation
1601 @rtype: string, or a list for pre-2.5 results
1602 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1603 list of (job ID, instance name, new secondary node); if dry_run was
1604 specified, then the actual move jobs were not submitted and the job IDs
1607 @raises GanetiApiError: if an iallocator and remote_node are both
1611 if iallocator and remote_node:
1612 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1615 _AppendDryRunIf(query, dry_run)
1616 _AppendReason(query, reason)
1618 if _NODE_EVAC_RES1 in self.GetFeatures():
1619 # Server supports body parameters
1622 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1623 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1624 _SetItemIf(body, early_release is not None,
1625 "early_release", early_release)
1626 _SetItemIf(body, mode is not None, "mode", mode)
1628 # Pre-2.5 request format
1632 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1633 " not accept old-style results (parameter"
1636 # Pre-2.5 servers can only evacuate secondaries
1637 if mode is not None and mode != NODE_EVAC_SEC:
1638 raise GanetiApiError("Server can only evacuate secondary instances")
1640 _AppendIf(query, iallocator, ("iallocator", iallocator))
1641 _AppendIf(query, remote_node, ("remote_node", remote_node))
1642 _AppendIf(query, early_release, ("early_release", 1))
1644 return self._SendRequest(HTTP_POST,
1645 ("/%s/nodes/%s/evacuate" %
1646 (GANETI_RAPI_VERSION, node)), query, body)
1648 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1649 target_node=None, reason=None):
1650 """Migrates all primary instances from a node.
1653 @param node: node to migrate
1655 @param mode: if passed, it will overwrite the live migration type,
1656 otherwise the hypervisor default will be used
1658 @param dry_run: whether to perform a dry run
1659 @type iallocator: string
1660 @param iallocator: instance allocator to use
1661 @type target_node: string
1662 @param target_node: Target node for shared-storage instances
1663 @type reason: string
1664 @param reason: the reason for executing this operation
1671 _AppendDryRunIf(query, dry_run)
1672 _AppendReason(query, reason)
1674 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1677 _SetItemIf(body, mode is not None, "mode", mode)
1678 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1679 _SetItemIf(body, target_node is not None, "target_node", target_node)
1681 assert len(query) <= 1
1683 return self._SendRequest(HTTP_POST,
1684 ("/%s/nodes/%s/migrate" %
1685 (GANETI_RAPI_VERSION, node)), query, body)
1687 # Use old request format
1688 if target_node is not None:
1689 raise GanetiApiError("Server does not support specifying target node"
1690 " for node migration")
1692 _AppendIf(query, mode is not None, ("mode", mode))
1694 return self._SendRequest(HTTP_POST,
1695 ("/%s/nodes/%s/migrate" %
1696 (GANETI_RAPI_VERSION, node)), query, None)
1698 def GetNodeRole(self, node, reason=None):
1699 """Gets the current role for a node.
1702 @param node: node whose role to return
1703 @type reason: string
1704 @param reason: the reason for executing this operation
1707 @return: the current role for a node
1711 _AppendReason(query, reason)
1713 return self._SendRequest(HTTP_GET,
1714 ("/%s/nodes/%s/role" %
1715 (GANETI_RAPI_VERSION, node)), query, None)
1717 def SetNodeRole(self, node, role, force=False, auto_promote=None,
1719 """Sets the role for a node.
1722 @param node: the node whose role to set
1724 @param role: the role to set for the node
1726 @param force: whether to force the role change
1727 @type auto_promote: bool
1728 @param auto_promote: Whether node(s) should be promoted to master candidate
1730 @type reason: string
1731 @param reason: the reason for executing this operation
1738 _AppendForceIf(query, force)
1739 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1740 _AppendReason(query, reason)
1742 return self._SendRequest(HTTP_PUT,
1743 ("/%s/nodes/%s/role" %
1744 (GANETI_RAPI_VERSION, node)), query, role)
1746 def PowercycleNode(self, node, force=False, reason=None):
1747 """Powercycles a node.
1750 @param node: Node name
1752 @param force: Whether to force the operation
1753 @type reason: string
1754 @param reason: the reason for executing this operation
1760 _AppendForceIf(query, force)
1761 _AppendReason(query, reason)
1763 return self._SendRequest(HTTP_POST,
1764 ("/%s/nodes/%s/powercycle" %
1765 (GANETI_RAPI_VERSION, node)), query, None)
1767 def ModifyNode(self, node, reason=None, **kwargs):
1770 More details for parameters can be found in the RAPI documentation.
1773 @param node: Node name
1774 @type reason: string
1775 @param reason: the reason for executing this operation
1781 _AppendReason(query, reason)
1783 return self._SendRequest(HTTP_POST,
1784 ("/%s/nodes/%s/modify" %
1785 (GANETI_RAPI_VERSION, node)), query, kwargs)
1787 def GetNodeStorageUnits(self, node, storage_type, output_fields, reason=None):
1788 """Gets the storage units for a node.
1791 @param node: the node whose storage units to return
1792 @type storage_type: str
1793 @param storage_type: storage type whose units to return
1794 @type output_fields: str
1795 @param output_fields: storage type fields to return
1796 @type reason: string
1797 @param reason: the reason for executing this operation
1800 @return: job id where results can be retrieved
1804 ("storage_type", storage_type),
1805 ("output_fields", output_fields),
1807 _AppendReason(query, reason)
1809 return self._SendRequest(HTTP_GET,
1810 ("/%s/nodes/%s/storage" %
1811 (GANETI_RAPI_VERSION, node)), query, None)
1813 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None,
1815 """Modifies parameters of storage units on the node.
1818 @param node: node whose storage units to modify
1819 @type storage_type: str
1820 @param storage_type: storage type whose units to modify
1822 @param name: name of the storage unit
1823 @type allocatable: bool or None
1824 @param allocatable: Whether to set the "allocatable" flag on the storage
1825 unit (None=no modification, True=set, False=unset)
1826 @type reason: string
1827 @param reason: the reason for executing this operation
1834 ("storage_type", storage_type),
1838 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1839 _AppendReason(query, reason)
1841 return self._SendRequest(HTTP_PUT,
1842 ("/%s/nodes/%s/storage/modify" %
1843 (GANETI_RAPI_VERSION, node)), query, None)
1845 def RepairNodeStorageUnits(self, node, storage_type, name, reason=None):
1846 """Repairs a storage unit on the node.
1849 @param node: node whose storage units to repair
1850 @type storage_type: str
1851 @param storage_type: storage type to repair
1853 @param name: name of the storage unit to repair
1854 @type reason: string
1855 @param reason: the reason for executing this operation
1862 ("storage_type", storage_type),
1865 _AppendReason(query, reason)
1867 return self._SendRequest(HTTP_PUT,
1868 ("/%s/nodes/%s/storage/repair" %
1869 (GANETI_RAPI_VERSION, node)), query, None)
1871 def GetNodeTags(self, node, reason=None):
1872 """Gets the tags for a node.
1875 @param node: node whose tags to return
1876 @type reason: string
1877 @param reason: the reason for executing this operation
1880 @return: tags for the node
1884 _AppendReason(query, reason)
1886 return self._SendRequest(HTTP_GET,
1887 ("/%s/nodes/%s/tags" %
1888 (GANETI_RAPI_VERSION, node)), query, None)
1890 def AddNodeTags(self, node, tags, dry_run=False, reason=None):
1891 """Adds tags to a node.
1894 @param node: node to add tags to
1895 @type tags: list of str
1896 @param tags: tags to add to the node
1898 @param dry_run: whether to perform a dry run
1899 @type reason: string
1900 @param reason: the reason for executing this operation
1906 query = [("tag", t) for t in tags]
1907 _AppendDryRunIf(query, dry_run)
1908 _AppendReason(query, reason)
1910 return self._SendRequest(HTTP_PUT,
1911 ("/%s/nodes/%s/tags" %
1912 (GANETI_RAPI_VERSION, node)), query, tags)
1914 def DeleteNodeTags(self, node, tags, dry_run=False, reason=None):
1915 """Delete tags from a node.
1918 @param node: node to remove tags from
1919 @type tags: list of str
1920 @param tags: tags to remove from the node
1922 @param dry_run: whether to perform a dry run
1923 @type reason: string
1924 @param reason: the reason for executing this operation
1930 query = [("tag", t) for t in tags]
1931 _AppendDryRunIf(query, dry_run)
1932 _AppendReason(query, reason)
1934 return self._SendRequest(HTTP_DELETE,
1935 ("/%s/nodes/%s/tags" %
1936 (GANETI_RAPI_VERSION, node)), query, None)
1938 def GetNetworks(self, bulk=False, reason=None):
1939 """Gets all networks in the cluster.
1942 @param bulk: whether to return all information about the networks
1944 @rtype: list of dict or str
1945 @return: if bulk is true, a list of dictionaries with info about all
1946 networks in the cluster, else a list of names of those networks
1950 _AppendIf(query, bulk, ("bulk", 1))
1951 _AppendReason(query, reason)
1953 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1958 return [n["name"] for n in networks]
1960 def GetNetwork(self, network, reason=None):
1961 """Gets information about a network.
1964 @param network: name of the network whose info to return
1965 @type reason: string
1966 @param reason: the reason for executing this operation
1969 @return: info about the network
1973 _AppendReason(query, reason)
1975 return self._SendRequest(HTTP_GET,
1976 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1979 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1980 gateway6=None, mac_prefix=None,
1981 add_reserved_ips=None, tags=None, dry_run=False,
1983 """Creates a new network.
1985 @type network_name: str
1986 @param network_name: the name of network to create
1988 @param dry_run: whether to peform a dry run
1989 @type reason: string
1990 @param reason: the reason for executing this operation
1997 _AppendDryRunIf(query, dry_run)
1998 _AppendReason(query, reason)
2000 if add_reserved_ips:
2001 add_reserved_ips = add_reserved_ips.split(",")
2004 tags = tags.split(",")
2007 "network_name": network_name,
2010 "gateway6": gateway6,
2011 "network6": network6,
2012 "mac_prefix": mac_prefix,
2013 "add_reserved_ips": add_reserved_ips,
2017 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2020 def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False,
2022 """Connects a Network to a NodeGroup with the given netparams
2026 "group_name": group_name,
2027 "network_mode": mode,
2028 "network_link": link,
2032 _AppendDryRunIf(query, dry_run)
2033 _AppendReason(query, reason)
2035 return self._SendRequest(HTTP_PUT,
2036 ("/%s/networks/%s/connect" %
2037 (GANETI_RAPI_VERSION, network_name)), query, body)
2039 def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2041 """Connects a Network to a NodeGroup with the given netparams
2045 "group_name": group_name,
2049 _AppendDryRunIf(query, dry_run)
2050 _AppendReason(query, reason)
2052 return self._SendRequest(HTTP_PUT,
2053 ("/%s/networks/%s/disconnect" %
2054 (GANETI_RAPI_VERSION, network_name)), query, body)
2056 def ModifyNetwork(self, network, reason=None, **kwargs):
2057 """Modifies a network.
2059 More details for parameters can be found in the RAPI documentation.
2061 @type network: string
2062 @param network: Network name
2063 @type reason: string
2064 @param reason: the reason for executing this operation
2070 _AppendReason(query, reason)
2072 return self._SendRequest(HTTP_PUT,
2073 ("/%s/networks/%s/modify" %
2074 (GANETI_RAPI_VERSION, network)), None, kwargs)
2076 def DeleteNetwork(self, network, dry_run=False, reason=None):
2077 """Deletes a network.
2080 @param network: the network to delete
2082 @param dry_run: whether to peform a dry run
2083 @type reason: string
2084 @param reason: the reason for executing this operation
2091 _AppendDryRunIf(query, dry_run)
2092 _AppendReason(query, reason)
2094 return self._SendRequest(HTTP_DELETE,
2095 ("/%s/networks/%s" %
2096 (GANETI_RAPI_VERSION, network)), query, None)
2098 def GetNetworkTags(self, network, reason=None):
2099 """Gets tags for a network.
2101 @type network: string
2102 @param network: Node group whose tags to return
2103 @type reason: string
2104 @param reason: the reason for executing this operation
2106 @rtype: list of strings
2107 @return: tags for the network
2111 _AppendReason(query, reason)
2113 return self._SendRequest(HTTP_GET,
2114 ("/%s/networks/%s/tags" %
2115 (GANETI_RAPI_VERSION, network)), query, None)
2117 def AddNetworkTags(self, network, tags, dry_run=False, reason=None):
2118 """Adds tags to a network.
2121 @param network: network to add tags to
2122 @type tags: list of string
2123 @param tags: tags to add to the network
2125 @param dry_run: whether to perform a dry run
2126 @type reason: string
2127 @param reason: the reason for executing this operation
2133 query = [("tag", t) for t in tags]
2134 _AppendDryRunIf(query, dry_run)
2135 _AppendReason(query, reason)
2137 return self._SendRequest(HTTP_PUT,
2138 ("/%s/networks/%s/tags" %
2139 (GANETI_RAPI_VERSION, network)), query, None)
2141 def DeleteNetworkTags(self, network, tags, dry_run=False, reason=None):
2142 """Deletes tags from a network.
2145 @param network: network to delete tags from
2146 @type tags: list of string
2147 @param tags: tags to delete
2149 @param dry_run: whether to perform a dry run
2150 @type reason: string
2151 @param reason: the reason for executing this operation
2156 query = [("tag", t) for t in tags]
2157 _AppendDryRunIf(query, dry_run)
2158 _AppendReason(query, reason)
2160 return self._SendRequest(HTTP_DELETE,
2161 ("/%s/networks/%s/tags" %
2162 (GANETI_RAPI_VERSION, network)), query, None)
2164 def GetGroups(self, bulk=False, reason=None):
2165 """Gets all node groups in the cluster.
2168 @param bulk: whether to return all information about the groups
2169 @type reason: string
2170 @param reason: the reason for executing this operation
2172 @rtype: list of dict or str
2173 @return: if bulk is true, a list of dictionaries with info about all node
2174 groups in the cluster, else a list of names of those node groups
2178 _AppendIf(query, bulk, ("bulk", 1))
2179 _AppendReason(query, reason)
2181 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2186 return [g["name"] for g in groups]
2188 def GetGroup(self, group, reason=None):
2189 """Gets information about a node group.
2192 @param group: name of the node group whose info to return
2193 @type reason: string
2194 @param reason: the reason for executing this operation
2197 @return: info about the node group
2201 _AppendReason(query, reason)
2203 return self._SendRequest(HTTP_GET,
2204 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2207 def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2208 """Creates a new node group.
2211 @param name: the name of node group to create
2212 @type alloc_policy: str
2213 @param alloc_policy: the desired allocation policy for the group, if any
2215 @param dry_run: whether to peform a dry run
2216 @type reason: string
2217 @param reason: the reason for executing this operation
2224 _AppendDryRunIf(query, dry_run)
2225 _AppendReason(query, reason)
2229 "alloc_policy": alloc_policy,
2232 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2235 def ModifyGroup(self, group, reason=None, **kwargs):
2236 """Modifies a node group.
2238 More details for parameters can be found in the RAPI documentation.
2241 @param group: Node group name
2242 @type reason: string
2243 @param reason: the reason for executing this operation
2249 _AppendReason(query, reason)
2251 return self._SendRequest(HTTP_PUT,
2252 ("/%s/groups/%s/modify" %
2253 (GANETI_RAPI_VERSION, group)), query, kwargs)
2255 def DeleteGroup(self, group, dry_run=False, reason=None):
2256 """Deletes a node group.
2259 @param group: the node group to delete
2261 @param dry_run: whether to peform a dry run
2262 @type reason: string
2263 @param reason: the reason for executing this operation
2270 _AppendDryRunIf(query, dry_run)
2271 _AppendReason(query, reason)
2273 return self._SendRequest(HTTP_DELETE,
2275 (GANETI_RAPI_VERSION, group)), query, None)
2277 def RenameGroup(self, group, new_name, reason=None):
2278 """Changes the name of a node group.
2281 @param group: Node group name
2282 @type new_name: string
2283 @param new_name: New node group name
2284 @type reason: string
2285 @param reason: the reason for executing this operation
2292 "new_name": new_name,
2296 _AppendReason(query, reason)
2298 return self._SendRequest(HTTP_PUT,
2299 ("/%s/groups/%s/rename" %
2300 (GANETI_RAPI_VERSION, group)), query, body)
2302 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2304 """Assigns nodes to a group.
2307 @param group: Node group name
2308 @type nodes: list of strings
2309 @param nodes: List of nodes to assign to the group
2310 @type reason: string
2311 @param reason: the reason for executing this operation
2318 _AppendForceIf(query, force)
2319 _AppendDryRunIf(query, dry_run)
2320 _AppendReason(query, reason)
2326 return self._SendRequest(HTTP_PUT,
2327 ("/%s/groups/%s/assign-nodes" %
2328 (GANETI_RAPI_VERSION, group)), query, body)
2330 def GetGroupTags(self, group, reason=None):
2331 """Gets tags for a node group.
2334 @param group: Node group whose tags to return
2335 @type reason: string
2336 @param reason: the reason for executing this operation
2338 @rtype: list of strings
2339 @return: tags for the group
2343 _AppendReason(query, reason)
2345 return self._SendRequest(HTTP_GET,
2346 ("/%s/groups/%s/tags" %
2347 (GANETI_RAPI_VERSION, group)), query, None)
2349 def AddGroupTags(self, group, tags, dry_run=False, reason=None):
2350 """Adds tags to a node group.
2353 @param group: group to add tags to
2354 @type tags: list of string
2355 @param tags: tags to add to the group
2357 @param dry_run: whether to perform a dry run
2358 @type reason: string
2359 @param reason: the reason for executing this operation
2365 query = [("tag", t) for t in tags]
2366 _AppendDryRunIf(query, dry_run)
2367 _AppendReason(query, reason)
2369 return self._SendRequest(HTTP_PUT,
2370 ("/%s/groups/%s/tags" %
2371 (GANETI_RAPI_VERSION, group)), query, None)
2373 def DeleteGroupTags(self, group, tags, dry_run=False, reason=None):
2374 """Deletes tags from a node group.
2377 @param group: group to delete tags from
2378 @type tags: list of string
2379 @param tags: tags to delete
2381 @param dry_run: whether to perform a dry run
2382 @type reason: string
2383 @param reason: the reason for executing this operation
2388 query = [("tag", t) for t in tags]
2389 _AppendDryRunIf(query, dry_run)
2390 _AppendReason(query, reason)
2392 return self._SendRequest(HTTP_DELETE,
2393 ("/%s/groups/%s/tags" %
2394 (GANETI_RAPI_VERSION, group)), query, None)
2396 def Query(self, what, fields, qfilter=None, reason=None):
2397 """Retrieves information about resources.
2400 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2401 @type fields: list of string
2402 @param fields: Requested fields
2403 @type qfilter: None or list
2404 @param qfilter: Query filter
2405 @type reason: string
2406 @param reason: the reason for executing this operation
2413 _AppendReason(query, reason)
2419 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2420 # TODO: remove "filter" after 2.7
2421 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2423 return self._SendRequest(HTTP_PUT,
2425 (GANETI_RAPI_VERSION, what)), query, body)
2427 def QueryFields(self, what, fields=None, reason=None):
2428 """Retrieves available fields for a resource.
2431 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2432 @type fields: list of string
2433 @param fields: Requested fields
2434 @type reason: string
2435 @param reason: the reason for executing this operation
2442 _AppendReason(query, reason)
2444 if fields is not None:
2445 _AppendIf(query, True, ("fields", ",".join(fields)))
2447 return self._SendRequest(HTTP_GET,
2448 ("/%s/query/%s/fields" %
2449 (GANETI_RAPI_VERSION, what)), query, None)