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 _SetItemIf(container, condition, item, value):
223 """Sets an item if a condition evaluates to truth.
227 container[item] = value
232 def UsesRapiClient(fn):
233 """Decorator for code using RAPI client to initialize pycURL.
236 def wrapper(*args, **kwargs):
237 # curl_global_init(3) and curl_global_cleanup(3) must be called with only
238 # one thread running. This check is just a safety measure -- it doesn't
240 assert threading.activeCount() == 1, \
241 "Found active threads when initializing pycURL"
243 pycurl.global_init(pycurl.GLOBAL_ALL)
245 return fn(*args, **kwargs)
247 pycurl.global_cleanup()
252 def GenericCurlConfig(verbose=False, use_signal=False,
253 use_curl_cabundle=False, cafile=None, capath=None,
254 proxy=None, verify_hostname=False,
255 connect_timeout=None, timeout=None,
256 _pycurl_version_fn=pycurl.version_info):
257 """Curl configuration function generator.
260 @param verbose: Whether to set cURL to verbose mode
261 @type use_signal: bool
262 @param use_signal: Whether to allow cURL to use signals
263 @type use_curl_cabundle: bool
264 @param use_curl_cabundle: Whether to use cURL's default CA bundle
266 @param cafile: In which file we can find the certificates
268 @param capath: In which directory we can find the certificates
270 @param proxy: Proxy to use, None for default behaviour and empty string for
271 disabling proxies (see curl_easy_setopt(3))
272 @type verify_hostname: bool
273 @param verify_hostname: Whether to verify the remote peer certificate's
275 @type connect_timeout: number
276 @param connect_timeout: Timeout for establishing connection in seconds
277 @type timeout: number
278 @param timeout: Timeout for complete transfer in seconds (see
279 curl_easy_setopt(3)).
282 if use_curl_cabundle and (cafile or capath):
283 raise Error("Can not use default CA bundle when CA file or path is set")
285 def _ConfigCurl(curl, logger):
286 """Configures a cURL object
288 @type curl: pycurl.Curl
289 @param curl: cURL object
292 logger.debug("Using cURL version %s", pycurl.version)
294 # pycurl.version_info returns a tuple with information about the used
295 # version of libcurl. Item 5 is the SSL library linked to it.
296 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
298 sslver = _pycurl_version_fn()[5]
300 raise Error("No SSL support in cURL")
302 lcsslver = sslver.lower()
303 if lcsslver.startswith("openssl/"):
305 elif lcsslver.startswith("nss/"):
306 # TODO: investigate compatibility beyond a simple test
308 elif lcsslver.startswith("gnutls/"):
310 raise Error("cURL linked against GnuTLS has no support for a"
311 " CA path (%s)" % (pycurl.version, ))
313 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
316 curl.setopt(pycurl.VERBOSE, verbose)
317 curl.setopt(pycurl.NOSIGNAL, not use_signal)
319 # Whether to verify remote peer's CN
321 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
322 # certificate must indicate that the server is the server to which you
323 # meant to connect, or the connection fails. [...] When the value is 1,
324 # the certificate must contain a Common Name field, but it doesn't matter
325 # what name it says. [...]"
326 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
328 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
330 if cafile or capath or use_curl_cabundle:
331 # Require certificates to be checked
332 curl.setopt(pycurl.SSL_VERIFYPEER, True)
334 curl.setopt(pycurl.CAINFO, str(cafile))
336 curl.setopt(pycurl.CAPATH, str(capath))
337 # Not changing anything for using default CA bundle
339 # Disable SSL certificate verification
340 curl.setopt(pycurl.SSL_VERIFYPEER, False)
342 if proxy is not None:
343 curl.setopt(pycurl.PROXY, str(proxy))
346 if connect_timeout is not None:
347 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
348 if timeout is not None:
349 curl.setopt(pycurl.TIMEOUT, timeout)
354 class GanetiRapiClient(object): # pylint: disable=R0904
355 """Ganeti RAPI client.
358 USER_AGENT = "Ganeti RAPI Client"
359 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
361 def __init__(self, host, port=GANETI_RAPI_PORT,
362 username=None, password=None, logger=logging,
363 curl_config_fn=None, curl_factory=None):
364 """Initializes this class.
367 @param host: the ganeti cluster master to interact with
369 @param port: the port on which the RAPI is running (default is 5080)
370 @type username: string
371 @param username: the username to connect with
372 @type password: string
373 @param password: the password to connect with
374 @type curl_config_fn: callable
375 @param curl_config_fn: Function to configure C{pycurl.Curl} object
376 @param logger: Logging object
379 self._username = username
380 self._password = password
381 self._logger = logger
382 self._curl_config_fn = curl_config_fn
383 self._curl_factory = curl_factory
386 socket.inet_pton(socket.AF_INET6, host)
387 address = "[%s]:%s" % (host, port)
389 address = "%s:%s" % (host, port)
391 self._base_url = "https://%s" % address
393 if username is not None:
395 raise Error("Password not specified")
397 raise Error("Specified password without username")
399 def _CreateCurl(self):
400 """Creates a cURL object.
403 # Create pycURL object if no factory is provided
404 if self._curl_factory:
405 curl = self._curl_factory()
409 # Default cURL settings
410 curl.setopt(pycurl.VERBOSE, False)
411 curl.setopt(pycurl.FOLLOWLOCATION, False)
412 curl.setopt(pycurl.MAXREDIRS, 5)
413 curl.setopt(pycurl.NOSIGNAL, True)
414 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
415 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
416 curl.setopt(pycurl.SSL_VERIFYPEER, False)
417 curl.setopt(pycurl.HTTPHEADER, [
418 "Accept: %s" % HTTP_APP_JSON,
419 "Content-type: %s" % HTTP_APP_JSON,
422 assert ((self._username is None and self._password is None) ^
423 (self._username is not None and self._password is not None))
426 # Setup authentication
427 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
428 curl.setopt(pycurl.USERPWD,
429 str("%s:%s" % (self._username, self._password)))
431 # Call external configuration function
432 if self._curl_config_fn:
433 self._curl_config_fn(curl, self._logger)
438 def _EncodeQuery(query):
439 """Encode query values for RAPI URL.
441 @type query: list of two-tuples
442 @param query: Query arguments
444 @return: Query list with encoded values
449 for name, value in query:
451 result.append((name, ""))
453 elif isinstance(value, bool):
454 # Boolean values must be encoded as 0 or 1
455 result.append((name, int(value)))
457 elif isinstance(value, (list, tuple, dict)):
458 raise ValueError("Invalid query data type %r" % type(value).__name__)
461 result.append((name, value))
465 def _SendRequest(self, method, path, query, content):
466 """Sends an HTTP request.
468 This constructs a full URL, encodes and decodes HTTP bodies, and
469 handles invalid responses in a pythonic way.
472 @param method: HTTP method to use
474 @param path: HTTP URL path
475 @type query: list of two-tuples
476 @param query: query arguments to pass to urllib.urlencode
477 @type content: str or None
478 @param content: HTTP body content
481 @return: JSON-Decoded response
483 @raises CertificateError: If an invalid SSL certificate is found
484 @raises GanetiApiError: If an invalid response is returned
487 assert path.startswith("/")
489 curl = self._CreateCurl()
491 if content is not None:
492 encoded_content = self._json_encoder.encode(content)
497 urlparts = [self._base_url, path]
500 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
502 url = "".join(urlparts)
504 self._logger.debug("Sending request %s %s (content=%r)",
505 method, url, encoded_content)
507 # Buffer for response
508 encoded_resp_body = StringIO()
511 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
512 curl.setopt(pycurl.URL, str(url))
513 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
514 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
517 # Send request and wait for response
520 except pycurl.error, err:
521 if err.args[0] in _CURL_SSL_CERT_ERRORS:
522 raise CertificateError("SSL certificate error %s" % err,
525 raise GanetiApiError(str(err), code=err.args[0])
527 # Reset settings to not keep references to large objects in memory
529 curl.setopt(pycurl.POSTFIELDS, "")
530 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
532 # Get HTTP response code
533 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
535 # Was anything written to the response buffer?
536 if encoded_resp_body.tell():
537 response_content = simplejson.loads(encoded_resp_body.getvalue())
539 response_content = None
541 if http_code != HTTP_OK:
542 if isinstance(response_content, dict):
544 (response_content["code"],
545 response_content["message"],
546 response_content["explain"]))
548 msg = str(response_content)
550 raise GanetiApiError(msg, code=http_code)
552 return response_content
554 def GetVersion(self):
555 """Gets the Remote API version running on the cluster.
558 @return: Ganeti Remote API version
561 return self._SendRequest(HTTP_GET, "/version", None, None)
563 def GetFeatures(self):
564 """Gets the list of optional features supported by RAPI server.
567 @return: List of optional features
571 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
573 except GanetiApiError, err:
574 # Older RAPI servers don't support this resource
575 if err.code == HTTP_NOT_FOUND:
580 def GetOperatingSystems(self):
581 """Gets the Operating Systems running in the Ganeti cluster.
584 @return: operating systems
587 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
591 """Gets info about the cluster.
594 @return: information about the cluster
597 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
600 def RedistributeConfig(self):
601 """Tells the cluster to redistribute its configuration files.
607 return self._SendRequest(HTTP_PUT,
608 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
611 def ModifyCluster(self, **kwargs):
612 """Modifies cluster parameters.
614 More details for parameters can be found in the RAPI documentation.
622 return self._SendRequest(HTTP_PUT,
623 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
625 def GetClusterTags(self):
626 """Gets the cluster tags.
629 @return: cluster tags
632 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
635 def AddClusterTags(self, tags, dry_run=False):
636 """Adds tags to the cluster.
638 @type tags: list of str
639 @param tags: tags to add to the cluster
641 @param dry_run: whether to perform a dry run
647 query = [("tag", t) for t in tags]
648 _AppendDryRunIf(query, dry_run)
650 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
653 def DeleteClusterTags(self, tags, dry_run=False):
654 """Deletes tags from the cluster.
656 @type tags: list of str
657 @param tags: tags to delete
659 @param dry_run: whether to perform a dry run
664 query = [("tag", t) for t in tags]
665 _AppendDryRunIf(query, dry_run)
667 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
670 def GetInstances(self, bulk=False):
671 """Gets information about instances on the cluster.
674 @param bulk: whether to return all information about all instances
676 @rtype: list of dict or list of str
677 @return: if bulk is True, info about the instances, else a list of instances
681 _AppendIf(query, bulk, ("bulk", 1))
683 instances = self._SendRequest(HTTP_GET,
684 "/%s/instances" % GANETI_RAPI_VERSION,
689 return [i["id"] for i in instances]
691 def GetInstance(self, instance):
692 """Gets information about an instance.
695 @param instance: instance whose info to return
698 @return: info about the instance
701 return self._SendRequest(HTTP_GET,
702 ("/%s/instances/%s" %
703 (GANETI_RAPI_VERSION, instance)), None, None)
705 def GetInstanceInfo(self, instance, static=None):
706 """Gets information about an instance.
708 @type instance: string
709 @param instance: Instance name
714 if static is not None:
715 query = [("static", static)]
719 return self._SendRequest(HTTP_GET,
720 ("/%s/instances/%s/info" %
721 (GANETI_RAPI_VERSION, instance)), query, None)
724 def _UpdateWithKwargs(base, **kwargs):
725 """Updates the base with params from kwargs.
727 @param base: The base dict, filled with required fields
729 @note: This is an inplace update of base
732 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
734 raise GanetiApiError("Required fields can not be specified as"
735 " keywords: %s" % ", ".join(conflicts))
737 base.update((key, value) for key, value in kwargs.iteritems()
740 def InstanceAllocation(self, mode, name, disk_template, disks, nics,
742 """Generates an instance allocation as used by multiallocate.
744 More details for parameters can be found in the RAPI documentation.
745 It is the same as used by CreateInstance.
748 @param mode: Instance creation mode
750 @param name: Hostname of the instance to create
751 @type disk_template: string
752 @param disk_template: Disk template for instance (e.g. plain, diskless,
754 @type disks: list of dicts
755 @param disks: List of disk definitions
756 @type nics: list of dicts
757 @param nics: List of NIC definitions
759 @return: A dict with the generated entry
762 # All required fields for request data version 1
766 "disk_template": disk_template,
771 self._UpdateWithKwargs(alloc, **kwargs)
775 def InstancesMultiAlloc(self, instances, **kwargs):
776 """Tries to allocate multiple instances.
778 More details for parameters can be found in the RAPI documentation.
780 @param instances: A list of L{InstanceAllocation} results
785 "instances": instances,
787 self._UpdateWithKwargs(body, **kwargs)
789 _AppendDryRunIf(query, kwargs.get("dry_run"))
791 return self._SendRequest(HTTP_POST,
792 "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
795 def CreateInstance(self, mode, name, disk_template, disks, nics,
797 """Creates a new instance.
799 More details for parameters can be found in the RAPI documentation.
802 @param mode: Instance creation mode
804 @param name: Hostname of the instance to create
805 @type disk_template: string
806 @param disk_template: Disk template for instance (e.g. plain, diskless,
808 @type disks: list of dicts
809 @param disks: List of disk definitions
810 @type nics: list of dicts
811 @param nics: List of NIC definitions
813 @keyword dry_run: whether to perform a dry run
821 _AppendDryRunIf(query, kwargs.get("dry_run"))
823 if _INST_CREATE_REQV1 in self.GetFeatures():
824 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
826 body[_REQ_DATA_VERSION_FIELD] = 1
828 raise GanetiApiError("Server does not support new-style (version 1)"
829 " instance creation requests")
831 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
834 def DeleteInstance(self, instance, dry_run=False):
835 """Deletes an instance.
838 @param instance: the instance to delete
845 _AppendDryRunIf(query, dry_run)
847 return self._SendRequest(HTTP_DELETE,
848 ("/%s/instances/%s" %
849 (GANETI_RAPI_VERSION, instance)), query, None)
851 def ModifyInstance(self, instance, **kwargs):
852 """Modifies an instance.
854 More details for parameters can be found in the RAPI documentation.
856 @type instance: string
857 @param instance: Instance name
864 return self._SendRequest(HTTP_PUT,
865 ("/%s/instances/%s/modify" %
866 (GANETI_RAPI_VERSION, instance)), None, body)
868 def ActivateInstanceDisks(self, instance, ignore_size=None):
869 """Activates an instance's disks.
871 @type instance: string
872 @param instance: Instance name
873 @type ignore_size: bool
874 @param ignore_size: Whether to ignore recorded size
880 _AppendIf(query, ignore_size, ("ignore_size", 1))
882 return self._SendRequest(HTTP_PUT,
883 ("/%s/instances/%s/activate-disks" %
884 (GANETI_RAPI_VERSION, instance)), query, None)
886 def DeactivateInstanceDisks(self, instance):
887 """Deactivates an instance's disks.
889 @type instance: string
890 @param instance: Instance name
895 return self._SendRequest(HTTP_PUT,
896 ("/%s/instances/%s/deactivate-disks" %
897 (GANETI_RAPI_VERSION, instance)), None, None)
899 def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
900 """Recreate an instance's disks.
902 @type instance: string
903 @param instance: Instance name
904 @type disks: list of int
905 @param disks: List of disk indexes
906 @type nodes: list of string
907 @param nodes: New instance nodes, if relocation is desired
913 _SetItemIf(body, disks is not None, "disks", disks)
914 _SetItemIf(body, nodes is not None, "nodes", nodes)
916 return self._SendRequest(HTTP_POST,
917 ("/%s/instances/%s/recreate-disks" %
918 (GANETI_RAPI_VERSION, instance)), None, body)
920 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
921 """Grows a disk of an instance.
923 More details for parameters can be found in the RAPI documentation.
925 @type instance: string
926 @param instance: Instance name
928 @param disk: Disk index
929 @type amount: integer
930 @param amount: Grow disk by this amount (MiB)
931 @type wait_for_sync: bool
932 @param wait_for_sync: Wait for disk to synchronize
941 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
943 return self._SendRequest(HTTP_POST,
944 ("/%s/instances/%s/disk/%s/grow" %
945 (GANETI_RAPI_VERSION, instance, disk)),
948 def GetInstanceTags(self, instance):
949 """Gets tags for an instance.
952 @param instance: instance whose tags to return
955 @return: tags for the instance
958 return self._SendRequest(HTTP_GET,
959 ("/%s/instances/%s/tags" %
960 (GANETI_RAPI_VERSION, instance)), None, None)
962 def AddInstanceTags(self, instance, tags, dry_run=False):
963 """Adds tags to an instance.
966 @param instance: instance to add tags to
967 @type tags: list of str
968 @param tags: tags to add to the instance
970 @param dry_run: whether to perform a dry run
976 query = [("tag", t) for t in tags]
977 _AppendDryRunIf(query, dry_run)
979 return self._SendRequest(HTTP_PUT,
980 ("/%s/instances/%s/tags" %
981 (GANETI_RAPI_VERSION, instance)), query, None)
983 def DeleteInstanceTags(self, instance, tags, dry_run=False):
984 """Deletes tags from an instance.
987 @param instance: instance to delete tags from
988 @type tags: list of str
989 @param tags: tags to delete
991 @param dry_run: whether to perform a dry run
996 query = [("tag", t) for t in tags]
997 _AppendDryRunIf(query, dry_run)
999 return self._SendRequest(HTTP_DELETE,
1000 ("/%s/instances/%s/tags" %
1001 (GANETI_RAPI_VERSION, instance)), query, None)
1003 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1005 """Reboots an instance.
1008 @param instance: instance to rebot
1009 @type reboot_type: str
1010 @param reboot_type: one of: hard, soft, full
1011 @type ignore_secondaries: bool
1012 @param ignore_secondaries: if True, ignores errors for the secondary node
1013 while re-assembling disks (in hard-reboot mode only)
1015 @param dry_run: whether to perform a dry run
1021 _AppendDryRunIf(query, dry_run)
1022 _AppendIf(query, reboot_type, ("type", reboot_type))
1023 _AppendIf(query, ignore_secondaries is not None,
1024 ("ignore_secondaries", ignore_secondaries))
1026 return self._SendRequest(HTTP_POST,
1027 ("/%s/instances/%s/reboot" %
1028 (GANETI_RAPI_VERSION, instance)), query, None)
1030 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1032 """Shuts down an instance.
1035 @param instance: the instance to shut down
1037 @param dry_run: whether to perform a dry run
1038 @type no_remember: bool
1039 @param no_remember: if true, will not record the state change
1047 _AppendDryRunIf(query, dry_run)
1048 _AppendIf(query, no_remember, ("no_remember", 1))
1050 return self._SendRequest(HTTP_PUT,
1051 ("/%s/instances/%s/shutdown" %
1052 (GANETI_RAPI_VERSION, instance)), query, body)
1054 def StartupInstance(self, instance, dry_run=False, no_remember=False):
1055 """Starts up an instance.
1058 @param instance: the instance to start up
1060 @param dry_run: whether to perform a dry run
1061 @type no_remember: bool
1062 @param no_remember: if true, will not record the state change
1068 _AppendDryRunIf(query, dry_run)
1069 _AppendIf(query, no_remember, ("no_remember", 1))
1071 return self._SendRequest(HTTP_PUT,
1072 ("/%s/instances/%s/startup" %
1073 (GANETI_RAPI_VERSION, instance)), query, None)
1075 def ReinstallInstance(self, instance, os=None, no_startup=False,
1077 """Reinstalls an instance.
1080 @param instance: The instance to reinstall
1081 @type os: str or None
1082 @param os: The operating system to reinstall. If None, the instance's
1083 current operating system will be installed again
1084 @type no_startup: bool
1085 @param no_startup: Whether to start the instance automatically
1090 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1092 "start": not no_startup,
1094 _SetItemIf(body, os is not None, "os", os)
1095 _SetItemIf(body, osparams is not None, "osparams", osparams)
1096 return self._SendRequest(HTTP_POST,
1097 ("/%s/instances/%s/reinstall" %
1098 (GANETI_RAPI_VERSION, instance)), None, body)
1100 # Use old request format
1102 raise GanetiApiError("Server does not support specifying OS parameters"
1103 " for instance reinstallation")
1106 _AppendIf(query, os, ("os", os))
1107 _AppendIf(query, no_startup, ("nostartup", 1))
1109 return self._SendRequest(HTTP_POST,
1110 ("/%s/instances/%s/reinstall" %
1111 (GANETI_RAPI_VERSION, instance)), query, None)
1113 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1114 remote_node=None, iallocator=None):
1115 """Replaces disks on an instance.
1118 @param instance: instance whose disks to replace
1119 @type disks: list of ints
1120 @param disks: Indexes of disks to replace
1122 @param mode: replacement mode to use (defaults to replace_auto)
1123 @type remote_node: str or None
1124 @param remote_node: new secondary node to use (for use with
1125 replace_new_secondary mode)
1126 @type iallocator: str or None
1127 @param iallocator: instance allocator plugin to use (for use with
1138 # TODO: Convert to body parameters
1140 if disks is not None:
1141 _AppendIf(query, True,
1142 ("disks", ",".join(str(idx) for idx in disks)))
1144 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1145 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1147 return self._SendRequest(HTTP_POST,
1148 ("/%s/instances/%s/replace-disks" %
1149 (GANETI_RAPI_VERSION, instance)), query, None)
1151 def PrepareExport(self, instance, mode):
1152 """Prepares an instance for an export.
1154 @type instance: string
1155 @param instance: Instance name
1157 @param mode: Export mode
1162 query = [("mode", mode)]
1163 return self._SendRequest(HTTP_PUT,
1164 ("/%s/instances/%s/prepare-export" %
1165 (GANETI_RAPI_VERSION, instance)), query, None)
1167 def ExportInstance(self, instance, mode, destination, shutdown=None,
1168 remove_instance=None,
1169 x509_key_name=None, destination_x509_ca=None):
1170 """Exports an instance.
1172 @type instance: string
1173 @param instance: Instance name
1175 @param mode: Export mode
1181 "destination": destination,
1185 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1186 _SetItemIf(body, remove_instance is not None,
1187 "remove_instance", remove_instance)
1188 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1189 _SetItemIf(body, destination_x509_ca is not None,
1190 "destination_x509_ca", destination_x509_ca)
1192 return self._SendRequest(HTTP_PUT,
1193 ("/%s/instances/%s/export" %
1194 (GANETI_RAPI_VERSION, instance)), None, body)
1196 def MigrateInstance(self, instance, mode=None, cleanup=None,
1198 """Migrates an instance.
1200 @type instance: string
1201 @param instance: Instance name
1203 @param mode: Migration mode
1205 @param cleanup: Whether to clean up a previously failed migration
1206 @type target_node: string
1207 @param target_node: Target Node for externally mirrored instances
1213 _SetItemIf(body, mode is not None, "mode", mode)
1214 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1215 _SetItemIf(body, target_node is not None, "target_node", target_node)
1217 return self._SendRequest(HTTP_PUT,
1218 ("/%s/instances/%s/migrate" %
1219 (GANETI_RAPI_VERSION, instance)), None, body)
1221 def FailoverInstance(self, instance, iallocator=None,
1222 ignore_consistency=None, target_node=None):
1223 """Does a failover of an instance.
1225 @type instance: string
1226 @param instance: Instance name
1227 @type iallocator: string
1228 @param iallocator: Iallocator for deciding the target node for
1229 shared-storage instances
1230 @type ignore_consistency: bool
1231 @param ignore_consistency: Whether to ignore disk consistency
1232 @type target_node: string
1233 @param target_node: Target node for shared-storage instances
1239 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1240 _SetItemIf(body, ignore_consistency is not None,
1241 "ignore_consistency", ignore_consistency)
1242 _SetItemIf(body, target_node is not None, "target_node", target_node)
1244 return self._SendRequest(HTTP_PUT,
1245 ("/%s/instances/%s/failover" %
1246 (GANETI_RAPI_VERSION, instance)), None, body)
1248 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1249 """Changes the name of an instance.
1251 @type instance: string
1252 @param instance: Instance name
1253 @type new_name: string
1254 @param new_name: New instance name
1255 @type ip_check: bool
1256 @param ip_check: Whether to ensure instance's IP address is inactive
1257 @type name_check: bool
1258 @param name_check: Whether to ensure instance's name is resolvable
1264 "new_name": new_name,
1267 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1268 _SetItemIf(body, name_check is not None, "name_check", name_check)
1270 return self._SendRequest(HTTP_PUT,
1271 ("/%s/instances/%s/rename" %
1272 (GANETI_RAPI_VERSION, instance)), None, body)
1274 def GetInstanceConsole(self, instance):
1275 """Request information for connecting to instance's console.
1277 @type instance: string
1278 @param instance: Instance name
1280 @return: dictionary containing information about instance's console
1283 return self._SendRequest(HTTP_GET,
1284 ("/%s/instances/%s/console" %
1285 (GANETI_RAPI_VERSION, instance)), None, None)
1288 """Gets all jobs for the cluster.
1291 @return: job ids for the cluster
1294 return [int(j["id"])
1295 for j in self._SendRequest(HTTP_GET,
1296 "/%s/jobs" % GANETI_RAPI_VERSION,
1299 def GetJobStatus(self, job_id):
1300 """Gets the status of a job.
1302 @type job_id: string
1303 @param job_id: job id whose status to query
1309 return self._SendRequest(HTTP_GET,
1310 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1313 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1314 """Polls cluster for job status until completion.
1316 Completion is defined as any of the following states listed in
1317 L{JOB_STATUS_FINALIZED}.
1319 @type job_id: string
1320 @param job_id: job id to watch
1322 @param period: how often to poll for status (optional, default 5s)
1324 @param retries: how many time to poll before giving up
1325 (optional, default -1 means unlimited)
1328 @return: C{True} if job succeeded or C{False} if failed/status timeout
1329 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1330 possible; L{WaitForJobChange} returns immediately after a job changed and
1331 does not use polling
1335 job_result = self.GetJobStatus(job_id)
1337 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1339 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1350 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1351 """Waits for job changes.
1353 @type job_id: string
1354 @param job_id: Job ID for which to wait
1355 @return: C{None} if no changes have been detected and a dict with two keys,
1356 C{job_info} and C{log_entries} otherwise.
1362 "previous_job_info": prev_job_info,
1363 "previous_log_serial": prev_log_serial,
1366 return self._SendRequest(HTTP_GET,
1367 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1370 def CancelJob(self, job_id, dry_run=False):
1373 @type job_id: string
1374 @param job_id: id of the job to delete
1376 @param dry_run: whether to perform a dry run
1378 @return: tuple containing the result, and a message (bool, string)
1382 _AppendDryRunIf(query, dry_run)
1384 return self._SendRequest(HTTP_DELETE,
1385 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1388 def GetNodes(self, bulk=False):
1389 """Gets all nodes in the cluster.
1392 @param bulk: whether to return all information about all instances
1394 @rtype: list of dict or str
1395 @return: if bulk is true, info about nodes in the cluster,
1396 else list of nodes in the cluster
1400 _AppendIf(query, bulk, ("bulk", 1))
1402 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1407 return [n["id"] for n in nodes]
1409 def GetNode(self, node):
1410 """Gets information about a node.
1413 @param node: node whose info to return
1416 @return: info about the node
1419 return self._SendRequest(HTTP_GET,
1420 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1423 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1424 dry_run=False, early_release=None,
1425 mode=None, accept_old=False):
1426 """Evacuates instances from a Ganeti node.
1429 @param node: node to evacuate
1430 @type iallocator: str or None
1431 @param iallocator: instance allocator to use
1432 @type remote_node: str
1433 @param remote_node: node to evaucate to
1435 @param dry_run: whether to perform a dry run
1436 @type early_release: bool
1437 @param early_release: whether to enable parallelization
1439 @param mode: Node evacuation mode
1440 @type accept_old: bool
1441 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1444 @rtype: string, or a list for pre-2.5 results
1445 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1446 list of (job ID, instance name, new secondary node); if dry_run was
1447 specified, then the actual move jobs were not submitted and the job IDs
1450 @raises GanetiApiError: if an iallocator and remote_node are both
1454 if iallocator and remote_node:
1455 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1458 _AppendDryRunIf(query, dry_run)
1460 if _NODE_EVAC_RES1 in self.GetFeatures():
1461 # Server supports body parameters
1464 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1465 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1466 _SetItemIf(body, early_release is not None,
1467 "early_release", early_release)
1468 _SetItemIf(body, mode is not None, "mode", mode)
1470 # Pre-2.5 request format
1474 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1475 " not accept old-style results (parameter"
1478 # Pre-2.5 servers can only evacuate secondaries
1479 if mode is not None and mode != NODE_EVAC_SEC:
1480 raise GanetiApiError("Server can only evacuate secondary instances")
1482 _AppendIf(query, iallocator, ("iallocator", iallocator))
1483 _AppendIf(query, remote_node, ("remote_node", remote_node))
1484 _AppendIf(query, early_release, ("early_release", 1))
1486 return self._SendRequest(HTTP_POST,
1487 ("/%s/nodes/%s/evacuate" %
1488 (GANETI_RAPI_VERSION, node)), query, body)
1490 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1492 """Migrates all primary instances from a node.
1495 @param node: node to migrate
1497 @param mode: if passed, it will overwrite the live migration type,
1498 otherwise the hypervisor default will be used
1500 @param dry_run: whether to perform a dry run
1501 @type iallocator: string
1502 @param iallocator: instance allocator to use
1503 @type target_node: string
1504 @param target_node: Target node for shared-storage instances
1511 _AppendDryRunIf(query, dry_run)
1513 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1516 _SetItemIf(body, mode is not None, "mode", mode)
1517 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1518 _SetItemIf(body, target_node is not None, "target_node", target_node)
1520 assert len(query) <= 1
1522 return self._SendRequest(HTTP_POST,
1523 ("/%s/nodes/%s/migrate" %
1524 (GANETI_RAPI_VERSION, node)), query, body)
1526 # Use old request format
1527 if target_node is not None:
1528 raise GanetiApiError("Server does not support specifying target node"
1529 " for node migration")
1531 _AppendIf(query, mode is not None, ("mode", mode))
1533 return self._SendRequest(HTTP_POST,
1534 ("/%s/nodes/%s/migrate" %
1535 (GANETI_RAPI_VERSION, node)), query, None)
1537 def GetNodeRole(self, node):
1538 """Gets the current role for a node.
1541 @param node: node whose role to return
1544 @return: the current role for a node
1547 return self._SendRequest(HTTP_GET,
1548 ("/%s/nodes/%s/role" %
1549 (GANETI_RAPI_VERSION, node)), None, None)
1551 def SetNodeRole(self, node, role, force=False, auto_promote=None):
1552 """Sets the role for a node.
1555 @param node: the node whose role to set
1557 @param role: the role to set for the node
1559 @param force: whether to force the role change
1560 @type auto_promote: bool
1561 @param auto_promote: Whether node(s) should be promoted to master candidate
1569 _AppendForceIf(query, force)
1570 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1572 return self._SendRequest(HTTP_PUT,
1573 ("/%s/nodes/%s/role" %
1574 (GANETI_RAPI_VERSION, node)), query, role)
1576 def PowercycleNode(self, node, force=False):
1577 """Powercycles a node.
1580 @param node: Node name
1582 @param force: Whether to force the operation
1588 _AppendForceIf(query, force)
1590 return self._SendRequest(HTTP_POST,
1591 ("/%s/nodes/%s/powercycle" %
1592 (GANETI_RAPI_VERSION, node)), query, None)
1594 def ModifyNode(self, node, **kwargs):
1597 More details for parameters can be found in the RAPI documentation.
1600 @param node: Node name
1605 return self._SendRequest(HTTP_POST,
1606 ("/%s/nodes/%s/modify" %
1607 (GANETI_RAPI_VERSION, node)), None, kwargs)
1609 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1610 """Gets the storage units for a node.
1613 @param node: the node whose storage units to return
1614 @type storage_type: str
1615 @param storage_type: storage type whose units to return
1616 @type output_fields: str
1617 @param output_fields: storage type fields to return
1620 @return: job id where results can be retrieved
1624 ("storage_type", storage_type),
1625 ("output_fields", output_fields),
1628 return self._SendRequest(HTTP_GET,
1629 ("/%s/nodes/%s/storage" %
1630 (GANETI_RAPI_VERSION, node)), query, None)
1632 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1633 """Modifies parameters of storage units on the node.
1636 @param node: node whose storage units to modify
1637 @type storage_type: str
1638 @param storage_type: storage type whose units to modify
1640 @param name: name of the storage unit
1641 @type allocatable: bool or None
1642 @param allocatable: Whether to set the "allocatable" flag on the storage
1643 unit (None=no modification, True=set, False=unset)
1650 ("storage_type", storage_type),
1654 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1656 return self._SendRequest(HTTP_PUT,
1657 ("/%s/nodes/%s/storage/modify" %
1658 (GANETI_RAPI_VERSION, node)), query, None)
1660 def RepairNodeStorageUnits(self, node, storage_type, name):
1661 """Repairs a storage unit on the node.
1664 @param node: node whose storage units to repair
1665 @type storage_type: str
1666 @param storage_type: storage type to repair
1668 @param name: name of the storage unit to repair
1675 ("storage_type", storage_type),
1679 return self._SendRequest(HTTP_PUT,
1680 ("/%s/nodes/%s/storage/repair" %
1681 (GANETI_RAPI_VERSION, node)), query, None)
1683 def GetNodeTags(self, node):
1684 """Gets the tags for a node.
1687 @param node: node whose tags to return
1690 @return: tags for the node
1693 return self._SendRequest(HTTP_GET,
1694 ("/%s/nodes/%s/tags" %
1695 (GANETI_RAPI_VERSION, node)), None, None)
1697 def AddNodeTags(self, node, tags, dry_run=False):
1698 """Adds tags to a node.
1701 @param node: node to add tags to
1702 @type tags: list of str
1703 @param tags: tags to add to the node
1705 @param dry_run: whether to perform a dry run
1711 query = [("tag", t) for t in tags]
1712 _AppendDryRunIf(query, dry_run)
1714 return self._SendRequest(HTTP_PUT,
1715 ("/%s/nodes/%s/tags" %
1716 (GANETI_RAPI_VERSION, node)), query, tags)
1718 def DeleteNodeTags(self, node, tags, dry_run=False):
1719 """Delete tags from a node.
1722 @param node: node to remove tags from
1723 @type tags: list of str
1724 @param tags: tags to remove from the node
1726 @param dry_run: whether to perform a dry run
1732 query = [("tag", t) for t in tags]
1733 _AppendDryRunIf(query, dry_run)
1735 return self._SendRequest(HTTP_DELETE,
1736 ("/%s/nodes/%s/tags" %
1737 (GANETI_RAPI_VERSION, node)), query, None)
1739 def GetNetworks(self, bulk=False):
1740 """Gets all networks in the cluster.
1743 @param bulk: whether to return all information about the networks
1745 @rtype: list of dict or str
1746 @return: if bulk is true, a list of dictionaries with info about all
1747 networks in the cluster, else a list of names of those networks
1751 _AppendIf(query, bulk, ("bulk", 1))
1753 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1758 return [n["name"] for n in networks]
1760 def GetNetwork(self, network):
1761 """Gets information about a network.
1764 @param network: name of the network whose info to return
1767 @return: info about the network
1770 return self._SendRequest(HTTP_GET,
1771 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1774 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1775 gateway6=None, mac_prefix=None,
1776 add_reserved_ips=None, tags=None, dry_run=False):
1777 """Creates a new network.
1779 @type network_name: str
1780 @param network_name: the name of network to create
1782 @param dry_run: whether to peform a dry run
1789 _AppendDryRunIf(query, dry_run)
1791 if add_reserved_ips:
1792 add_reserved_ips = add_reserved_ips.split(",")
1795 tags = tags.split(",")
1798 "network_name": network_name,
1801 "gateway6": gateway6,
1802 "network6": network6,
1803 "mac_prefix": mac_prefix,
1804 "add_reserved_ips": add_reserved_ips,
1808 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1811 def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
1812 """Connects a Network to a NodeGroup with the given netparams
1816 "group_name": group_name,
1817 "network_mode": mode,
1818 "network_link": link,
1822 _AppendDryRunIf(query, dry_run)
1824 return self._SendRequest(HTTP_PUT,
1825 ("/%s/networks/%s/connect" %
1826 (GANETI_RAPI_VERSION, network_name)), query, body)
1828 def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1829 """Connects a Network to a NodeGroup with the given netparams
1833 "group_name": group_name,
1837 _AppendDryRunIf(query, dry_run)
1839 return self._SendRequest(HTTP_PUT,
1840 ("/%s/networks/%s/disconnect" %
1841 (GANETI_RAPI_VERSION, network_name)), query, body)
1843 def ModifyNetwork(self, network, **kwargs):
1844 """Modifies a network.
1846 More details for parameters can be found in the RAPI documentation.
1848 @type network: string
1849 @param network: Network name
1854 return self._SendRequest(HTTP_PUT,
1855 ("/%s/networks/%s/modify" %
1856 (GANETI_RAPI_VERSION, network)), None, kwargs)
1858 def DeleteNetwork(self, network, dry_run=False):
1859 """Deletes a network.
1862 @param network: the network to delete
1864 @param dry_run: whether to peform a dry run
1871 _AppendDryRunIf(query, dry_run)
1873 return self._SendRequest(HTTP_DELETE,
1874 ("/%s/networks/%s" %
1875 (GANETI_RAPI_VERSION, network)), query, None)
1877 def GetNetworkTags(self, network):
1878 """Gets tags for a network.
1880 @type network: string
1881 @param network: Node group whose tags to return
1883 @rtype: list of strings
1884 @return: tags for the network
1887 return self._SendRequest(HTTP_GET,
1888 ("/%s/networks/%s/tags" %
1889 (GANETI_RAPI_VERSION, network)), None, None)
1891 def AddNetworkTags(self, network, tags, dry_run=False):
1892 """Adds tags to a network.
1895 @param network: network to add tags to
1896 @type tags: list of string
1897 @param tags: tags to add to the network
1899 @param dry_run: whether to perform a dry run
1905 query = [("tag", t) for t in tags]
1906 _AppendDryRunIf(query, dry_run)
1908 return self._SendRequest(HTTP_PUT,
1909 ("/%s/networks/%s/tags" %
1910 (GANETI_RAPI_VERSION, network)), query, None)
1912 def DeleteNetworkTags(self, network, tags, dry_run=False):
1913 """Deletes tags from a network.
1916 @param network: network to delete tags from
1917 @type tags: list of string
1918 @param tags: tags to delete
1920 @param dry_run: whether to perform a dry run
1925 query = [("tag", t) for t in tags]
1926 _AppendDryRunIf(query, dry_run)
1928 return self._SendRequest(HTTP_DELETE,
1929 ("/%s/networks/%s/tags" %
1930 (GANETI_RAPI_VERSION, network)), query, None)
1932 def GetGroups(self, bulk=False):
1933 """Gets all node groups in the cluster.
1936 @param bulk: whether to return all information about the groups
1938 @rtype: list of dict or str
1939 @return: if bulk is true, a list of dictionaries with info about all node
1940 groups in the cluster, else a list of names of those node groups
1944 _AppendIf(query, bulk, ("bulk", 1))
1946 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1951 return [g["name"] for g in groups]
1953 def GetGroup(self, group):
1954 """Gets information about a node group.
1957 @param group: name of the node group whose info to return
1960 @return: info about the node group
1963 return self._SendRequest(HTTP_GET,
1964 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1967 def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1968 """Creates a new node group.
1971 @param name: the name of node group to create
1972 @type alloc_policy: str
1973 @param alloc_policy: the desired allocation policy for the group, if any
1975 @param dry_run: whether to peform a dry run
1982 _AppendDryRunIf(query, dry_run)
1986 "alloc_policy": alloc_policy,
1989 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1992 def ModifyGroup(self, group, **kwargs):
1993 """Modifies a node group.
1995 More details for parameters can be found in the RAPI documentation.
1998 @param group: Node group name
2003 return self._SendRequest(HTTP_PUT,
2004 ("/%s/groups/%s/modify" %
2005 (GANETI_RAPI_VERSION, group)), None, kwargs)
2007 def DeleteGroup(self, group, dry_run=False):
2008 """Deletes a node group.
2011 @param group: the node group to delete
2013 @param dry_run: whether to peform a dry run
2020 _AppendDryRunIf(query, dry_run)
2022 return self._SendRequest(HTTP_DELETE,
2024 (GANETI_RAPI_VERSION, group)), query, None)
2026 def RenameGroup(self, group, new_name):
2027 """Changes the name of a node group.
2030 @param group: Node group name
2031 @type new_name: string
2032 @param new_name: New node group name
2039 "new_name": new_name,
2042 return self._SendRequest(HTTP_PUT,
2043 ("/%s/groups/%s/rename" %
2044 (GANETI_RAPI_VERSION, group)), None, body)
2046 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2047 """Assigns nodes to a group.
2050 @param group: Node group name
2051 @type nodes: list of strings
2052 @param nodes: List of nodes to assign to the group
2059 _AppendForceIf(query, force)
2060 _AppendDryRunIf(query, dry_run)
2066 return self._SendRequest(HTTP_PUT,
2067 ("/%s/groups/%s/assign-nodes" %
2068 (GANETI_RAPI_VERSION, group)), query, body)
2070 def GetGroupTags(self, group):
2071 """Gets tags for a node group.
2074 @param group: Node group whose tags to return
2076 @rtype: list of strings
2077 @return: tags for the group
2080 return self._SendRequest(HTTP_GET,
2081 ("/%s/groups/%s/tags" %
2082 (GANETI_RAPI_VERSION, group)), None, None)
2084 def AddGroupTags(self, group, tags, dry_run=False):
2085 """Adds tags to a node group.
2088 @param group: group to add tags to
2089 @type tags: list of string
2090 @param tags: tags to add to the group
2092 @param dry_run: whether to perform a dry run
2098 query = [("tag", t) for t in tags]
2099 _AppendDryRunIf(query, dry_run)
2101 return self._SendRequest(HTTP_PUT,
2102 ("/%s/groups/%s/tags" %
2103 (GANETI_RAPI_VERSION, group)), query, None)
2105 def DeleteGroupTags(self, group, tags, dry_run=False):
2106 """Deletes tags from a node group.
2109 @param group: group to delete tags from
2110 @type tags: list of string
2111 @param tags: tags to delete
2113 @param dry_run: whether to perform a dry run
2118 query = [("tag", t) for t in tags]
2119 _AppendDryRunIf(query, dry_run)
2121 return self._SendRequest(HTTP_DELETE,
2122 ("/%s/groups/%s/tags" %
2123 (GANETI_RAPI_VERSION, group)), query, None)
2125 def Query(self, what, fields, qfilter=None):
2126 """Retrieves information about resources.
2129 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2130 @type fields: list of string
2131 @param fields: Requested fields
2132 @type qfilter: None or list
2133 @param qfilter: Query filter
2143 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2144 # TODO: remove "filter" after 2.7
2145 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2147 return self._SendRequest(HTTP_PUT,
2149 (GANETI_RAPI_VERSION, what)), None, body)
2151 def QueryFields(self, what, fields=None):
2152 """Retrieves available fields for a resource.
2155 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2156 @type fields: list of string
2157 @param fields: Requested fields
2165 if fields is not None:
2166 _AppendIf(query, True, ("fields", ",".join(fields)))
2168 return self._SendRequest(HTTP_GET,
2169 ("/%s/query/%s/fields" %
2170 (GANETI_RAPI_VERSION, what)), query, None)