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
117 # Older pycURL versions don't have all error constants
119 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
120 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
121 except AttributeError:
122 _CURLE_SSL_CACERT = 60
123 _CURLE_SSL_CACERT_BADFILE = 77
125 _CURL_SSL_CERT_ERRORS = frozenset([
127 _CURLE_SSL_CACERT_BADFILE,
131 class Error(Exception):
132 """Base error class for this module.
138 class GanetiApiError(Error):
139 """Generic error raised from Ganeti API.
142 def __init__(self, msg, code=None):
143 Error.__init__(self, msg)
147 class CertificateError(GanetiApiError):
148 """Raised when a problem is found with the SSL certificate.
154 def _AppendIf(container, condition, value):
155 """Appends to a list if a condition evaluates to truth.
159 container.append(value)
164 def _AppendDryRunIf(container, condition):
165 """Appends a "dry-run" parameter if a condition evaluates to truth.
168 return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
171 def _AppendForceIf(container, condition):
172 """Appends a "force" parameter if a condition evaluates to truth.
175 return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
178 def _SetItemIf(container, condition, item, value):
179 """Sets an item if a condition evaluates to truth.
183 container[item] = value
188 def UsesRapiClient(fn):
189 """Decorator for code using RAPI client to initialize pycURL.
192 def wrapper(*args, **kwargs):
193 # curl_global_init(3) and curl_global_cleanup(3) must be called with only
194 # one thread running. This check is just a safety measure -- it doesn't
196 assert threading.activeCount() == 1, \
197 "Found active threads when initializing pycURL"
199 pycurl.global_init(pycurl.GLOBAL_ALL)
201 return fn(*args, **kwargs)
203 pycurl.global_cleanup()
208 def GenericCurlConfig(verbose=False, use_signal=False,
209 use_curl_cabundle=False, cafile=None, capath=None,
210 proxy=None, verify_hostname=False,
211 connect_timeout=None, timeout=None,
212 _pycurl_version_fn=pycurl.version_info):
213 """Curl configuration function generator.
216 @param verbose: Whether to set cURL to verbose mode
217 @type use_signal: bool
218 @param use_signal: Whether to allow cURL to use signals
219 @type use_curl_cabundle: bool
220 @param use_curl_cabundle: Whether to use cURL's default CA bundle
222 @param cafile: In which file we can find the certificates
224 @param capath: In which directory we can find the certificates
226 @param proxy: Proxy to use, None for default behaviour and empty string for
227 disabling proxies (see curl_easy_setopt(3))
228 @type verify_hostname: bool
229 @param verify_hostname: Whether to verify the remote peer certificate's
231 @type connect_timeout: number
232 @param connect_timeout: Timeout for establishing connection in seconds
233 @type timeout: number
234 @param timeout: Timeout for complete transfer in seconds (see
235 curl_easy_setopt(3)).
238 if use_curl_cabundle and (cafile or capath):
239 raise Error("Can not use default CA bundle when CA file or path is set")
241 def _ConfigCurl(curl, logger):
242 """Configures a cURL object
244 @type curl: pycurl.Curl
245 @param curl: cURL object
248 logger.debug("Using cURL version %s", pycurl.version)
250 # pycurl.version_info returns a tuple with information about the used
251 # version of libcurl. Item 5 is the SSL library linked to it.
252 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
254 sslver = _pycurl_version_fn()[5]
256 raise Error("No SSL support in cURL")
258 lcsslver = sslver.lower()
259 if lcsslver.startswith("openssl/"):
261 elif lcsslver.startswith("nss/"):
262 # TODO: investigate compatibility beyond a simple test
264 elif lcsslver.startswith("gnutls/"):
266 raise Error("cURL linked against GnuTLS has no support for a"
267 " CA path (%s)" % (pycurl.version, ))
269 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
272 curl.setopt(pycurl.VERBOSE, verbose)
273 curl.setopt(pycurl.NOSIGNAL, not use_signal)
275 # Whether to verify remote peer's CN
277 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
278 # certificate must indicate that the server is the server to which you
279 # meant to connect, or the connection fails. [...] When the value is 1,
280 # the certificate must contain a Common Name field, but it doesn't matter
281 # what name it says. [...]"
282 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
284 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
286 if cafile or capath or use_curl_cabundle:
287 # Require certificates to be checked
288 curl.setopt(pycurl.SSL_VERIFYPEER, True)
290 curl.setopt(pycurl.CAINFO, str(cafile))
292 curl.setopt(pycurl.CAPATH, str(capath))
293 # Not changing anything for using default CA bundle
295 # Disable SSL certificate verification
296 curl.setopt(pycurl.SSL_VERIFYPEER, False)
298 if proxy is not None:
299 curl.setopt(pycurl.PROXY, str(proxy))
302 if connect_timeout is not None:
303 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
304 if timeout is not None:
305 curl.setopt(pycurl.TIMEOUT, timeout)
310 class GanetiRapiClient(object): # pylint: disable=R0904
311 """Ganeti RAPI client.
314 USER_AGENT = "Ganeti RAPI Client"
315 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
317 def __init__(self, host, port=GANETI_RAPI_PORT,
318 username=None, password=None, logger=logging,
319 curl_config_fn=None, curl_factory=None):
320 """Initializes this class.
323 @param host: the ganeti cluster master to interact with
325 @param port: the port on which the RAPI is running (default is 5080)
326 @type username: string
327 @param username: the username to connect with
328 @type password: string
329 @param password: the password to connect with
330 @type curl_config_fn: callable
331 @param curl_config_fn: Function to configure C{pycurl.Curl} object
332 @param logger: Logging object
335 self._username = username
336 self._password = password
337 self._logger = logger
338 self._curl_config_fn = curl_config_fn
339 self._curl_factory = curl_factory
342 socket.inet_pton(socket.AF_INET6, host)
343 address = "[%s]:%s" % (host, port)
345 address = "%s:%s" % (host, port)
347 self._base_url = "https://%s" % address
349 if username is not None:
351 raise Error("Password not specified")
353 raise Error("Specified password without username")
355 def _CreateCurl(self):
356 """Creates a cURL object.
359 # Create pycURL object if no factory is provided
360 if self._curl_factory:
361 curl = self._curl_factory()
365 # Default cURL settings
366 curl.setopt(pycurl.VERBOSE, False)
367 curl.setopt(pycurl.FOLLOWLOCATION, False)
368 curl.setopt(pycurl.MAXREDIRS, 5)
369 curl.setopt(pycurl.NOSIGNAL, True)
370 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
371 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
372 curl.setopt(pycurl.SSL_VERIFYPEER, False)
373 curl.setopt(pycurl.HTTPHEADER, [
374 "Accept: %s" % HTTP_APP_JSON,
375 "Content-type: %s" % HTTP_APP_JSON,
378 assert ((self._username is None and self._password is None) ^
379 (self._username is not None and self._password is not None))
382 # Setup authentication
383 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
384 curl.setopt(pycurl.USERPWD,
385 str("%s:%s" % (self._username, self._password)))
387 # Call external configuration function
388 if self._curl_config_fn:
389 self._curl_config_fn(curl, self._logger)
394 def _EncodeQuery(query):
395 """Encode query values for RAPI URL.
397 @type query: list of two-tuples
398 @param query: Query arguments
400 @return: Query list with encoded values
405 for name, value in query:
407 result.append((name, ""))
409 elif isinstance(value, bool):
410 # Boolean values must be encoded as 0 or 1
411 result.append((name, int(value)))
413 elif isinstance(value, (list, tuple, dict)):
414 raise ValueError("Invalid query data type %r" % type(value).__name__)
417 result.append((name, value))
421 def _SendRequest(self, method, path, query, content):
422 """Sends an HTTP request.
424 This constructs a full URL, encodes and decodes HTTP bodies, and
425 handles invalid responses in a pythonic way.
428 @param method: HTTP method to use
430 @param path: HTTP URL path
431 @type query: list of two-tuples
432 @param query: query arguments to pass to urllib.urlencode
433 @type content: str or None
434 @param content: HTTP body content
437 @return: JSON-Decoded response
439 @raises CertificateError: If an invalid SSL certificate is found
440 @raises GanetiApiError: If an invalid response is returned
443 assert path.startswith("/")
445 curl = self._CreateCurl()
447 if content is not None:
448 encoded_content = self._json_encoder.encode(content)
453 urlparts = [self._base_url, path]
456 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
458 url = "".join(urlparts)
460 self._logger.debug("Sending request %s %s (content=%r)",
461 method, url, encoded_content)
463 # Buffer for response
464 encoded_resp_body = StringIO()
467 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
468 curl.setopt(pycurl.URL, str(url))
469 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
470 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
473 # Send request and wait for response
476 except pycurl.error, err:
477 if err.args[0] in _CURL_SSL_CERT_ERRORS:
478 raise CertificateError("SSL certificate error %s" % err,
481 raise GanetiApiError(str(err), code=err.args[0])
483 # Reset settings to not keep references to large objects in memory
485 curl.setopt(pycurl.POSTFIELDS, "")
486 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
488 # Get HTTP response code
489 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
491 # Was anything written to the response buffer?
492 if encoded_resp_body.tell():
493 response_content = simplejson.loads(encoded_resp_body.getvalue())
495 response_content = None
497 if http_code != HTTP_OK:
498 if isinstance(response_content, dict):
500 (response_content["code"],
501 response_content["message"],
502 response_content["explain"]))
504 msg = str(response_content)
506 raise GanetiApiError(msg, code=http_code)
508 return response_content
510 def GetVersion(self):
511 """Gets the Remote API version running on the cluster.
514 @return: Ganeti Remote API version
517 return self._SendRequest(HTTP_GET, "/version", None, None)
519 def GetFeatures(self):
520 """Gets the list of optional features supported by RAPI server.
523 @return: List of optional features
527 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
529 except GanetiApiError, err:
530 # Older RAPI servers don't support this resource
531 if err.code == HTTP_NOT_FOUND:
536 def GetOperatingSystems(self):
537 """Gets the Operating Systems running in the Ganeti cluster.
540 @return: operating systems
543 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
547 """Gets info about the cluster.
550 @return: information about the cluster
553 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
556 def RedistributeConfig(self):
557 """Tells the cluster to redistribute its configuration files.
563 return self._SendRequest(HTTP_PUT,
564 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
567 def ModifyCluster(self, **kwargs):
568 """Modifies cluster parameters.
570 More details for parameters can be found in the RAPI documentation.
578 return self._SendRequest(HTTP_PUT,
579 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
581 def GetClusterTags(self):
582 """Gets the cluster tags.
585 @return: cluster tags
588 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
591 def AddClusterTags(self, tags, dry_run=False):
592 """Adds tags to the cluster.
594 @type tags: list of str
595 @param tags: tags to add to the cluster
597 @param dry_run: whether to perform a dry run
603 query = [("tag", t) for t in tags]
604 _AppendDryRunIf(query, dry_run)
606 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
609 def DeleteClusterTags(self, tags, dry_run=False):
610 """Deletes tags from the cluster.
612 @type tags: list of str
613 @param tags: tags to delete
615 @param dry_run: whether to perform a dry run
620 query = [("tag", t) for t in tags]
621 _AppendDryRunIf(query, dry_run)
623 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
626 def GetInstances(self, bulk=False):
627 """Gets information about instances on the cluster.
630 @param bulk: whether to return all information about all instances
632 @rtype: list of dict or list of str
633 @return: if bulk is True, info about the instances, else a list of instances
637 _AppendIf(query, bulk, ("bulk", 1))
639 instances = self._SendRequest(HTTP_GET,
640 "/%s/instances" % GANETI_RAPI_VERSION,
645 return [i["id"] for i in instances]
647 def GetInstance(self, instance):
648 """Gets information about an instance.
651 @param instance: instance whose info to return
654 @return: info about the instance
657 return self._SendRequest(HTTP_GET,
658 ("/%s/instances/%s" %
659 (GANETI_RAPI_VERSION, instance)), None, None)
661 def GetInstanceInfo(self, instance, static=None):
662 """Gets information about an instance.
664 @type instance: string
665 @param instance: Instance name
670 if static is not None:
671 query = [("static", static)]
675 return self._SendRequest(HTTP_GET,
676 ("/%s/instances/%s/info" %
677 (GANETI_RAPI_VERSION, instance)), query, None)
680 def _UpdateWithKwargs(base, **kwargs):
681 """Updates the base with params from kwargs.
683 @param base: The base dict, filled with required fields
685 @note: This is an inplace update of base
688 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
690 raise GanetiApiError("Required fields can not be specified as"
691 " keywords: %s" % ", ".join(conflicts))
693 base.update((key, value) for key, value in kwargs.iteritems()
696 def InstanceAllocation(self, mode, name, disk_template, disks, nics,
698 """Generates an instance allocation as used by multiallocate.
700 More details for parameters can be found in the RAPI documentation.
701 It is the same as used by CreateInstance.
704 @param mode: Instance creation mode
706 @param name: Hostname of the instance to create
707 @type disk_template: string
708 @param disk_template: Disk template for instance (e.g. plain, diskless,
710 @type disks: list of dicts
711 @param disks: List of disk definitions
712 @type nics: list of dicts
713 @param nics: List of NIC definitions
715 @return: A dict with the generated entry
718 # All required fields for request data version 1
722 "disk_template": disk_template,
727 self._UpdateWithKwargs(alloc, **kwargs)
731 def InstancesMultiAlloc(self, instances, **kwargs):
732 """Tries to allocate multiple instances.
734 More details for parameters can be found in the RAPI documentation.
736 @param instances: A list of L{InstanceAllocation} results
741 "instances": instances,
743 self._UpdateWithKwargs(body, **kwargs)
745 _AppendDryRunIf(query, kwargs.get("dry_run"))
747 return self._SendRequest(HTTP_POST,
748 "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
751 def CreateInstance(self, mode, name, disk_template, disks, nics,
753 """Creates a new instance.
755 More details for parameters can be found in the RAPI documentation.
758 @param mode: Instance creation mode
760 @param name: Hostname of the instance to create
761 @type disk_template: string
762 @param disk_template: Disk template for instance (e.g. plain, diskless,
764 @type disks: list of dicts
765 @param disks: List of disk definitions
766 @type nics: list of dicts
767 @param nics: List of NIC definitions
769 @keyword dry_run: whether to perform a dry run
777 _AppendDryRunIf(query, kwargs.get("dry_run"))
779 if _INST_CREATE_REQV1 in self.GetFeatures():
780 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
782 body[_REQ_DATA_VERSION_FIELD] = 1
784 raise GanetiApiError("Server does not support new-style (version 1)"
785 " instance creation requests")
787 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
790 def DeleteInstance(self, instance, dry_run=False):
791 """Deletes an instance.
794 @param instance: the instance to delete
801 _AppendDryRunIf(query, dry_run)
803 return self._SendRequest(HTTP_DELETE,
804 ("/%s/instances/%s" %
805 (GANETI_RAPI_VERSION, instance)), query, None)
807 def ModifyInstance(self, instance, **kwargs):
808 """Modifies an instance.
810 More details for parameters can be found in the RAPI documentation.
812 @type instance: string
813 @param instance: Instance name
820 return self._SendRequest(HTTP_PUT,
821 ("/%s/instances/%s/modify" %
822 (GANETI_RAPI_VERSION, instance)), None, body)
824 def ActivateInstanceDisks(self, instance, ignore_size=None):
825 """Activates an instance's disks.
827 @type instance: string
828 @param instance: Instance name
829 @type ignore_size: bool
830 @param ignore_size: Whether to ignore recorded size
836 _AppendIf(query, ignore_size, ("ignore_size", 1))
838 return self._SendRequest(HTTP_PUT,
839 ("/%s/instances/%s/activate-disks" %
840 (GANETI_RAPI_VERSION, instance)), query, None)
842 def DeactivateInstanceDisks(self, instance):
843 """Deactivates an instance's disks.
845 @type instance: string
846 @param instance: Instance name
851 return self._SendRequest(HTTP_PUT,
852 ("/%s/instances/%s/deactivate-disks" %
853 (GANETI_RAPI_VERSION, instance)), None, None)
855 def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
856 """Recreate an instance's disks.
858 @type instance: string
859 @param instance: Instance name
860 @type disks: list of int
861 @param disks: List of disk indexes
862 @type nodes: list of string
863 @param nodes: New instance nodes, if relocation is desired
869 _SetItemIf(body, disks is not None, "disks", disks)
870 _SetItemIf(body, nodes is not None, "nodes", nodes)
872 return self._SendRequest(HTTP_POST,
873 ("/%s/instances/%s/recreate-disks" %
874 (GANETI_RAPI_VERSION, instance)), None, body)
876 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
877 """Grows a disk of an instance.
879 More details for parameters can be found in the RAPI documentation.
881 @type instance: string
882 @param instance: Instance name
884 @param disk: Disk index
885 @type amount: integer
886 @param amount: Grow disk by this amount (MiB)
887 @type wait_for_sync: bool
888 @param wait_for_sync: Wait for disk to synchronize
897 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
899 return self._SendRequest(HTTP_POST,
900 ("/%s/instances/%s/disk/%s/grow" %
901 (GANETI_RAPI_VERSION, instance, disk)),
904 def GetInstanceTags(self, instance):
905 """Gets tags for an instance.
908 @param instance: instance whose tags to return
911 @return: tags for the instance
914 return self._SendRequest(HTTP_GET,
915 ("/%s/instances/%s/tags" %
916 (GANETI_RAPI_VERSION, instance)), None, None)
918 def AddInstanceTags(self, instance, tags, dry_run=False):
919 """Adds tags to an instance.
922 @param instance: instance to add tags to
923 @type tags: list of str
924 @param tags: tags to add to the instance
926 @param dry_run: whether to perform a dry run
932 query = [("tag", t) for t in tags]
933 _AppendDryRunIf(query, dry_run)
935 return self._SendRequest(HTTP_PUT,
936 ("/%s/instances/%s/tags" %
937 (GANETI_RAPI_VERSION, instance)), query, None)
939 def DeleteInstanceTags(self, instance, tags, dry_run=False):
940 """Deletes tags from an instance.
943 @param instance: instance to delete tags from
944 @type tags: list of str
945 @param tags: tags to delete
947 @param dry_run: whether to perform a dry run
952 query = [("tag", t) for t in tags]
953 _AppendDryRunIf(query, dry_run)
955 return self._SendRequest(HTTP_DELETE,
956 ("/%s/instances/%s/tags" %
957 (GANETI_RAPI_VERSION, instance)), query, None)
959 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
961 """Reboots an instance.
964 @param instance: instance to rebot
965 @type reboot_type: str
966 @param reboot_type: one of: hard, soft, full
967 @type ignore_secondaries: bool
968 @param ignore_secondaries: if True, ignores errors for the secondary node
969 while re-assembling disks (in hard-reboot mode only)
971 @param dry_run: whether to perform a dry run
977 _AppendDryRunIf(query, dry_run)
978 _AppendIf(query, reboot_type, ("type", reboot_type))
979 _AppendIf(query, ignore_secondaries is not None,
980 ("ignore_secondaries", ignore_secondaries))
982 return self._SendRequest(HTTP_POST,
983 ("/%s/instances/%s/reboot" %
984 (GANETI_RAPI_VERSION, instance)), query, None)
986 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
988 """Shuts down an instance.
991 @param instance: the instance to shut down
993 @param dry_run: whether to perform a dry run
994 @type no_remember: bool
995 @param no_remember: if true, will not record the state change
1003 _AppendDryRunIf(query, dry_run)
1004 _AppendIf(query, no_remember, ("no-remember", 1))
1006 return self._SendRequest(HTTP_PUT,
1007 ("/%s/instances/%s/shutdown" %
1008 (GANETI_RAPI_VERSION, instance)), query, body)
1010 def StartupInstance(self, instance, dry_run=False, no_remember=False):
1011 """Starts up an instance.
1014 @param instance: the instance to start up
1016 @param dry_run: whether to perform a dry run
1017 @type no_remember: bool
1018 @param no_remember: if true, will not record the state change
1024 _AppendDryRunIf(query, dry_run)
1025 _AppendIf(query, no_remember, ("no-remember", 1))
1027 return self._SendRequest(HTTP_PUT,
1028 ("/%s/instances/%s/startup" %
1029 (GANETI_RAPI_VERSION, instance)), query, None)
1031 def ReinstallInstance(self, instance, os=None, no_startup=False,
1033 """Reinstalls an instance.
1036 @param instance: The instance to reinstall
1037 @type os: str or None
1038 @param os: The operating system to reinstall. If None, the instance's
1039 current operating system will be installed again
1040 @type no_startup: bool
1041 @param no_startup: Whether to start the instance automatically
1046 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1048 "start": not no_startup,
1050 _SetItemIf(body, os is not None, "os", os)
1051 _SetItemIf(body, osparams is not None, "osparams", osparams)
1052 return self._SendRequest(HTTP_POST,
1053 ("/%s/instances/%s/reinstall" %
1054 (GANETI_RAPI_VERSION, instance)), None, body)
1056 # Use old request format
1058 raise GanetiApiError("Server does not support specifying OS parameters"
1059 " for instance reinstallation")
1062 _AppendIf(query, os, ("os", os))
1063 _AppendIf(query, no_startup, ("nostartup", 1))
1065 return self._SendRequest(HTTP_POST,
1066 ("/%s/instances/%s/reinstall" %
1067 (GANETI_RAPI_VERSION, instance)), query, None)
1069 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1070 remote_node=None, iallocator=None):
1071 """Replaces disks on an instance.
1074 @param instance: instance whose disks to replace
1075 @type disks: list of ints
1076 @param disks: Indexes of disks to replace
1078 @param mode: replacement mode to use (defaults to replace_auto)
1079 @type remote_node: str or None
1080 @param remote_node: new secondary node to use (for use with
1081 replace_new_secondary mode)
1082 @type iallocator: str or None
1083 @param iallocator: instance allocator plugin to use (for use with
1094 # TODO: Convert to body parameters
1096 if disks is not None:
1097 _AppendIf(query, True,
1098 ("disks", ",".join(str(idx) for idx in disks)))
1100 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1101 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1103 return self._SendRequest(HTTP_POST,
1104 ("/%s/instances/%s/replace-disks" %
1105 (GANETI_RAPI_VERSION, instance)), query, None)
1107 def PrepareExport(self, instance, mode):
1108 """Prepares an instance for an export.
1110 @type instance: string
1111 @param instance: Instance name
1113 @param mode: Export mode
1118 query = [("mode", mode)]
1119 return self._SendRequest(HTTP_PUT,
1120 ("/%s/instances/%s/prepare-export" %
1121 (GANETI_RAPI_VERSION, instance)), query, None)
1123 def ExportInstance(self, instance, mode, destination, shutdown=None,
1124 remove_instance=None,
1125 x509_key_name=None, destination_x509_ca=None):
1126 """Exports an instance.
1128 @type instance: string
1129 @param instance: Instance name
1131 @param mode: Export mode
1137 "destination": destination,
1141 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1142 _SetItemIf(body, remove_instance is not None,
1143 "remove_instance", remove_instance)
1144 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1145 _SetItemIf(body, destination_x509_ca is not None,
1146 "destination_x509_ca", destination_x509_ca)
1148 return self._SendRequest(HTTP_PUT,
1149 ("/%s/instances/%s/export" %
1150 (GANETI_RAPI_VERSION, instance)), None, body)
1152 def MigrateInstance(self, instance, mode=None, cleanup=None):
1153 """Migrates an instance.
1155 @type instance: string
1156 @param instance: Instance name
1158 @param mode: Migration mode
1160 @param cleanup: Whether to clean up a previously failed migration
1166 _SetItemIf(body, mode is not None, "mode", mode)
1167 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1169 return self._SendRequest(HTTP_PUT,
1170 ("/%s/instances/%s/migrate" %
1171 (GANETI_RAPI_VERSION, instance)), None, body)
1173 def FailoverInstance(self, instance, iallocator=None,
1174 ignore_consistency=None, target_node=None):
1175 """Does a failover of an instance.
1177 @type instance: string
1178 @param instance: Instance name
1179 @type iallocator: string
1180 @param iallocator: Iallocator for deciding the target node for
1181 shared-storage instances
1182 @type ignore_consistency: bool
1183 @param ignore_consistency: Whether to ignore disk consistency
1184 @type target_node: string
1185 @param target_node: Target node for shared-storage instances
1191 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1192 _SetItemIf(body, ignore_consistency is not None,
1193 "ignore_consistency", ignore_consistency)
1194 _SetItemIf(body, target_node is not None, "target_node", target_node)
1196 return self._SendRequest(HTTP_PUT,
1197 ("/%s/instances/%s/failover" %
1198 (GANETI_RAPI_VERSION, instance)), None, body)
1200 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1201 """Changes the name of an instance.
1203 @type instance: string
1204 @param instance: Instance name
1205 @type new_name: string
1206 @param new_name: New instance name
1207 @type ip_check: bool
1208 @param ip_check: Whether to ensure instance's IP address is inactive
1209 @type name_check: bool
1210 @param name_check: Whether to ensure instance's name is resolvable
1216 "new_name": new_name,
1219 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1220 _SetItemIf(body, name_check is not None, "name_check", name_check)
1222 return self._SendRequest(HTTP_PUT,
1223 ("/%s/instances/%s/rename" %
1224 (GANETI_RAPI_VERSION, instance)), None, body)
1226 def GetInstanceConsole(self, instance):
1227 """Request information for connecting to instance's console.
1229 @type instance: string
1230 @param instance: Instance name
1232 @return: dictionary containing information about instance's console
1235 return self._SendRequest(HTTP_GET,
1236 ("/%s/instances/%s/console" %
1237 (GANETI_RAPI_VERSION, instance)), None, None)
1240 """Gets all jobs for the cluster.
1243 @return: job ids for the cluster
1246 return [int(j["id"])
1247 for j in self._SendRequest(HTTP_GET,
1248 "/%s/jobs" % GANETI_RAPI_VERSION,
1251 def GetJobStatus(self, job_id):
1252 """Gets the status of a job.
1254 @type job_id: string
1255 @param job_id: job id whose status to query
1261 return self._SendRequest(HTTP_GET,
1262 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1265 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1266 """Polls cluster for job status until completion.
1268 Completion is defined as any of the following states listed in
1269 L{JOB_STATUS_FINALIZED}.
1271 @type job_id: string
1272 @param job_id: job id to watch
1274 @param period: how often to poll for status (optional, default 5s)
1276 @param retries: how many time to poll before giving up
1277 (optional, default -1 means unlimited)
1280 @return: C{True} if job succeeded or C{False} if failed/status timeout
1281 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1282 possible; L{WaitForJobChange} returns immediately after a job changed and
1283 does not use polling
1287 job_result = self.GetJobStatus(job_id)
1289 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1291 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1302 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1303 """Waits for job changes.
1305 @type job_id: string
1306 @param job_id: Job ID for which to wait
1307 @return: C{None} if no changes have been detected and a dict with two keys,
1308 C{job_info} and C{log_entries} otherwise.
1314 "previous_job_info": prev_job_info,
1315 "previous_log_serial": prev_log_serial,
1318 return self._SendRequest(HTTP_GET,
1319 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1322 def CancelJob(self, job_id, dry_run=False):
1325 @type job_id: string
1326 @param job_id: id of the job to delete
1328 @param dry_run: whether to perform a dry run
1330 @return: tuple containing the result, and a message (bool, string)
1334 _AppendDryRunIf(query, dry_run)
1336 return self._SendRequest(HTTP_DELETE,
1337 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1340 def GetNodes(self, bulk=False):
1341 """Gets all nodes in the cluster.
1344 @param bulk: whether to return all information about all instances
1346 @rtype: list of dict or str
1347 @return: if bulk is true, info about nodes in the cluster,
1348 else list of nodes in the cluster
1352 _AppendIf(query, bulk, ("bulk", 1))
1354 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1359 return [n["id"] for n in nodes]
1361 def GetNode(self, node):
1362 """Gets information about a node.
1365 @param node: node whose info to return
1368 @return: info about the node
1371 return self._SendRequest(HTTP_GET,
1372 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1375 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1376 dry_run=False, early_release=None,
1377 mode=None, accept_old=False):
1378 """Evacuates instances from a Ganeti node.
1381 @param node: node to evacuate
1382 @type iallocator: str or None
1383 @param iallocator: instance allocator to use
1384 @type remote_node: str
1385 @param remote_node: node to evaucate to
1387 @param dry_run: whether to perform a dry run
1388 @type early_release: bool
1389 @param early_release: whether to enable parallelization
1391 @param mode: Node evacuation mode
1392 @type accept_old: bool
1393 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1396 @rtype: string, or a list for pre-2.5 results
1397 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1398 list of (job ID, instance name, new secondary node); if dry_run was
1399 specified, then the actual move jobs were not submitted and the job IDs
1402 @raises GanetiApiError: if an iallocator and remote_node are both
1406 if iallocator and remote_node:
1407 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1410 _AppendDryRunIf(query, dry_run)
1412 if _NODE_EVAC_RES1 in self.GetFeatures():
1413 # Server supports body parameters
1416 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1417 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1418 _SetItemIf(body, early_release is not None,
1419 "early_release", early_release)
1420 _SetItemIf(body, mode is not None, "mode", mode)
1422 # Pre-2.5 request format
1426 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1427 " not accept old-style results (parameter"
1430 # Pre-2.5 servers can only evacuate secondaries
1431 if mode is not None and mode != NODE_EVAC_SEC:
1432 raise GanetiApiError("Server can only evacuate secondary instances")
1434 _AppendIf(query, iallocator, ("iallocator", iallocator))
1435 _AppendIf(query, remote_node, ("remote_node", remote_node))
1436 _AppendIf(query, early_release, ("early_release", 1))
1438 return self._SendRequest(HTTP_POST,
1439 ("/%s/nodes/%s/evacuate" %
1440 (GANETI_RAPI_VERSION, node)), query, body)
1442 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1444 """Migrates all primary instances from a node.
1447 @param node: node to migrate
1449 @param mode: if passed, it will overwrite the live migration type,
1450 otherwise the hypervisor default will be used
1452 @param dry_run: whether to perform a dry run
1453 @type iallocator: string
1454 @param iallocator: instance allocator to use
1455 @type target_node: string
1456 @param target_node: Target node for shared-storage instances
1463 _AppendDryRunIf(query, dry_run)
1465 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1468 _SetItemIf(body, mode is not None, "mode", mode)
1469 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1470 _SetItemIf(body, target_node is not None, "target_node", target_node)
1472 assert len(query) <= 1
1474 return self._SendRequest(HTTP_POST,
1475 ("/%s/nodes/%s/migrate" %
1476 (GANETI_RAPI_VERSION, node)), query, body)
1478 # Use old request format
1479 if target_node is not None:
1480 raise GanetiApiError("Server does not support specifying target node"
1481 " for node migration")
1483 _AppendIf(query, mode is not None, ("mode", mode))
1485 return self._SendRequest(HTTP_POST,
1486 ("/%s/nodes/%s/migrate" %
1487 (GANETI_RAPI_VERSION, node)), query, None)
1489 def GetNodeRole(self, node):
1490 """Gets the current role for a node.
1493 @param node: node whose role to return
1496 @return: the current role for a node
1499 return self._SendRequest(HTTP_GET,
1500 ("/%s/nodes/%s/role" %
1501 (GANETI_RAPI_VERSION, node)), None, None)
1503 def SetNodeRole(self, node, role, force=False, auto_promote=None):
1504 """Sets the role for a node.
1507 @param node: the node whose role to set
1509 @param role: the role to set for the node
1511 @param force: whether to force the role change
1512 @type auto_promote: bool
1513 @param auto_promote: Whether node(s) should be promoted to master candidate
1521 _AppendForceIf(query, force)
1522 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1524 return self._SendRequest(HTTP_PUT,
1525 ("/%s/nodes/%s/role" %
1526 (GANETI_RAPI_VERSION, node)), query, role)
1528 def PowercycleNode(self, node, force=False):
1529 """Powercycles a node.
1532 @param node: Node name
1534 @param force: Whether to force the operation
1540 _AppendForceIf(query, force)
1542 return self._SendRequest(HTTP_POST,
1543 ("/%s/nodes/%s/powercycle" %
1544 (GANETI_RAPI_VERSION, node)), query, None)
1546 def ModifyNode(self, node, **kwargs):
1549 More details for parameters can be found in the RAPI documentation.
1552 @param node: Node name
1557 return self._SendRequest(HTTP_POST,
1558 ("/%s/nodes/%s/modify" %
1559 (GANETI_RAPI_VERSION, node)), None, kwargs)
1561 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1562 """Gets the storage units for a node.
1565 @param node: the node whose storage units to return
1566 @type storage_type: str
1567 @param storage_type: storage type whose units to return
1568 @type output_fields: str
1569 @param output_fields: storage type fields to return
1572 @return: job id where results can be retrieved
1576 ("storage_type", storage_type),
1577 ("output_fields", output_fields),
1580 return self._SendRequest(HTTP_GET,
1581 ("/%s/nodes/%s/storage" %
1582 (GANETI_RAPI_VERSION, node)), query, None)
1584 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1585 """Modifies parameters of storage units on the node.
1588 @param node: node whose storage units to modify
1589 @type storage_type: str
1590 @param storage_type: storage type whose units to modify
1592 @param name: name of the storage unit
1593 @type allocatable: bool or None
1594 @param allocatable: Whether to set the "allocatable" flag on the storage
1595 unit (None=no modification, True=set, False=unset)
1602 ("storage_type", storage_type),
1606 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1608 return self._SendRequest(HTTP_PUT,
1609 ("/%s/nodes/%s/storage/modify" %
1610 (GANETI_RAPI_VERSION, node)), query, None)
1612 def RepairNodeStorageUnits(self, node, storage_type, name):
1613 """Repairs a storage unit on the node.
1616 @param node: node whose storage units to repair
1617 @type storage_type: str
1618 @param storage_type: storage type to repair
1620 @param name: name of the storage unit to repair
1627 ("storage_type", storage_type),
1631 return self._SendRequest(HTTP_PUT,
1632 ("/%s/nodes/%s/storage/repair" %
1633 (GANETI_RAPI_VERSION, node)), query, None)
1635 def GetNodeTags(self, node):
1636 """Gets the tags for a node.
1639 @param node: node whose tags to return
1642 @return: tags for the node
1645 return self._SendRequest(HTTP_GET,
1646 ("/%s/nodes/%s/tags" %
1647 (GANETI_RAPI_VERSION, node)), None, None)
1649 def AddNodeTags(self, node, tags, dry_run=False):
1650 """Adds tags to a node.
1653 @param node: node to add tags to
1654 @type tags: list of str
1655 @param tags: tags to add to the node
1657 @param dry_run: whether to perform a dry run
1663 query = [("tag", t) for t in tags]
1664 _AppendDryRunIf(query, dry_run)
1666 return self._SendRequest(HTTP_PUT,
1667 ("/%s/nodes/%s/tags" %
1668 (GANETI_RAPI_VERSION, node)), query, tags)
1670 def DeleteNodeTags(self, node, tags, dry_run=False):
1671 """Delete tags from a node.
1674 @param node: node to remove tags from
1675 @type tags: list of str
1676 @param tags: tags to remove from the node
1678 @param dry_run: whether to perform a dry run
1684 query = [("tag", t) for t in tags]
1685 _AppendDryRunIf(query, dry_run)
1687 return self._SendRequest(HTTP_DELETE,
1688 ("/%s/nodes/%s/tags" %
1689 (GANETI_RAPI_VERSION, node)), query, None)
1691 def GetNetworks(self, bulk=False):
1692 """Gets all networks in the cluster.
1695 @param bulk: whether to return all information about the networks
1697 @rtype: list of dict or str
1698 @return: if bulk is true, a list of dictionaries with info about all
1699 networks in the cluster, else a list of names of those networks
1703 _AppendIf(query, bulk, ("bulk", 1))
1705 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1710 return [n["name"] for n in networks]
1712 def GetNetwork(self, network):
1713 """Gets information about a network.
1716 @param group: name of the network whose info to return
1719 @return: info about the network
1722 return self._SendRequest(HTTP_GET,
1723 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1726 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1727 gateway6=None, mac_prefix=None, network_type=None,
1728 add_reserved_ips=None, tags=None, dry_run=False):
1729 """Creates a new network.
1732 @param name: the name of network to create
1734 @param dry_run: whether to peform a dry run
1741 _AppendDryRunIf(query, dry_run)
1743 if add_reserved_ips:
1744 add_reserved_ips = add_reserved_ips.split(',')
1747 tags = tags.split(',')
1750 "network_name": network_name,
1753 "gateway6": gateway6,
1754 "network6": network6,
1755 "mac_prefix": mac_prefix,
1756 "network_type": network_type,
1757 "add_reserved_ips": add_reserved_ips,
1761 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1764 def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
1765 """Connects a Network to a NodeGroup with the given netparams
1769 "group_name": group_name,
1770 "network_mode": mode,
1771 "network_link": link,
1775 _AppendDryRunIf(query, dry_run)
1777 return self._SendRequest(HTTP_PUT,
1778 ("/%s/networks/%s/connect" %
1779 (GANETI_RAPI_VERSION, network_name)), query, body)
1781 def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1782 """Connects a Network to a NodeGroup with the given netparams
1786 "group_name": group_name,
1790 _AppendDryRunIf(query, dry_run)
1792 return self._SendRequest(HTTP_PUT,
1793 ("/%s/networks/%s/disconnect" %
1794 (GANETI_RAPI_VERSION, network_name)), query, body)
1796 def ModifyNetwork(self, network, **kwargs):
1797 """Modifies a network.
1799 More details for parameters can be found in the RAPI documentation.
1801 @type network: string
1802 @param network: Network name
1807 return self._SendRequest(HTTP_PUT,
1808 ("/%s/networks/%s/modify" %
1809 (GANETI_RAPI_VERSION, network)), None, kwargs)
1811 def DeleteNetwork(self, network, dry_run=False):
1812 """Deletes a network.
1815 @param group: the network to delete
1817 @param dry_run: whether to peform a dry run
1824 _AppendDryRunIf(query, dry_run)
1826 return self._SendRequest(HTTP_DELETE,
1827 ("/%s/networks/%s" %
1828 (GANETI_RAPI_VERSION, network)), query, None)
1830 def GetNetworkTags(self, network):
1831 """Gets tags for a network.
1833 @type network: string
1834 @param network: Node group whose tags to return
1836 @rtype: list of strings
1837 @return: tags for the network
1840 return self._SendRequest(HTTP_GET,
1841 ("/%s/networks/%s/tags" %
1842 (GANETI_RAPI_VERSION, network)), None, None)
1844 def AddNetworkTags(self, network, tags, dry_run=False):
1845 """Adds tags to a network.
1848 @param network: network to add tags to
1849 @type tags: list of string
1850 @param tags: tags to add to the network
1852 @param dry_run: whether to perform a dry run
1858 query = [("tag", t) for t in tags]
1859 _AppendDryRunIf(query, dry_run)
1861 return self._SendRequest(HTTP_PUT,
1862 ("/%s/networks/%s/tags" %
1863 (GANETI_RAPI_VERSION, network)), query, None)
1865 def DeleteNetworkTags(self, network, tags, dry_run=False):
1866 """Deletes tags from a network.
1869 @param network: network to delete tags from
1870 @type tags: list of string
1871 @param tags: tags to delete
1873 @param dry_run: whether to perform a dry run
1878 query = [("tag", t) for t in tags]
1879 _AppendDryRunIf(query, dry_run)
1881 return self._SendRequest(HTTP_DELETE,
1882 ("/%s/networks/%s/tags" %
1883 (GANETI_RAPI_VERSION, network)), query, None)
1885 def GetGroups(self, bulk=False):
1886 """Gets all node groups in the cluster.
1889 @param bulk: whether to return all information about the groups
1891 @rtype: list of dict or str
1892 @return: if bulk is true, a list of dictionaries with info about all node
1893 groups in the cluster, else a list of names of those node groups
1897 _AppendIf(query, bulk, ("bulk", 1))
1899 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1904 return [g["name"] for g in groups]
1906 def GetGroup(self, group):
1907 """Gets information about a node group.
1910 @param group: name of the node group whose info to return
1913 @return: info about the node group
1916 return self._SendRequest(HTTP_GET,
1917 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1920 def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1921 """Creates a new node group.
1924 @param name: the name of node group to create
1925 @type alloc_policy: str
1926 @param alloc_policy: the desired allocation policy for the group, if any
1928 @param dry_run: whether to peform a dry run
1935 _AppendDryRunIf(query, dry_run)
1939 "alloc_policy": alloc_policy,
1942 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1945 def ModifyGroup(self, group, **kwargs):
1946 """Modifies a node group.
1948 More details for parameters can be found in the RAPI documentation.
1951 @param group: Node group name
1956 return self._SendRequest(HTTP_PUT,
1957 ("/%s/groups/%s/modify" %
1958 (GANETI_RAPI_VERSION, group)), None, kwargs)
1960 def DeleteGroup(self, group, dry_run=False):
1961 """Deletes a node group.
1964 @param group: the node group to delete
1966 @param dry_run: whether to peform a dry run
1973 _AppendDryRunIf(query, dry_run)
1975 return self._SendRequest(HTTP_DELETE,
1977 (GANETI_RAPI_VERSION, group)), query, None)
1979 def RenameGroup(self, group, new_name):
1980 """Changes the name of a node group.
1983 @param group: Node group name
1984 @type new_name: string
1985 @param new_name: New node group name
1992 "new_name": new_name,
1995 return self._SendRequest(HTTP_PUT,
1996 ("/%s/groups/%s/rename" %
1997 (GANETI_RAPI_VERSION, group)), None, body)
1999 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2000 """Assigns nodes to a group.
2003 @param group: Node group name
2004 @type nodes: list of strings
2005 @param nodes: List of nodes to assign to the group
2012 _AppendForceIf(query, force)
2013 _AppendDryRunIf(query, dry_run)
2019 return self._SendRequest(HTTP_PUT,
2020 ("/%s/groups/%s/assign-nodes" %
2021 (GANETI_RAPI_VERSION, group)), query, body)
2023 def GetGroupTags(self, group):
2024 """Gets tags for a node group.
2027 @param group: Node group whose tags to return
2029 @rtype: list of strings
2030 @return: tags for the group
2033 return self._SendRequest(HTTP_GET,
2034 ("/%s/groups/%s/tags" %
2035 (GANETI_RAPI_VERSION, group)), None, None)
2037 def AddGroupTags(self, group, tags, dry_run=False):
2038 """Adds tags to a node group.
2041 @param group: group to add tags to
2042 @type tags: list of string
2043 @param tags: tags to add to the group
2045 @param dry_run: whether to perform a dry run
2051 query = [("tag", t) for t in tags]
2052 _AppendDryRunIf(query, dry_run)
2054 return self._SendRequest(HTTP_PUT,
2055 ("/%s/groups/%s/tags" %
2056 (GANETI_RAPI_VERSION, group)), query, None)
2058 def DeleteGroupTags(self, group, tags, dry_run=False):
2059 """Deletes tags from a node group.
2062 @param group: group to delete tags from
2063 @type tags: list of string
2064 @param tags: tags to delete
2066 @param dry_run: whether to perform a dry run
2071 query = [("tag", t) for t in tags]
2072 _AppendDryRunIf(query, dry_run)
2074 return self._SendRequest(HTTP_DELETE,
2075 ("/%s/groups/%s/tags" %
2076 (GANETI_RAPI_VERSION, group)), query, None)
2078 def Query(self, what, fields, qfilter=None):
2079 """Retrieves information about resources.
2082 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2083 @type fields: list of string
2084 @param fields: Requested fields
2085 @type qfilter: None or list
2086 @param qfilter: Query filter
2096 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2097 # TODO: remove "filter" after 2.7
2098 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2100 return self._SendRequest(HTTP_PUT,
2102 (GANETI_RAPI_VERSION, what)), None, body)
2104 def QueryFields(self, what, fields=None):
2105 """Retrieves available fields for a resource.
2108 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2109 @type fields: list of string
2110 @param fields: Requested fields
2118 if fields is not None:
2119 _AppendIf(query, True, ("fields", ",".join(fields)))
2121 return self._SendRequest(HTTP_GET,
2122 ("/%s/query/%s/fields" %
2123 (GANETI_RAPI_VERSION, what)), query, None)