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,
1004 dry_run=False, reason=None):
1005 """Reboots an instance.
1008 @param instance: instance to reboot
1009 @type reboot_type: str
1010 @param reboot_type: one of: hard, soft, full
1011 @type ignore_secondaries: bool
1012 @param ignore_secondaries: if True, ignores errors for the secondary node
1013 while re-assembling disks (in hard-reboot mode only)
1015 @param dry_run: whether to perform a dry run
1016 @type reason: string
1017 @param reason: the reason for the reboot
1023 _AppendDryRunIf(query, dry_run)
1024 _AppendIf(query, reboot_type, ("type", reboot_type))
1025 _AppendIf(query, ignore_secondaries is not None,
1026 ("ignore_secondaries", ignore_secondaries))
1027 _AppendIf(query, reason, ("reason", reason))
1029 return self._SendRequest(HTTP_POST,
1030 ("/%s/instances/%s/reboot" %
1031 (GANETI_RAPI_VERSION, instance)), query, None)
1033 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1034 reason=None, **kwargs):
1035 """Shuts down an instance.
1038 @param instance: the instance to shut down
1040 @param dry_run: whether to perform a dry run
1041 @type no_remember: bool
1042 @param no_remember: if true, will not record the state change
1043 @type reason: string
1044 @param reason: the reason for the shutdown
1052 _AppendDryRunIf(query, dry_run)
1053 _AppendIf(query, no_remember, ("no_remember", 1))
1054 _AppendIf(query, reason, ("reason", reason))
1056 return self._SendRequest(HTTP_PUT,
1057 ("/%s/instances/%s/shutdown" %
1058 (GANETI_RAPI_VERSION, instance)), query, body)
1060 def StartupInstance(self, instance, dry_run=False, no_remember=False,
1062 """Starts up an instance.
1065 @param instance: the instance to start up
1067 @param dry_run: whether to perform a dry run
1068 @type no_remember: bool
1069 @param no_remember: if true, will not record the state change
1070 @type reason: string
1071 @param reason: the reason for the startup
1077 _AppendDryRunIf(query, dry_run)
1078 _AppendIf(query, no_remember, ("no_remember", 1))
1079 _AppendIf(query, reason, ("reason", reason))
1081 return self._SendRequest(HTTP_PUT,
1082 ("/%s/instances/%s/startup" %
1083 (GANETI_RAPI_VERSION, instance)), query, None)
1085 def ReinstallInstance(self, instance, os=None, no_startup=False,
1087 """Reinstalls an instance.
1090 @param instance: The instance to reinstall
1091 @type os: str or None
1092 @param os: The operating system to reinstall. If None, the instance's
1093 current operating system will be installed again
1094 @type no_startup: bool
1095 @param no_startup: Whether to start the instance automatically
1100 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1102 "start": not no_startup,
1104 _SetItemIf(body, os is not None, "os", os)
1105 _SetItemIf(body, osparams is not None, "osparams", osparams)
1106 return self._SendRequest(HTTP_POST,
1107 ("/%s/instances/%s/reinstall" %
1108 (GANETI_RAPI_VERSION, instance)), None, body)
1110 # Use old request format
1112 raise GanetiApiError("Server does not support specifying OS parameters"
1113 " for instance reinstallation")
1116 _AppendIf(query, os, ("os", os))
1117 _AppendIf(query, no_startup, ("nostartup", 1))
1119 return self._SendRequest(HTTP_POST,
1120 ("/%s/instances/%s/reinstall" %
1121 (GANETI_RAPI_VERSION, instance)), query, None)
1123 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1124 remote_node=None, iallocator=None):
1125 """Replaces disks on an instance.
1128 @param instance: instance whose disks to replace
1129 @type disks: list of ints
1130 @param disks: Indexes of disks to replace
1132 @param mode: replacement mode to use (defaults to replace_auto)
1133 @type remote_node: str or None
1134 @param remote_node: new secondary node to use (for use with
1135 replace_new_secondary mode)
1136 @type iallocator: str or None
1137 @param iallocator: instance allocator plugin to use (for use with
1148 # TODO: Convert to body parameters
1150 if disks is not None:
1151 _AppendIf(query, True,
1152 ("disks", ",".join(str(idx) for idx in disks)))
1154 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1155 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1157 return self._SendRequest(HTTP_POST,
1158 ("/%s/instances/%s/replace-disks" %
1159 (GANETI_RAPI_VERSION, instance)), query, None)
1161 def PrepareExport(self, instance, mode):
1162 """Prepares an instance for an export.
1164 @type instance: string
1165 @param instance: Instance name
1167 @param mode: Export mode
1172 query = [("mode", mode)]
1173 return self._SendRequest(HTTP_PUT,
1174 ("/%s/instances/%s/prepare-export" %
1175 (GANETI_RAPI_VERSION, instance)), query, None)
1177 def ExportInstance(self, instance, mode, destination, shutdown=None,
1178 remove_instance=None,
1179 x509_key_name=None, destination_x509_ca=None):
1180 """Exports an instance.
1182 @type instance: string
1183 @param instance: Instance name
1185 @param mode: Export mode
1191 "destination": destination,
1195 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1196 _SetItemIf(body, remove_instance is not None,
1197 "remove_instance", remove_instance)
1198 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1199 _SetItemIf(body, destination_x509_ca is not None,
1200 "destination_x509_ca", destination_x509_ca)
1202 return self._SendRequest(HTTP_PUT,
1203 ("/%s/instances/%s/export" %
1204 (GANETI_RAPI_VERSION, instance)), None, body)
1206 def MigrateInstance(self, instance, mode=None, cleanup=None,
1208 """Migrates an instance.
1210 @type instance: string
1211 @param instance: Instance name
1213 @param mode: Migration mode
1215 @param cleanup: Whether to clean up a previously failed migration
1216 @type target_node: string
1217 @param target_node: Target Node for externally mirrored instances
1223 _SetItemIf(body, mode is not None, "mode", mode)
1224 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1225 _SetItemIf(body, target_node is not None, "target_node", target_node)
1227 return self._SendRequest(HTTP_PUT,
1228 ("/%s/instances/%s/migrate" %
1229 (GANETI_RAPI_VERSION, instance)), None, body)
1231 def FailoverInstance(self, instance, iallocator=None,
1232 ignore_consistency=None, target_node=None):
1233 """Does a failover of an instance.
1235 @type instance: string
1236 @param instance: Instance name
1237 @type iallocator: string
1238 @param iallocator: Iallocator for deciding the target node for
1239 shared-storage instances
1240 @type ignore_consistency: bool
1241 @param ignore_consistency: Whether to ignore disk consistency
1242 @type target_node: string
1243 @param target_node: Target node for shared-storage instances
1249 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1250 _SetItemIf(body, ignore_consistency is not None,
1251 "ignore_consistency", ignore_consistency)
1252 _SetItemIf(body, target_node is not None, "target_node", target_node)
1254 return self._SendRequest(HTTP_PUT,
1255 ("/%s/instances/%s/failover" %
1256 (GANETI_RAPI_VERSION, instance)), None, body)
1258 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1259 """Changes the name of an instance.
1261 @type instance: string
1262 @param instance: Instance name
1263 @type new_name: string
1264 @param new_name: New instance name
1265 @type ip_check: bool
1266 @param ip_check: Whether to ensure instance's IP address is inactive
1267 @type name_check: bool
1268 @param name_check: Whether to ensure instance's name is resolvable
1274 "new_name": new_name,
1277 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1278 _SetItemIf(body, name_check is not None, "name_check", name_check)
1280 return self._SendRequest(HTTP_PUT,
1281 ("/%s/instances/%s/rename" %
1282 (GANETI_RAPI_VERSION, instance)), None, body)
1284 def GetInstanceConsole(self, instance):
1285 """Request information for connecting to instance's console.
1287 @type instance: string
1288 @param instance: Instance name
1290 @return: dictionary containing information about instance's console
1293 return self._SendRequest(HTTP_GET,
1294 ("/%s/instances/%s/console" %
1295 (GANETI_RAPI_VERSION, instance)), None, None)
1298 """Gets all jobs for the cluster.
1301 @return: job ids for the cluster
1304 return [int(j["id"])
1305 for j in self._SendRequest(HTTP_GET,
1306 "/%s/jobs" % GANETI_RAPI_VERSION,
1309 def GetJobStatus(self, job_id):
1310 """Gets the status of a job.
1312 @type job_id: string
1313 @param job_id: job id whose status to query
1319 return self._SendRequest(HTTP_GET,
1320 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1323 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1324 """Polls cluster for job status until completion.
1326 Completion is defined as any of the following states listed in
1327 L{JOB_STATUS_FINALIZED}.
1329 @type job_id: string
1330 @param job_id: job id to watch
1332 @param period: how often to poll for status (optional, default 5s)
1334 @param retries: how many time to poll before giving up
1335 (optional, default -1 means unlimited)
1338 @return: C{True} if job succeeded or C{False} if failed/status timeout
1339 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1340 possible; L{WaitForJobChange} returns immediately after a job changed and
1341 does not use polling
1345 job_result = self.GetJobStatus(job_id)
1347 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1349 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1360 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1361 """Waits for job changes.
1363 @type job_id: string
1364 @param job_id: Job ID for which to wait
1365 @return: C{None} if no changes have been detected and a dict with two keys,
1366 C{job_info} and C{log_entries} otherwise.
1372 "previous_job_info": prev_job_info,
1373 "previous_log_serial": prev_log_serial,
1376 return self._SendRequest(HTTP_GET,
1377 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1380 def CancelJob(self, job_id, dry_run=False):
1383 @type job_id: string
1384 @param job_id: id of the job to delete
1386 @param dry_run: whether to perform a dry run
1388 @return: tuple containing the result, and a message (bool, string)
1392 _AppendDryRunIf(query, dry_run)
1394 return self._SendRequest(HTTP_DELETE,
1395 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1398 def GetNodes(self, bulk=False):
1399 """Gets all nodes in the cluster.
1402 @param bulk: whether to return all information about all instances
1404 @rtype: list of dict or str
1405 @return: if bulk is true, info about nodes in the cluster,
1406 else list of nodes in the cluster
1410 _AppendIf(query, bulk, ("bulk", 1))
1412 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1417 return [n["id"] for n in nodes]
1419 def GetNode(self, node):
1420 """Gets information about a node.
1423 @param node: node whose info to return
1426 @return: info about the node
1429 return self._SendRequest(HTTP_GET,
1430 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1433 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1434 dry_run=False, early_release=None,
1435 mode=None, accept_old=False):
1436 """Evacuates instances from a Ganeti node.
1439 @param node: node to evacuate
1440 @type iallocator: str or None
1441 @param iallocator: instance allocator to use
1442 @type remote_node: str
1443 @param remote_node: node to evaucate to
1445 @param dry_run: whether to perform a dry run
1446 @type early_release: bool
1447 @param early_release: whether to enable parallelization
1449 @param mode: Node evacuation mode
1450 @type accept_old: bool
1451 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1454 @rtype: string, or a list for pre-2.5 results
1455 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1456 list of (job ID, instance name, new secondary node); if dry_run was
1457 specified, then the actual move jobs were not submitted and the job IDs
1460 @raises GanetiApiError: if an iallocator and remote_node are both
1464 if iallocator and remote_node:
1465 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1468 _AppendDryRunIf(query, dry_run)
1470 if _NODE_EVAC_RES1 in self.GetFeatures():
1471 # Server supports body parameters
1474 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1475 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1476 _SetItemIf(body, early_release is not None,
1477 "early_release", early_release)
1478 _SetItemIf(body, mode is not None, "mode", mode)
1480 # Pre-2.5 request format
1484 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1485 " not accept old-style results (parameter"
1488 # Pre-2.5 servers can only evacuate secondaries
1489 if mode is not None and mode != NODE_EVAC_SEC:
1490 raise GanetiApiError("Server can only evacuate secondary instances")
1492 _AppendIf(query, iallocator, ("iallocator", iallocator))
1493 _AppendIf(query, remote_node, ("remote_node", remote_node))
1494 _AppendIf(query, early_release, ("early_release", 1))
1496 return self._SendRequest(HTTP_POST,
1497 ("/%s/nodes/%s/evacuate" %
1498 (GANETI_RAPI_VERSION, node)), query, body)
1500 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1502 """Migrates all primary instances from a node.
1505 @param node: node to migrate
1507 @param mode: if passed, it will overwrite the live migration type,
1508 otherwise the hypervisor default will be used
1510 @param dry_run: whether to perform a dry run
1511 @type iallocator: string
1512 @param iallocator: instance allocator to use
1513 @type target_node: string
1514 @param target_node: Target node for shared-storage instances
1521 _AppendDryRunIf(query, dry_run)
1523 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1526 _SetItemIf(body, mode is not None, "mode", mode)
1527 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1528 _SetItemIf(body, target_node is not None, "target_node", target_node)
1530 assert len(query) <= 1
1532 return self._SendRequest(HTTP_POST,
1533 ("/%s/nodes/%s/migrate" %
1534 (GANETI_RAPI_VERSION, node)), query, body)
1536 # Use old request format
1537 if target_node is not None:
1538 raise GanetiApiError("Server does not support specifying target node"
1539 " for node migration")
1541 _AppendIf(query, mode is not None, ("mode", mode))
1543 return self._SendRequest(HTTP_POST,
1544 ("/%s/nodes/%s/migrate" %
1545 (GANETI_RAPI_VERSION, node)), query, None)
1547 def GetNodeRole(self, node):
1548 """Gets the current role for a node.
1551 @param node: node whose role to return
1554 @return: the current role for a node
1557 return self._SendRequest(HTTP_GET,
1558 ("/%s/nodes/%s/role" %
1559 (GANETI_RAPI_VERSION, node)), None, None)
1561 def SetNodeRole(self, node, role, force=False, auto_promote=None):
1562 """Sets the role for a node.
1565 @param node: the node whose role to set
1567 @param role: the role to set for the node
1569 @param force: whether to force the role change
1570 @type auto_promote: bool
1571 @param auto_promote: Whether node(s) should be promoted to master candidate
1579 _AppendForceIf(query, force)
1580 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1582 return self._SendRequest(HTTP_PUT,
1583 ("/%s/nodes/%s/role" %
1584 (GANETI_RAPI_VERSION, node)), query, role)
1586 def PowercycleNode(self, node, force=False):
1587 """Powercycles a node.
1590 @param node: Node name
1592 @param force: Whether to force the operation
1598 _AppendForceIf(query, force)
1600 return self._SendRequest(HTTP_POST,
1601 ("/%s/nodes/%s/powercycle" %
1602 (GANETI_RAPI_VERSION, node)), query, None)
1604 def ModifyNode(self, node, **kwargs):
1607 More details for parameters can be found in the RAPI documentation.
1610 @param node: Node name
1615 return self._SendRequest(HTTP_POST,
1616 ("/%s/nodes/%s/modify" %
1617 (GANETI_RAPI_VERSION, node)), None, kwargs)
1619 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1620 """Gets the storage units for a node.
1623 @param node: the node whose storage units to return
1624 @type storage_type: str
1625 @param storage_type: storage type whose units to return
1626 @type output_fields: str
1627 @param output_fields: storage type fields to return
1630 @return: job id where results can be retrieved
1634 ("storage_type", storage_type),
1635 ("output_fields", output_fields),
1638 return self._SendRequest(HTTP_GET,
1639 ("/%s/nodes/%s/storage" %
1640 (GANETI_RAPI_VERSION, node)), query, None)
1642 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1643 """Modifies parameters of storage units on the node.
1646 @param node: node whose storage units to modify
1647 @type storage_type: str
1648 @param storage_type: storage type whose units to modify
1650 @param name: name of the storage unit
1651 @type allocatable: bool or None
1652 @param allocatable: Whether to set the "allocatable" flag on the storage
1653 unit (None=no modification, True=set, False=unset)
1660 ("storage_type", storage_type),
1664 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1666 return self._SendRequest(HTTP_PUT,
1667 ("/%s/nodes/%s/storage/modify" %
1668 (GANETI_RAPI_VERSION, node)), query, None)
1670 def RepairNodeStorageUnits(self, node, storage_type, name):
1671 """Repairs a storage unit on the node.
1674 @param node: node whose storage units to repair
1675 @type storage_type: str
1676 @param storage_type: storage type to repair
1678 @param name: name of the storage unit to repair
1685 ("storage_type", storage_type),
1689 return self._SendRequest(HTTP_PUT,
1690 ("/%s/nodes/%s/storage/repair" %
1691 (GANETI_RAPI_VERSION, node)), query, None)
1693 def GetNodeTags(self, node):
1694 """Gets the tags for a node.
1697 @param node: node whose tags to return
1700 @return: tags for the node
1703 return self._SendRequest(HTTP_GET,
1704 ("/%s/nodes/%s/tags" %
1705 (GANETI_RAPI_VERSION, node)), None, None)
1707 def AddNodeTags(self, node, tags, dry_run=False):
1708 """Adds tags to a node.
1711 @param node: node to add tags to
1712 @type tags: list of str
1713 @param tags: tags to add to the node
1715 @param dry_run: whether to perform a dry run
1721 query = [("tag", t) for t in tags]
1722 _AppendDryRunIf(query, dry_run)
1724 return self._SendRequest(HTTP_PUT,
1725 ("/%s/nodes/%s/tags" %
1726 (GANETI_RAPI_VERSION, node)), query, tags)
1728 def DeleteNodeTags(self, node, tags, dry_run=False):
1729 """Delete tags from a node.
1732 @param node: node to remove tags from
1733 @type tags: list of str
1734 @param tags: tags to remove from the node
1736 @param dry_run: whether to perform a dry run
1742 query = [("tag", t) for t in tags]
1743 _AppendDryRunIf(query, dry_run)
1745 return self._SendRequest(HTTP_DELETE,
1746 ("/%s/nodes/%s/tags" %
1747 (GANETI_RAPI_VERSION, node)), query, None)
1749 def GetNetworks(self, bulk=False):
1750 """Gets all networks in the cluster.
1753 @param bulk: whether to return all information about the networks
1755 @rtype: list of dict or str
1756 @return: if bulk is true, a list of dictionaries with info about all
1757 networks in the cluster, else a list of names of those networks
1761 _AppendIf(query, bulk, ("bulk", 1))
1763 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1768 return [n["name"] for n in networks]
1770 def GetNetwork(self, network):
1771 """Gets information about a network.
1774 @param network: name of the network whose info to return
1777 @return: info about the network
1780 return self._SendRequest(HTTP_GET,
1781 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1784 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1785 gateway6=None, mac_prefix=None,
1786 add_reserved_ips=None, tags=None, dry_run=False):
1787 """Creates a new network.
1789 @type network_name: str
1790 @param network_name: the name of network to create
1792 @param dry_run: whether to peform a dry run
1799 _AppendDryRunIf(query, dry_run)
1801 if add_reserved_ips:
1802 add_reserved_ips = add_reserved_ips.split(",")
1805 tags = tags.split(",")
1808 "network_name": network_name,
1811 "gateway6": gateway6,
1812 "network6": network6,
1813 "mac_prefix": mac_prefix,
1814 "add_reserved_ips": add_reserved_ips,
1818 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1821 def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
1822 """Connects a Network to a NodeGroup with the given netparams
1826 "group_name": group_name,
1827 "network_mode": mode,
1828 "network_link": link,
1832 _AppendDryRunIf(query, dry_run)
1834 return self._SendRequest(HTTP_PUT,
1835 ("/%s/networks/%s/connect" %
1836 (GANETI_RAPI_VERSION, network_name)), query, body)
1838 def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1839 """Connects a Network to a NodeGroup with the given netparams
1843 "group_name": group_name,
1847 _AppendDryRunIf(query, dry_run)
1849 return self._SendRequest(HTTP_PUT,
1850 ("/%s/networks/%s/disconnect" %
1851 (GANETI_RAPI_VERSION, network_name)), query, body)
1853 def ModifyNetwork(self, network, **kwargs):
1854 """Modifies a network.
1856 More details for parameters can be found in the RAPI documentation.
1858 @type network: string
1859 @param network: Network name
1864 return self._SendRequest(HTTP_PUT,
1865 ("/%s/networks/%s/modify" %
1866 (GANETI_RAPI_VERSION, network)), None, kwargs)
1868 def DeleteNetwork(self, network, dry_run=False):
1869 """Deletes a network.
1872 @param network: the network to delete
1874 @param dry_run: whether to peform a dry run
1881 _AppendDryRunIf(query, dry_run)
1883 return self._SendRequest(HTTP_DELETE,
1884 ("/%s/networks/%s" %
1885 (GANETI_RAPI_VERSION, network)), query, None)
1887 def GetNetworkTags(self, network):
1888 """Gets tags for a network.
1890 @type network: string
1891 @param network: Node group whose tags to return
1893 @rtype: list of strings
1894 @return: tags for the network
1897 return self._SendRequest(HTTP_GET,
1898 ("/%s/networks/%s/tags" %
1899 (GANETI_RAPI_VERSION, network)), None, None)
1901 def AddNetworkTags(self, network, tags, dry_run=False):
1902 """Adds tags to a network.
1905 @param network: network to add tags to
1906 @type tags: list of string
1907 @param tags: tags to add to the network
1909 @param dry_run: whether to perform a dry run
1915 query = [("tag", t) for t in tags]
1916 _AppendDryRunIf(query, dry_run)
1918 return self._SendRequest(HTTP_PUT,
1919 ("/%s/networks/%s/tags" %
1920 (GANETI_RAPI_VERSION, network)), query, None)
1922 def DeleteNetworkTags(self, network, tags, dry_run=False):
1923 """Deletes tags from a network.
1926 @param network: network to delete tags from
1927 @type tags: list of string
1928 @param tags: tags to delete
1930 @param dry_run: whether to perform a dry run
1935 query = [("tag", t) for t in tags]
1936 _AppendDryRunIf(query, dry_run)
1938 return self._SendRequest(HTTP_DELETE,
1939 ("/%s/networks/%s/tags" %
1940 (GANETI_RAPI_VERSION, network)), query, None)
1942 def GetGroups(self, bulk=False):
1943 """Gets all node groups in the cluster.
1946 @param bulk: whether to return all information about the groups
1948 @rtype: list of dict or str
1949 @return: if bulk is true, a list of dictionaries with info about all node
1950 groups in the cluster, else a list of names of those node groups
1954 _AppendIf(query, bulk, ("bulk", 1))
1956 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1961 return [g["name"] for g in groups]
1963 def GetGroup(self, group):
1964 """Gets information about a node group.
1967 @param group: name of the node group whose info to return
1970 @return: info about the node group
1973 return self._SendRequest(HTTP_GET,
1974 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1977 def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1978 """Creates a new node group.
1981 @param name: the name of node group to create
1982 @type alloc_policy: str
1983 @param alloc_policy: the desired allocation policy for the group, if any
1985 @param dry_run: whether to peform a dry run
1992 _AppendDryRunIf(query, dry_run)
1996 "alloc_policy": alloc_policy,
1999 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2002 def ModifyGroup(self, group, **kwargs):
2003 """Modifies a node group.
2005 More details for parameters can be found in the RAPI documentation.
2008 @param group: Node group name
2013 return self._SendRequest(HTTP_PUT,
2014 ("/%s/groups/%s/modify" %
2015 (GANETI_RAPI_VERSION, group)), None, kwargs)
2017 def DeleteGroup(self, group, dry_run=False):
2018 """Deletes a node group.
2021 @param group: the node group to delete
2023 @param dry_run: whether to peform a dry run
2030 _AppendDryRunIf(query, dry_run)
2032 return self._SendRequest(HTTP_DELETE,
2034 (GANETI_RAPI_VERSION, group)), query, None)
2036 def RenameGroup(self, group, new_name):
2037 """Changes the name of a node group.
2040 @param group: Node group name
2041 @type new_name: string
2042 @param new_name: New node group name
2049 "new_name": new_name,
2052 return self._SendRequest(HTTP_PUT,
2053 ("/%s/groups/%s/rename" %
2054 (GANETI_RAPI_VERSION, group)), None, body)
2056 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2057 """Assigns nodes to a group.
2060 @param group: Node group name
2061 @type nodes: list of strings
2062 @param nodes: List of nodes to assign to the group
2069 _AppendForceIf(query, force)
2070 _AppendDryRunIf(query, dry_run)
2076 return self._SendRequest(HTTP_PUT,
2077 ("/%s/groups/%s/assign-nodes" %
2078 (GANETI_RAPI_VERSION, group)), query, body)
2080 def GetGroupTags(self, group):
2081 """Gets tags for a node group.
2084 @param group: Node group whose tags to return
2086 @rtype: list of strings
2087 @return: tags for the group
2090 return self._SendRequest(HTTP_GET,
2091 ("/%s/groups/%s/tags" %
2092 (GANETI_RAPI_VERSION, group)), None, None)
2094 def AddGroupTags(self, group, tags, dry_run=False):
2095 """Adds tags to a node group.
2098 @param group: group to add tags to
2099 @type tags: list of string
2100 @param tags: tags to add to the group
2102 @param dry_run: whether to perform a dry run
2108 query = [("tag", t) for t in tags]
2109 _AppendDryRunIf(query, dry_run)
2111 return self._SendRequest(HTTP_PUT,
2112 ("/%s/groups/%s/tags" %
2113 (GANETI_RAPI_VERSION, group)), query, None)
2115 def DeleteGroupTags(self, group, tags, dry_run=False):
2116 """Deletes tags from a node group.
2119 @param group: group to delete tags from
2120 @type tags: list of string
2121 @param tags: tags to delete
2123 @param dry_run: whether to perform a dry run
2128 query = [("tag", t) for t in tags]
2129 _AppendDryRunIf(query, dry_run)
2131 return self._SendRequest(HTTP_DELETE,
2132 ("/%s/groups/%s/tags" %
2133 (GANETI_RAPI_VERSION, group)), query, None)
2135 def Query(self, what, fields, qfilter=None):
2136 """Retrieves information about resources.
2139 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2140 @type fields: list of string
2141 @param fields: Requested fields
2142 @type qfilter: None or list
2143 @param qfilter: Query filter
2153 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2154 # TODO: remove "filter" after 2.7
2155 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2157 return self._SendRequest(HTTP_PUT,
2159 (GANETI_RAPI_VERSION, what)), None, body)
2161 def QueryFields(self, what, fields=None):
2162 """Retrieves available fields for a resource.
2165 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2166 @type fields: list of string
2167 @param fields: Requested fields
2175 if fields is not None:
2176 _AppendIf(query, True, ("fields", ",".join(fields)))
2178 return self._SendRequest(HTTP_GET,
2179 ("/%s/query/%s/fields" %
2180 (GANETI_RAPI_VERSION, what)), query, None)