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)
1297 def GetJobs(self, bulk=False):
1298 """Gets all jobs for the cluster.
1301 @param bulk: Whether to return detailed information about jobs.
1303 @return: List of job ids for the cluster or list of dicts with detailed
1304 information about the jobs if bulk parameter was true.
1308 _AppendIf(query, bulk, ("bulk", 1))
1311 return self._SendRequest(HTTP_GET,
1312 "/%s/jobs" % GANETI_RAPI_VERSION,
1315 return [int(j["id"])
1316 for j in self._SendRequest(HTTP_GET,
1317 "/%s/jobs" % GANETI_RAPI_VERSION,
1320 def GetJobStatus(self, job_id):
1321 """Gets the status of a job.
1323 @type job_id: string
1324 @param job_id: job id whose status to query
1330 return self._SendRequest(HTTP_GET,
1331 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1334 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1335 """Polls cluster for job status until completion.
1337 Completion is defined as any of the following states listed in
1338 L{JOB_STATUS_FINALIZED}.
1340 @type job_id: string
1341 @param job_id: job id to watch
1343 @param period: how often to poll for status (optional, default 5s)
1345 @param retries: how many time to poll before giving up
1346 (optional, default -1 means unlimited)
1349 @return: C{True} if job succeeded or C{False} if failed/status timeout
1350 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1351 possible; L{WaitForJobChange} returns immediately after a job changed and
1352 does not use polling
1356 job_result = self.GetJobStatus(job_id)
1358 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1360 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1371 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1372 """Waits for job changes.
1374 @type job_id: string
1375 @param job_id: Job ID for which to wait
1376 @return: C{None} if no changes have been detected and a dict with two keys,
1377 C{job_info} and C{log_entries} otherwise.
1383 "previous_job_info": prev_job_info,
1384 "previous_log_serial": prev_log_serial,
1387 return self._SendRequest(HTTP_GET,
1388 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1391 def CancelJob(self, job_id, dry_run=False):
1394 @type job_id: string
1395 @param job_id: id of the job to delete
1397 @param dry_run: whether to perform a dry run
1399 @return: tuple containing the result, and a message (bool, string)
1403 _AppendDryRunIf(query, dry_run)
1405 return self._SendRequest(HTTP_DELETE,
1406 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1409 def GetNodes(self, bulk=False):
1410 """Gets all nodes in the cluster.
1413 @param bulk: whether to return all information about all instances
1415 @rtype: list of dict or str
1416 @return: if bulk is true, info about nodes in the cluster,
1417 else list of nodes in the cluster
1421 _AppendIf(query, bulk, ("bulk", 1))
1423 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1428 return [n["id"] for n in nodes]
1430 def GetNode(self, node):
1431 """Gets information about a node.
1434 @param node: node whose info to return
1437 @return: info about the node
1440 return self._SendRequest(HTTP_GET,
1441 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1444 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1445 dry_run=False, early_release=None,
1446 mode=None, accept_old=False):
1447 """Evacuates instances from a Ganeti node.
1450 @param node: node to evacuate
1451 @type iallocator: str or None
1452 @param iallocator: instance allocator to use
1453 @type remote_node: str
1454 @param remote_node: node to evaucate to
1456 @param dry_run: whether to perform a dry run
1457 @type early_release: bool
1458 @param early_release: whether to enable parallelization
1460 @param mode: Node evacuation mode
1461 @type accept_old: bool
1462 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1465 @rtype: string, or a list for pre-2.5 results
1466 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1467 list of (job ID, instance name, new secondary node); if dry_run was
1468 specified, then the actual move jobs were not submitted and the job IDs
1471 @raises GanetiApiError: if an iallocator and remote_node are both
1475 if iallocator and remote_node:
1476 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1479 _AppendDryRunIf(query, dry_run)
1481 if _NODE_EVAC_RES1 in self.GetFeatures():
1482 # Server supports body parameters
1485 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1486 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1487 _SetItemIf(body, early_release is not None,
1488 "early_release", early_release)
1489 _SetItemIf(body, mode is not None, "mode", mode)
1491 # Pre-2.5 request format
1495 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1496 " not accept old-style results (parameter"
1499 # Pre-2.5 servers can only evacuate secondaries
1500 if mode is not None and mode != NODE_EVAC_SEC:
1501 raise GanetiApiError("Server can only evacuate secondary instances")
1503 _AppendIf(query, iallocator, ("iallocator", iallocator))
1504 _AppendIf(query, remote_node, ("remote_node", remote_node))
1505 _AppendIf(query, early_release, ("early_release", 1))
1507 return self._SendRequest(HTTP_POST,
1508 ("/%s/nodes/%s/evacuate" %
1509 (GANETI_RAPI_VERSION, node)), query, body)
1511 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1513 """Migrates all primary instances from a node.
1516 @param node: node to migrate
1518 @param mode: if passed, it will overwrite the live migration type,
1519 otherwise the hypervisor default will be used
1521 @param dry_run: whether to perform a dry run
1522 @type iallocator: string
1523 @param iallocator: instance allocator to use
1524 @type target_node: string
1525 @param target_node: Target node for shared-storage instances
1532 _AppendDryRunIf(query, dry_run)
1534 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1537 _SetItemIf(body, mode is not None, "mode", mode)
1538 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1539 _SetItemIf(body, target_node is not None, "target_node", target_node)
1541 assert len(query) <= 1
1543 return self._SendRequest(HTTP_POST,
1544 ("/%s/nodes/%s/migrate" %
1545 (GANETI_RAPI_VERSION, node)), query, body)
1547 # Use old request format
1548 if target_node is not None:
1549 raise GanetiApiError("Server does not support specifying target node"
1550 " for node migration")
1552 _AppendIf(query, mode is not None, ("mode", mode))
1554 return self._SendRequest(HTTP_POST,
1555 ("/%s/nodes/%s/migrate" %
1556 (GANETI_RAPI_VERSION, node)), query, None)
1558 def GetNodeRole(self, node):
1559 """Gets the current role for a node.
1562 @param node: node whose role to return
1565 @return: the current role for a node
1568 return self._SendRequest(HTTP_GET,
1569 ("/%s/nodes/%s/role" %
1570 (GANETI_RAPI_VERSION, node)), None, None)
1572 def SetNodeRole(self, node, role, force=False, auto_promote=None):
1573 """Sets the role for a node.
1576 @param node: the node whose role to set
1578 @param role: the role to set for the node
1580 @param force: whether to force the role change
1581 @type auto_promote: bool
1582 @param auto_promote: Whether node(s) should be promoted to master candidate
1590 _AppendForceIf(query, force)
1591 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1593 return self._SendRequest(HTTP_PUT,
1594 ("/%s/nodes/%s/role" %
1595 (GANETI_RAPI_VERSION, node)), query, role)
1597 def PowercycleNode(self, node, force=False):
1598 """Powercycles a node.
1601 @param node: Node name
1603 @param force: Whether to force the operation
1609 _AppendForceIf(query, force)
1611 return self._SendRequest(HTTP_POST,
1612 ("/%s/nodes/%s/powercycle" %
1613 (GANETI_RAPI_VERSION, node)), query, None)
1615 def ModifyNode(self, node, **kwargs):
1618 More details for parameters can be found in the RAPI documentation.
1621 @param node: Node name
1626 return self._SendRequest(HTTP_POST,
1627 ("/%s/nodes/%s/modify" %
1628 (GANETI_RAPI_VERSION, node)), None, kwargs)
1630 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1631 """Gets the storage units for a node.
1634 @param node: the node whose storage units to return
1635 @type storage_type: str
1636 @param storage_type: storage type whose units to return
1637 @type output_fields: str
1638 @param output_fields: storage type fields to return
1641 @return: job id where results can be retrieved
1645 ("storage_type", storage_type),
1646 ("output_fields", output_fields),
1649 return self._SendRequest(HTTP_GET,
1650 ("/%s/nodes/%s/storage" %
1651 (GANETI_RAPI_VERSION, node)), query, None)
1653 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1654 """Modifies parameters of storage units on the node.
1657 @param node: node whose storage units to modify
1658 @type storage_type: str
1659 @param storage_type: storage type whose units to modify
1661 @param name: name of the storage unit
1662 @type allocatable: bool or None
1663 @param allocatable: Whether to set the "allocatable" flag on the storage
1664 unit (None=no modification, True=set, False=unset)
1671 ("storage_type", storage_type),
1675 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1677 return self._SendRequest(HTTP_PUT,
1678 ("/%s/nodes/%s/storage/modify" %
1679 (GANETI_RAPI_VERSION, node)), query, None)
1681 def RepairNodeStorageUnits(self, node, storage_type, name):
1682 """Repairs a storage unit on the node.
1685 @param node: node whose storage units to repair
1686 @type storage_type: str
1687 @param storage_type: storage type to repair
1689 @param name: name of the storage unit to repair
1696 ("storage_type", storage_type),
1700 return self._SendRequest(HTTP_PUT,
1701 ("/%s/nodes/%s/storage/repair" %
1702 (GANETI_RAPI_VERSION, node)), query, None)
1704 def GetNodeTags(self, node):
1705 """Gets the tags for a node.
1708 @param node: node whose tags to return
1711 @return: tags for the node
1714 return self._SendRequest(HTTP_GET,
1715 ("/%s/nodes/%s/tags" %
1716 (GANETI_RAPI_VERSION, node)), None, None)
1718 def AddNodeTags(self, node, tags, dry_run=False):
1719 """Adds tags to a node.
1722 @param node: node to add tags to
1723 @type tags: list of str
1724 @param tags: tags to add to 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_PUT,
1736 ("/%s/nodes/%s/tags" %
1737 (GANETI_RAPI_VERSION, node)), query, tags)
1739 def DeleteNodeTags(self, node, tags, dry_run=False):
1740 """Delete tags from a node.
1743 @param node: node to remove tags from
1744 @type tags: list of str
1745 @param tags: tags to remove from the node
1747 @param dry_run: whether to perform a dry run
1753 query = [("tag", t) for t in tags]
1754 _AppendDryRunIf(query, dry_run)
1756 return self._SendRequest(HTTP_DELETE,
1757 ("/%s/nodes/%s/tags" %
1758 (GANETI_RAPI_VERSION, node)), query, None)
1760 def GetNetworks(self, bulk=False):
1761 """Gets all networks in the cluster.
1764 @param bulk: whether to return all information about the networks
1766 @rtype: list of dict or str
1767 @return: if bulk is true, a list of dictionaries with info about all
1768 networks in the cluster, else a list of names of those networks
1772 _AppendIf(query, bulk, ("bulk", 1))
1774 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1779 return [n["name"] for n in networks]
1781 def GetNetwork(self, network):
1782 """Gets information about a network.
1785 @param network: name of the network whose info to return
1788 @return: info about the network
1791 return self._SendRequest(HTTP_GET,
1792 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1795 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1796 gateway6=None, mac_prefix=None,
1797 add_reserved_ips=None, tags=None, dry_run=False):
1798 """Creates a new network.
1800 @type network_name: str
1801 @param network_name: the name of network to create
1803 @param dry_run: whether to peform a dry run
1810 _AppendDryRunIf(query, dry_run)
1812 if add_reserved_ips:
1813 add_reserved_ips = add_reserved_ips.split(",")
1816 tags = tags.split(",")
1819 "network_name": network_name,
1822 "gateway6": gateway6,
1823 "network6": network6,
1824 "mac_prefix": mac_prefix,
1825 "add_reserved_ips": add_reserved_ips,
1829 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1832 def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
1833 """Connects a Network to a NodeGroup with the given netparams
1837 "group_name": group_name,
1838 "network_mode": mode,
1839 "network_link": link,
1843 _AppendDryRunIf(query, dry_run)
1845 return self._SendRequest(HTTP_PUT,
1846 ("/%s/networks/%s/connect" %
1847 (GANETI_RAPI_VERSION, network_name)), query, body)
1849 def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1850 """Connects a Network to a NodeGroup with the given netparams
1854 "group_name": group_name,
1858 _AppendDryRunIf(query, dry_run)
1860 return self._SendRequest(HTTP_PUT,
1861 ("/%s/networks/%s/disconnect" %
1862 (GANETI_RAPI_VERSION, network_name)), query, body)
1864 def ModifyNetwork(self, network, **kwargs):
1865 """Modifies a network.
1867 More details for parameters can be found in the RAPI documentation.
1869 @type network: string
1870 @param network: Network name
1875 return self._SendRequest(HTTP_PUT,
1876 ("/%s/networks/%s/modify" %
1877 (GANETI_RAPI_VERSION, network)), None, kwargs)
1879 def DeleteNetwork(self, network, dry_run=False):
1880 """Deletes a network.
1883 @param network: the network to delete
1885 @param dry_run: whether to peform a dry run
1892 _AppendDryRunIf(query, dry_run)
1894 return self._SendRequest(HTTP_DELETE,
1895 ("/%s/networks/%s" %
1896 (GANETI_RAPI_VERSION, network)), query, None)
1898 def GetNetworkTags(self, network):
1899 """Gets tags for a network.
1901 @type network: string
1902 @param network: Node group whose tags to return
1904 @rtype: list of strings
1905 @return: tags for the network
1908 return self._SendRequest(HTTP_GET,
1909 ("/%s/networks/%s/tags" %
1910 (GANETI_RAPI_VERSION, network)), None, None)
1912 def AddNetworkTags(self, network, tags, dry_run=False):
1913 """Adds tags to a network.
1916 @param network: network to add tags to
1917 @type tags: list of string
1918 @param tags: tags to add to the network
1920 @param dry_run: whether to perform a dry run
1926 query = [("tag", t) for t in tags]
1927 _AppendDryRunIf(query, dry_run)
1929 return self._SendRequest(HTTP_PUT,
1930 ("/%s/networks/%s/tags" %
1931 (GANETI_RAPI_VERSION, network)), query, None)
1933 def DeleteNetworkTags(self, network, tags, dry_run=False):
1934 """Deletes tags from a network.
1937 @param network: network to delete tags from
1938 @type tags: list of string
1939 @param tags: tags to delete
1941 @param dry_run: whether to perform a dry run
1946 query = [("tag", t) for t in tags]
1947 _AppendDryRunIf(query, dry_run)
1949 return self._SendRequest(HTTP_DELETE,
1950 ("/%s/networks/%s/tags" %
1951 (GANETI_RAPI_VERSION, network)), query, None)
1953 def GetGroups(self, bulk=False):
1954 """Gets all node groups in the cluster.
1957 @param bulk: whether to return all information about the groups
1959 @rtype: list of dict or str
1960 @return: if bulk is true, a list of dictionaries with info about all node
1961 groups in the cluster, else a list of names of those node groups
1965 _AppendIf(query, bulk, ("bulk", 1))
1967 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1972 return [g["name"] for g in groups]
1974 def GetGroup(self, group):
1975 """Gets information about a node group.
1978 @param group: name of the node group whose info to return
1981 @return: info about the node group
1984 return self._SendRequest(HTTP_GET,
1985 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1988 def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1989 """Creates a new node group.
1992 @param name: the name of node group to create
1993 @type alloc_policy: str
1994 @param alloc_policy: the desired allocation policy for the group, if any
1996 @param dry_run: whether to peform a dry run
2003 _AppendDryRunIf(query, dry_run)
2007 "alloc_policy": alloc_policy,
2010 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2013 def ModifyGroup(self, group, **kwargs):
2014 """Modifies a node group.
2016 More details for parameters can be found in the RAPI documentation.
2019 @param group: Node group name
2024 return self._SendRequest(HTTP_PUT,
2025 ("/%s/groups/%s/modify" %
2026 (GANETI_RAPI_VERSION, group)), None, kwargs)
2028 def DeleteGroup(self, group, dry_run=False):
2029 """Deletes a node group.
2032 @param group: the node group to delete
2034 @param dry_run: whether to peform a dry run
2041 _AppendDryRunIf(query, dry_run)
2043 return self._SendRequest(HTTP_DELETE,
2045 (GANETI_RAPI_VERSION, group)), query, None)
2047 def RenameGroup(self, group, new_name):
2048 """Changes the name of a node group.
2051 @param group: Node group name
2052 @type new_name: string
2053 @param new_name: New node group name
2060 "new_name": new_name,
2063 return self._SendRequest(HTTP_PUT,
2064 ("/%s/groups/%s/rename" %
2065 (GANETI_RAPI_VERSION, group)), None, body)
2067 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2068 """Assigns nodes to a group.
2071 @param group: Node group name
2072 @type nodes: list of strings
2073 @param nodes: List of nodes to assign to the group
2080 _AppendForceIf(query, force)
2081 _AppendDryRunIf(query, dry_run)
2087 return self._SendRequest(HTTP_PUT,
2088 ("/%s/groups/%s/assign-nodes" %
2089 (GANETI_RAPI_VERSION, group)), query, body)
2091 def GetGroupTags(self, group):
2092 """Gets tags for a node group.
2095 @param group: Node group whose tags to return
2097 @rtype: list of strings
2098 @return: tags for the group
2101 return self._SendRequest(HTTP_GET,
2102 ("/%s/groups/%s/tags" %
2103 (GANETI_RAPI_VERSION, group)), None, None)
2105 def AddGroupTags(self, group, tags, dry_run=False):
2106 """Adds tags to a node group.
2109 @param group: group to add tags to
2110 @type tags: list of string
2111 @param tags: tags to add to the group
2113 @param dry_run: whether to perform a dry run
2119 query = [("tag", t) for t in tags]
2120 _AppendDryRunIf(query, dry_run)
2122 return self._SendRequest(HTTP_PUT,
2123 ("/%s/groups/%s/tags" %
2124 (GANETI_RAPI_VERSION, group)), query, None)
2126 def DeleteGroupTags(self, group, tags, dry_run=False):
2127 """Deletes tags from a node group.
2130 @param group: group to delete tags from
2131 @type tags: list of string
2132 @param tags: tags to delete
2134 @param dry_run: whether to perform a dry run
2139 query = [("tag", t) for t in tags]
2140 _AppendDryRunIf(query, dry_run)
2142 return self._SendRequest(HTTP_DELETE,
2143 ("/%s/groups/%s/tags" %
2144 (GANETI_RAPI_VERSION, group)), query, None)
2146 def Query(self, what, fields, qfilter=None):
2147 """Retrieves information about resources.
2150 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2151 @type fields: list of string
2152 @param fields: Requested fields
2153 @type qfilter: None or list
2154 @param qfilter: Query filter
2164 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2165 # TODO: remove "filter" after 2.7
2166 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2168 return self._SendRequest(HTTP_PUT,
2170 (GANETI_RAPI_VERSION, what)), None, body)
2172 def QueryFields(self, what, fields=None):
2173 """Retrieves available fields for a resource.
2176 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2177 @type fields: list of string
2178 @param fields: Requested fields
2186 if fields is not None:
2187 _AppendIf(query, True, ("fields", ",".join(fields)))
2189 return self._SendRequest(HTTP_GET,
2190 ("/%s/query/%s/fields" %
2191 (GANETI_RAPI_VERSION, what)), query, None)