4 # Copyright (C) 2010, 2011 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_ROLE_DRAINED = "drained"
67 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
68 NODE_ROLE_MASTER = "master"
69 NODE_ROLE_OFFLINE = "offline"
70 NODE_ROLE_REGULAR = "regular"
72 JOB_STATUS_QUEUED = "queued"
73 JOB_STATUS_WAITING = "waiting"
74 JOB_STATUS_CANCELING = "canceling"
75 JOB_STATUS_RUNNING = "running"
76 JOB_STATUS_CANCELED = "canceled"
77 JOB_STATUS_SUCCESS = "success"
78 JOB_STATUS_ERROR = "error"
79 JOB_STATUS_FINALIZED = frozenset([
84 JOB_STATUS_ALL = frozenset([
89 ]) | JOB_STATUS_FINALIZED
92 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
95 _REQ_DATA_VERSION_FIELD = "__version__"
96 _INST_CREATE_REQV1 = "instance-create-reqv1"
97 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
98 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
99 _NODE_EVAC_RES1 = "node-evac-res1"
100 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"])
101 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
102 _INST_CREATE_V0_PARAMS = frozenset([
103 "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
104 "hypervisor", "file_storage_dir", "file_driver", "dry_run",
106 _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
108 # Older pycURL versions don't have all error constants
110 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
111 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
112 except AttributeError:
113 _CURLE_SSL_CACERT = 60
114 _CURLE_SSL_CACERT_BADFILE = 77
116 _CURL_SSL_CERT_ERRORS = frozenset([
118 _CURLE_SSL_CACERT_BADFILE,
122 class Error(Exception):
123 """Base error class for this module.
129 class CertificateError(Error):
130 """Raised when a problem is found with the SSL certificate.
136 class GanetiApiError(Error):
137 """Generic error raised from Ganeti API.
140 def __init__(self, msg, code=None):
141 Error.__init__(self, msg)
145 def UsesRapiClient(fn):
146 """Decorator for code using RAPI client to initialize pycURL.
149 def wrapper(*args, **kwargs):
150 # curl_global_init(3) and curl_global_cleanup(3) must be called with only
151 # one thread running. This check is just a safety measure -- it doesn't
153 assert threading.activeCount() == 1, \
154 "Found active threads when initializing pycURL"
156 pycurl.global_init(pycurl.GLOBAL_ALL)
158 return fn(*args, **kwargs)
160 pycurl.global_cleanup()
165 def GenericCurlConfig(verbose=False, use_signal=False,
166 use_curl_cabundle=False, cafile=None, capath=None,
167 proxy=None, verify_hostname=False,
168 connect_timeout=None, timeout=None,
169 _pycurl_version_fn=pycurl.version_info):
170 """Curl configuration function generator.
173 @param verbose: Whether to set cURL to verbose mode
174 @type use_signal: bool
175 @param use_signal: Whether to allow cURL to use signals
176 @type use_curl_cabundle: bool
177 @param use_curl_cabundle: Whether to use cURL's default CA bundle
179 @param cafile: In which file we can find the certificates
181 @param capath: In which directory we can find the certificates
183 @param proxy: Proxy to use, None for default behaviour and empty string for
184 disabling proxies (see curl_easy_setopt(3))
185 @type verify_hostname: bool
186 @param verify_hostname: Whether to verify the remote peer certificate's
188 @type connect_timeout: number
189 @param connect_timeout: Timeout for establishing connection in seconds
190 @type timeout: number
191 @param timeout: Timeout for complete transfer in seconds (see
192 curl_easy_setopt(3)).
195 if use_curl_cabundle and (cafile or capath):
196 raise Error("Can not use default CA bundle when CA file or path is set")
198 def _ConfigCurl(curl, logger):
199 """Configures a cURL object
201 @type curl: pycurl.Curl
202 @param curl: cURL object
205 logger.debug("Using cURL version %s", pycurl.version)
207 # pycurl.version_info returns a tuple with information about the used
208 # version of libcurl. Item 5 is the SSL library linked to it.
209 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
211 sslver = _pycurl_version_fn()[5]
213 raise Error("No SSL support in cURL")
215 lcsslver = sslver.lower()
216 if lcsslver.startswith("openssl/"):
218 elif lcsslver.startswith("gnutls/"):
220 raise Error("cURL linked against GnuTLS has no support for a"
221 " CA path (%s)" % (pycurl.version, ))
223 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
226 curl.setopt(pycurl.VERBOSE, verbose)
227 curl.setopt(pycurl.NOSIGNAL, not use_signal)
229 # Whether to verify remote peer's CN
231 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
232 # certificate must indicate that the server is the server to which you
233 # meant to connect, or the connection fails. [...] When the value is 1,
234 # the certificate must contain a Common Name field, but it doesn't matter
235 # what name it says. [...]"
236 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
238 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
240 if cafile or capath or use_curl_cabundle:
241 # Require certificates to be checked
242 curl.setopt(pycurl.SSL_VERIFYPEER, True)
244 curl.setopt(pycurl.CAINFO, str(cafile))
246 curl.setopt(pycurl.CAPATH, str(capath))
247 # Not changing anything for using default CA bundle
249 # Disable SSL certificate verification
250 curl.setopt(pycurl.SSL_VERIFYPEER, False)
252 if proxy is not None:
253 curl.setopt(pycurl.PROXY, str(proxy))
256 if connect_timeout is not None:
257 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
258 if timeout is not None:
259 curl.setopt(pycurl.TIMEOUT, timeout)
264 class GanetiRapiClient(object): # pylint: disable=R0904
265 """Ganeti RAPI client.
268 USER_AGENT = "Ganeti RAPI Client"
269 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
271 def __init__(self, host, port=GANETI_RAPI_PORT,
272 username=None, password=None, logger=logging,
273 curl_config_fn=None, curl_factory=None):
274 """Initializes this class.
277 @param host: the ganeti cluster master to interact with
279 @param port: the port on which the RAPI is running (default is 5080)
280 @type username: string
281 @param username: the username to connect with
282 @type password: string
283 @param password: the password to connect with
284 @type curl_config_fn: callable
285 @param curl_config_fn: Function to configure C{pycurl.Curl} object
286 @param logger: Logging object
289 self._username = username
290 self._password = password
291 self._logger = logger
292 self._curl_config_fn = curl_config_fn
293 self._curl_factory = curl_factory
296 socket.inet_pton(socket.AF_INET6, host)
297 address = "[%s]:%s" % (host, port)
299 address = "%s:%s" % (host, port)
301 self._base_url = "https://%s" % address
303 if username is not None:
305 raise Error("Password not specified")
307 raise Error("Specified password without username")
309 def _CreateCurl(self):
310 """Creates a cURL object.
313 # Create pycURL object if no factory is provided
314 if self._curl_factory:
315 curl = self._curl_factory()
319 # Default cURL settings
320 curl.setopt(pycurl.VERBOSE, False)
321 curl.setopt(pycurl.FOLLOWLOCATION, False)
322 curl.setopt(pycurl.MAXREDIRS, 5)
323 curl.setopt(pycurl.NOSIGNAL, True)
324 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
325 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
326 curl.setopt(pycurl.SSL_VERIFYPEER, False)
327 curl.setopt(pycurl.HTTPHEADER, [
328 "Accept: %s" % HTTP_APP_JSON,
329 "Content-type: %s" % HTTP_APP_JSON,
332 assert ((self._username is None and self._password is None) ^
333 (self._username is not None and self._password is not None))
336 # Setup authentication
337 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
338 curl.setopt(pycurl.USERPWD,
339 str("%s:%s" % (self._username, self._password)))
341 # Call external configuration function
342 if self._curl_config_fn:
343 self._curl_config_fn(curl, self._logger)
348 def _EncodeQuery(query):
349 """Encode query values for RAPI URL.
351 @type query: list of two-tuples
352 @param query: Query arguments
354 @return: Query list with encoded values
359 for name, value in query:
361 result.append((name, ""))
363 elif isinstance(value, bool):
364 # Boolean values must be encoded as 0 or 1
365 result.append((name, int(value)))
367 elif isinstance(value, (list, tuple, dict)):
368 raise ValueError("Invalid query data type %r" % type(value).__name__)
371 result.append((name, value))
375 def _SendRequest(self, method, path, query, content):
376 """Sends an HTTP request.
378 This constructs a full URL, encodes and decodes HTTP bodies, and
379 handles invalid responses in a pythonic way.
382 @param method: HTTP method to use
384 @param path: HTTP URL path
385 @type query: list of two-tuples
386 @param query: query arguments to pass to urllib.urlencode
387 @type content: str or None
388 @param content: HTTP body content
391 @return: JSON-Decoded response
393 @raises CertificateError: If an invalid SSL certificate is found
394 @raises GanetiApiError: If an invalid response is returned
397 assert path.startswith("/")
399 curl = self._CreateCurl()
401 if content is not None:
402 encoded_content = self._json_encoder.encode(content)
407 urlparts = [self._base_url, path]
410 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
412 url = "".join(urlparts)
414 self._logger.debug("Sending request %s %s (content=%r)",
415 method, url, encoded_content)
417 # Buffer for response
418 encoded_resp_body = StringIO()
421 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
422 curl.setopt(pycurl.URL, str(url))
423 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
424 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
427 # Send request and wait for response
430 except pycurl.error, err:
431 if err.args[0] in _CURL_SSL_CERT_ERRORS:
432 raise CertificateError("SSL certificate error %s" % err)
434 raise GanetiApiError(str(err))
436 # Reset settings to not keep references to large objects in memory
438 curl.setopt(pycurl.POSTFIELDS, "")
439 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
441 # Get HTTP response code
442 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
444 # Was anything written to the response buffer?
445 if encoded_resp_body.tell():
446 response_content = simplejson.loads(encoded_resp_body.getvalue())
448 response_content = None
450 if http_code != HTTP_OK:
451 if isinstance(response_content, dict):
453 (response_content["code"],
454 response_content["message"],
455 response_content["explain"]))
457 msg = str(response_content)
459 raise GanetiApiError(msg, code=http_code)
461 return response_content
463 def GetVersion(self):
464 """Gets the Remote API version running on the cluster.
467 @return: Ganeti Remote API version
470 return self._SendRequest(HTTP_GET, "/version", None, None)
472 def GetFeatures(self):
473 """Gets the list of optional features supported by RAPI server.
476 @return: List of optional features
480 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
482 except GanetiApiError, err:
483 # Older RAPI servers don't support this resource
484 if err.code == HTTP_NOT_FOUND:
489 def GetOperatingSystems(self):
490 """Gets the Operating Systems running in the Ganeti cluster.
493 @return: operating systems
496 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
500 """Gets info about the cluster.
503 @return: information about the cluster
506 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
509 def RedistributeConfig(self):
510 """Tells the cluster to redistribute its configuration files.
516 return self._SendRequest(HTTP_PUT,
517 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
520 def ModifyCluster(self, **kwargs):
521 """Modifies cluster parameters.
523 More details for parameters can be found in the RAPI documentation.
531 return self._SendRequest(HTTP_PUT,
532 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
534 def GetClusterTags(self):
535 """Gets the cluster tags.
538 @return: cluster tags
541 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
544 def AddClusterTags(self, tags, dry_run=False):
545 """Adds tags to the cluster.
547 @type tags: list of str
548 @param tags: tags to add to the cluster
550 @param dry_run: whether to perform a dry run
556 query = [("tag", t) for t in tags]
558 query.append(("dry-run", 1))
560 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
563 def DeleteClusterTags(self, tags, dry_run=False):
564 """Deletes tags from the cluster.
566 @type tags: list of str
567 @param tags: tags to delete
569 @param dry_run: whether to perform a dry run
574 query = [("tag", t) for t in tags]
576 query.append(("dry-run", 1))
578 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
581 def GetInstances(self, bulk=False):
582 """Gets information about instances on the cluster.
585 @param bulk: whether to return all information about all instances
587 @rtype: list of dict or list of str
588 @return: if bulk is True, info about the instances, else a list of instances
593 query.append(("bulk", 1))
595 instances = self._SendRequest(HTTP_GET,
596 "/%s/instances" % GANETI_RAPI_VERSION,
601 return [i["id"] for i in instances]
603 def GetInstance(self, instance):
604 """Gets information about an instance.
607 @param instance: instance whose info to return
610 @return: info about the instance
613 return self._SendRequest(HTTP_GET,
614 ("/%s/instances/%s" %
615 (GANETI_RAPI_VERSION, instance)), None, None)
617 def GetInstanceInfo(self, instance, static=None):
618 """Gets information about an instance.
620 @type instance: string
621 @param instance: Instance name
626 if static is not None:
627 query = [("static", static)]
631 return self._SendRequest(HTTP_GET,
632 ("/%s/instances/%s/info" %
633 (GANETI_RAPI_VERSION, instance)), query, None)
635 def CreateInstance(self, mode, name, disk_template, disks, nics,
637 """Creates a new instance.
639 More details for parameters can be found in the RAPI documentation.
642 @param mode: Instance creation mode
644 @param name: Hostname of the instance to create
645 @type disk_template: string
646 @param disk_template: Disk template for instance (e.g. plain, diskless,
648 @type disks: list of dicts
649 @param disks: List of disk definitions
650 @type nics: list of dicts
651 @param nics: List of NIC definitions
653 @keyword dry_run: whether to perform a dry run
661 if kwargs.get("dry_run"):
662 query.append(("dry-run", 1))
664 if _INST_CREATE_REQV1 in self.GetFeatures():
665 # All required fields for request data version 1
667 _REQ_DATA_VERSION_FIELD: 1,
670 "disk_template": disk_template,
675 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
677 raise GanetiApiError("Required fields can not be specified as"
678 " keywords: %s" % ", ".join(conflicts))
680 body.update((key, value) for key, value in kwargs.iteritems()
683 raise GanetiApiError("Server does not support new-style (version 1)"
684 " instance creation requests")
686 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
689 def DeleteInstance(self, instance, dry_run=False):
690 """Deletes an instance.
693 @param instance: the instance to delete
701 query.append(("dry-run", 1))
703 return self._SendRequest(HTTP_DELETE,
704 ("/%s/instances/%s" %
705 (GANETI_RAPI_VERSION, instance)), query, None)
707 def ModifyInstance(self, instance, **kwargs):
708 """Modifies an instance.
710 More details for parameters can be found in the RAPI documentation.
712 @type instance: string
713 @param instance: Instance name
720 return self._SendRequest(HTTP_PUT,
721 ("/%s/instances/%s/modify" %
722 (GANETI_RAPI_VERSION, instance)), None, body)
724 def ActivateInstanceDisks(self, instance, ignore_size=None):
725 """Activates an instance's disks.
727 @type instance: string
728 @param instance: Instance name
729 @type ignore_size: bool
730 @param ignore_size: Whether to ignore recorded size
737 query.append(("ignore_size", 1))
739 return self._SendRequest(HTTP_PUT,
740 ("/%s/instances/%s/activate-disks" %
741 (GANETI_RAPI_VERSION, instance)), query, None)
743 def DeactivateInstanceDisks(self, instance):
744 """Deactivates an instance's disks.
746 @type instance: string
747 @param instance: Instance name
752 return self._SendRequest(HTTP_PUT,
753 ("/%s/instances/%s/deactivate-disks" %
754 (GANETI_RAPI_VERSION, instance)), None, None)
756 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
757 """Grows a disk of an instance.
759 More details for parameters can be found in the RAPI documentation.
761 @type instance: string
762 @param instance: Instance name
764 @param disk: Disk index
765 @type amount: integer
766 @param amount: Grow disk by this amount (MiB)
767 @type wait_for_sync: bool
768 @param wait_for_sync: Wait for disk to synchronize
777 if wait_for_sync is not None:
778 body["wait_for_sync"] = wait_for_sync
780 return self._SendRequest(HTTP_POST,
781 ("/%s/instances/%s/disk/%s/grow" %
782 (GANETI_RAPI_VERSION, instance, disk)),
785 def GetInstanceTags(self, instance):
786 """Gets tags for an instance.
789 @param instance: instance whose tags to return
792 @return: tags for the instance
795 return self._SendRequest(HTTP_GET,
796 ("/%s/instances/%s/tags" %
797 (GANETI_RAPI_VERSION, instance)), None, None)
799 def AddInstanceTags(self, instance, tags, dry_run=False):
800 """Adds tags to an instance.
803 @param instance: instance to add tags to
804 @type tags: list of str
805 @param tags: tags to add to the instance
807 @param dry_run: whether to perform a dry run
813 query = [("tag", t) for t in tags]
815 query.append(("dry-run", 1))
817 return self._SendRequest(HTTP_PUT,
818 ("/%s/instances/%s/tags" %
819 (GANETI_RAPI_VERSION, instance)), query, None)
821 def DeleteInstanceTags(self, instance, tags, dry_run=False):
822 """Deletes tags from an instance.
825 @param instance: instance to delete tags from
826 @type tags: list of str
827 @param tags: tags to delete
829 @param dry_run: whether to perform a dry run
834 query = [("tag", t) for t in tags]
836 query.append(("dry-run", 1))
838 return self._SendRequest(HTTP_DELETE,
839 ("/%s/instances/%s/tags" %
840 (GANETI_RAPI_VERSION, instance)), query, None)
842 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
844 """Reboots an instance.
847 @param instance: instance to rebot
848 @type reboot_type: str
849 @param reboot_type: one of: hard, soft, full
850 @type ignore_secondaries: bool
851 @param ignore_secondaries: if True, ignores errors for the secondary node
852 while re-assembling disks (in hard-reboot mode only)
854 @param dry_run: whether to perform a dry run
861 query.append(("type", reboot_type))
862 if ignore_secondaries is not None:
863 query.append(("ignore_secondaries", ignore_secondaries))
865 query.append(("dry-run", 1))
867 return self._SendRequest(HTTP_POST,
868 ("/%s/instances/%s/reboot" %
869 (GANETI_RAPI_VERSION, instance)), query, None)
871 def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
872 """Shuts down an instance.
875 @param instance: the instance to shut down
877 @param dry_run: whether to perform a dry run
878 @type no_remember: bool
879 @param no_remember: if true, will not record the state change
886 query.append(("dry-run", 1))
888 query.append(("no-remember", 1))
890 return self._SendRequest(HTTP_PUT,
891 ("/%s/instances/%s/shutdown" %
892 (GANETI_RAPI_VERSION, instance)), query, None)
894 def StartupInstance(self, instance, dry_run=False, no_remember=False):
895 """Starts up an instance.
898 @param instance: the instance to start up
900 @param dry_run: whether to perform a dry run
901 @type no_remember: bool
902 @param no_remember: if true, will not record the state change
909 query.append(("dry-run", 1))
911 query.append(("no-remember", 1))
913 return self._SendRequest(HTTP_PUT,
914 ("/%s/instances/%s/startup" %
915 (GANETI_RAPI_VERSION, instance)), query, None)
917 def ReinstallInstance(self, instance, os=None, no_startup=False,
919 """Reinstalls an instance.
922 @param instance: The instance to reinstall
923 @type os: str or None
924 @param os: The operating system to reinstall. If None, the instance's
925 current operating system will be installed again
926 @type no_startup: bool
927 @param no_startup: Whether to start the instance automatically
932 if _INST_REINSTALL_REQV1 in self.GetFeatures():
934 "start": not no_startup,
938 if osparams is not None:
939 body["osparams"] = osparams
940 return self._SendRequest(HTTP_POST,
941 ("/%s/instances/%s/reinstall" %
942 (GANETI_RAPI_VERSION, instance)), None, body)
944 # Use old request format
946 raise GanetiApiError("Server does not support specifying OS parameters"
947 " for instance reinstallation")
951 query.append(("os", os))
953 query.append(("nostartup", 1))
954 return self._SendRequest(HTTP_POST,
955 ("/%s/instances/%s/reinstall" %
956 (GANETI_RAPI_VERSION, instance)), query, None)
958 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
959 remote_node=None, iallocator=None, dry_run=False):
960 """Replaces disks on an instance.
963 @param instance: instance whose disks to replace
964 @type disks: list of ints
965 @param disks: Indexes of disks to replace
967 @param mode: replacement mode to use (defaults to replace_auto)
968 @type remote_node: str or None
969 @param remote_node: new secondary node to use (for use with
970 replace_new_secondary mode)
971 @type iallocator: str or None
972 @param iallocator: instance allocator plugin to use (for use with
975 @param dry_run: whether to perform a dry run
986 query.append(("disks", ",".join(str(idx) for idx in disks)))
989 query.append(("remote_node", remote_node))
992 query.append(("iallocator", iallocator))
995 query.append(("dry-run", 1))
997 return self._SendRequest(HTTP_POST,
998 ("/%s/instances/%s/replace-disks" %
999 (GANETI_RAPI_VERSION, instance)), query, None)
1001 def PrepareExport(self, instance, mode):
1002 """Prepares an instance for an export.
1004 @type instance: string
1005 @param instance: Instance name
1007 @param mode: Export mode
1012 query = [("mode", mode)]
1013 return self._SendRequest(HTTP_PUT,
1014 ("/%s/instances/%s/prepare-export" %
1015 (GANETI_RAPI_VERSION, instance)), query, None)
1017 def ExportInstance(self, instance, mode, destination, shutdown=None,
1018 remove_instance=None,
1019 x509_key_name=None, destination_x509_ca=None):
1020 """Exports an instance.
1022 @type instance: string
1023 @param instance: Instance name
1025 @param mode: Export mode
1031 "destination": destination,
1035 if shutdown is not None:
1036 body["shutdown"] = shutdown
1038 if remove_instance is not None:
1039 body["remove_instance"] = remove_instance
1041 if x509_key_name is not None:
1042 body["x509_key_name"] = x509_key_name
1044 if destination_x509_ca is not None:
1045 body["destination_x509_ca"] = destination_x509_ca
1047 return self._SendRequest(HTTP_PUT,
1048 ("/%s/instances/%s/export" %
1049 (GANETI_RAPI_VERSION, instance)), None, body)
1051 def MigrateInstance(self, instance, mode=None, cleanup=None):
1052 """Migrates an instance.
1054 @type instance: string
1055 @param instance: Instance name
1057 @param mode: Migration mode
1059 @param cleanup: Whether to clean up a previously failed migration
1066 if mode is not None:
1069 if cleanup is not None:
1070 body["cleanup"] = cleanup
1072 return self._SendRequest(HTTP_PUT,
1073 ("/%s/instances/%s/migrate" %
1074 (GANETI_RAPI_VERSION, instance)), None, body)
1076 def FailoverInstance(self, instance, iallocator=None,
1077 ignore_consistency=None, target_node=None):
1078 """Does a failover of an instance.
1080 @type instance: string
1081 @param instance: Instance name
1082 @type iallocator: string
1083 @param iallocator: Iallocator for deciding the target node for
1084 shared-storage instances
1085 @type ignore_consistency: bool
1086 @param ignore_consistency: Whether to ignore disk consistency
1087 @type target_node: string
1088 @param target_node: Target node for shared-storage instances
1095 if iallocator is not None:
1096 body["iallocator"] = iallocator
1098 if ignore_consistency is not None:
1099 body["ignore_consistency"] = ignore_consistency
1101 if target_node is not None:
1102 body["target_node"] = target_node
1104 return self._SendRequest(HTTP_PUT,
1105 ("/%s/instances/%s/failover" %
1106 (GANETI_RAPI_VERSION, instance)), None, body)
1108 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1109 """Changes the name of an instance.
1111 @type instance: string
1112 @param instance: Instance name
1113 @type new_name: string
1114 @param new_name: New instance name
1115 @type ip_check: bool
1116 @param ip_check: Whether to ensure instance's IP address is inactive
1117 @type name_check: bool
1118 @param name_check: Whether to ensure instance's name is resolvable
1124 "new_name": new_name,
1127 if ip_check is not None:
1128 body["ip_check"] = ip_check
1130 if name_check is not None:
1131 body["name_check"] = name_check
1133 return self._SendRequest(HTTP_PUT,
1134 ("/%s/instances/%s/rename" %
1135 (GANETI_RAPI_VERSION, instance)), None, body)
1137 def GetInstanceConsole(self, instance):
1138 """Request information for connecting to instance's console.
1140 @type instance: string
1141 @param instance: Instance name
1143 @return: dictionary containing information about instance's console
1146 return self._SendRequest(HTTP_GET,
1147 ("/%s/instances/%s/console" %
1148 (GANETI_RAPI_VERSION, instance)), None, None)
1151 """Gets all jobs for the cluster.
1154 @return: job ids for the cluster
1157 return [int(j["id"])
1158 for j in self._SendRequest(HTTP_GET,
1159 "/%s/jobs" % GANETI_RAPI_VERSION,
1162 def GetJobStatus(self, job_id):
1163 """Gets the status of a job.
1165 @type job_id: string
1166 @param job_id: job id whose status to query
1172 return self._SendRequest(HTTP_GET,
1173 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1176 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1177 """Polls cluster for job status until completion.
1179 Completion is defined as any of the following states listed in
1180 L{JOB_STATUS_FINALIZED}.
1182 @type job_id: string
1183 @param job_id: job id to watch
1185 @param period: how often to poll for status (optional, default 5s)
1187 @param retries: how many time to poll before giving up
1188 (optional, default -1 means unlimited)
1191 @return: C{True} if job succeeded or C{False} if failed/status timeout
1192 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1193 possible; L{WaitForJobChange} returns immediately after a job changed and
1194 does not use polling
1198 job_result = self.GetJobStatus(job_id)
1200 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1202 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1213 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1214 """Waits for job changes.
1216 @type job_id: string
1217 @param job_id: Job ID for which to wait
1218 @return: C{None} if no changes have been detected and a dict with two keys,
1219 C{job_info} and C{log_entries} otherwise.
1225 "previous_job_info": prev_job_info,
1226 "previous_log_serial": prev_log_serial,
1229 return self._SendRequest(HTTP_GET,
1230 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1233 def CancelJob(self, job_id, dry_run=False):
1236 @type job_id: string
1237 @param job_id: id of the job to delete
1239 @param dry_run: whether to perform a dry run
1241 @return: tuple containing the result, and a message (bool, string)
1246 query.append(("dry-run", 1))
1248 return self._SendRequest(HTTP_DELETE,
1249 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1252 def GetNodes(self, bulk=False):
1253 """Gets all nodes in the cluster.
1256 @param bulk: whether to return all information about all instances
1258 @rtype: list of dict or str
1259 @return: if bulk is true, info about nodes in the cluster,
1260 else list of nodes in the cluster
1265 query.append(("bulk", 1))
1267 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1272 return [n["id"] for n in nodes]
1274 def GetNode(self, node):
1275 """Gets information about a node.
1278 @param node: node whose info to return
1281 @return: info about the node
1284 return self._SendRequest(HTTP_GET,
1285 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1288 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1289 dry_run=False, early_release=None,
1290 primary=None, secondary=None, accept_old=False):
1291 """Evacuates instances from a Ganeti node.
1294 @param node: node to evacuate
1295 @type iallocator: str or None
1296 @param iallocator: instance allocator to use
1297 @type remote_node: str
1298 @param remote_node: node to evaucate to
1300 @param dry_run: whether to perform a dry run
1301 @type early_release: bool
1302 @param early_release: whether to enable parallelization
1304 @param primary: Whether to evacuate primary instances
1305 @type secondary: bool
1306 @param secondary: Whether to evacuate secondary instances
1307 @type accept_old: bool
1308 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1311 @rtype: string, or a list for pre-2.5 results
1312 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1313 list of (job ID, instance name, new secondary node); if dry_run was
1314 specified, then the actual move jobs were not submitted and the job IDs
1317 @raises GanetiApiError: if an iallocator and remote_node are both
1321 if iallocator and remote_node:
1322 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1326 query.append(("dry-run", 1))
1328 if _NODE_EVAC_RES1 in self.GetFeatures():
1331 if iallocator is not None:
1332 body["iallocator"] = iallocator
1333 if remote_node is not None:
1334 body["remote_node"] = remote_node
1335 if early_release is not None:
1336 body["early_release"] = early_release
1337 if primary is not None:
1338 body["primary"] = primary
1339 if secondary is not None:
1340 body["secondary"] = secondary
1342 # Pre-2.5 request format
1346 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1347 " not accept old-style results (parameter"
1350 if primary or primary is None or not (secondary is None or secondary):
1351 raise GanetiApiError("Server can only evacuate secondary instances")
1354 query.append(("iallocator", iallocator))
1356 query.append(("remote_node", remote_node))
1358 query.append(("early_release", 1))
1360 return self._SendRequest(HTTP_POST,
1361 ("/%s/nodes/%s/evacuate" %
1362 (GANETI_RAPI_VERSION, node)), query, body)
1364 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1366 """Migrates all primary instances from a node.
1369 @param node: node to migrate
1371 @param mode: if passed, it will overwrite the live migration type,
1372 otherwise the hypervisor default will be used
1374 @param dry_run: whether to perform a dry run
1375 @type iallocator: string
1376 @param iallocator: instance allocator to use
1377 @type target_node: string
1378 @param target_node: Target node for shared-storage instances
1386 query.append(("dry-run", 1))
1388 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1391 if mode is not None:
1393 if iallocator is not None:
1394 body["iallocator"] = iallocator
1395 if target_node is not None:
1396 body["target_node"] = target_node
1398 assert len(query) <= 1
1400 return self._SendRequest(HTTP_POST,
1401 ("/%s/nodes/%s/migrate" %
1402 (GANETI_RAPI_VERSION, node)), query, body)
1404 # Use old request format
1405 if target_node is not None:
1406 raise GanetiApiError("Server does not support specifying target node"
1407 " for node migration")
1409 if mode is not None:
1410 query.append(("mode", mode))
1412 return self._SendRequest(HTTP_POST,
1413 ("/%s/nodes/%s/migrate" %
1414 (GANETI_RAPI_VERSION, node)), query, None)
1416 def GetNodeRole(self, node):
1417 """Gets the current role for a node.
1420 @param node: node whose role to return
1423 @return: the current role for a node
1426 return self._SendRequest(HTTP_GET,
1427 ("/%s/nodes/%s/role" %
1428 (GANETI_RAPI_VERSION, node)), None, None)
1430 def SetNodeRole(self, node, role, force=False):
1431 """Sets the role for a node.
1434 @param node: the node whose role to set
1436 @param role: the role to set for the node
1438 @param force: whether to force the role change
1448 return self._SendRequest(HTTP_PUT,
1449 ("/%s/nodes/%s/role" %
1450 (GANETI_RAPI_VERSION, node)), query, role)
1452 def ModifyNode(self, group, **kwargs):
1455 More details for parameters can be found in the RAPI documentation.
1458 @param group: Node name
1463 return self._SendRequest(HTTP_POST,
1464 ("/%s/nodes/%s/modify" %
1465 (GANETI_RAPI_VERSION, group)), None, kwargs)
1467 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1468 """Gets the storage units for a node.
1471 @param node: the node whose storage units to return
1472 @type storage_type: str
1473 @param storage_type: storage type whose units to return
1474 @type output_fields: str
1475 @param output_fields: storage type fields to return
1478 @return: job id where results can be retrieved
1482 ("storage_type", storage_type),
1483 ("output_fields", output_fields),
1486 return self._SendRequest(HTTP_GET,
1487 ("/%s/nodes/%s/storage" %
1488 (GANETI_RAPI_VERSION, node)), query, None)
1490 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1491 """Modifies parameters of storage units on the node.
1494 @param node: node whose storage units to modify
1495 @type storage_type: str
1496 @param storage_type: storage type whose units to modify
1498 @param name: name of the storage unit
1499 @type allocatable: bool or None
1500 @param allocatable: Whether to set the "allocatable" flag on the storage
1501 unit (None=no modification, True=set, False=unset)
1508 ("storage_type", storage_type),
1512 if allocatable is not None:
1513 query.append(("allocatable", allocatable))
1515 return self._SendRequest(HTTP_PUT,
1516 ("/%s/nodes/%s/storage/modify" %
1517 (GANETI_RAPI_VERSION, node)), query, None)
1519 def RepairNodeStorageUnits(self, node, storage_type, name):
1520 """Repairs a storage unit on the node.
1523 @param node: node whose storage units to repair
1524 @type storage_type: str
1525 @param storage_type: storage type to repair
1527 @param name: name of the storage unit to repair
1534 ("storage_type", storage_type),
1538 return self._SendRequest(HTTP_PUT,
1539 ("/%s/nodes/%s/storage/repair" %
1540 (GANETI_RAPI_VERSION, node)), query, None)
1542 def GetNodeTags(self, node):
1543 """Gets the tags for a node.
1546 @param node: node whose tags to return
1549 @return: tags for the node
1552 return self._SendRequest(HTTP_GET,
1553 ("/%s/nodes/%s/tags" %
1554 (GANETI_RAPI_VERSION, node)), None, None)
1556 def AddNodeTags(self, node, tags, dry_run=False):
1557 """Adds tags to a node.
1560 @param node: node to add tags to
1561 @type tags: list of str
1562 @param tags: tags to add to the node
1564 @param dry_run: whether to perform a dry run
1570 query = [("tag", t) for t in tags]
1572 query.append(("dry-run", 1))
1574 return self._SendRequest(HTTP_PUT,
1575 ("/%s/nodes/%s/tags" %
1576 (GANETI_RAPI_VERSION, node)), query, tags)
1578 def DeleteNodeTags(self, node, tags, dry_run=False):
1579 """Delete tags from a node.
1582 @param node: node to remove tags from
1583 @type tags: list of str
1584 @param tags: tags to remove from the node
1586 @param dry_run: whether to perform a dry run
1592 query = [("tag", t) for t in tags]
1594 query.append(("dry-run", 1))
1596 return self._SendRequest(HTTP_DELETE,
1597 ("/%s/nodes/%s/tags" %
1598 (GANETI_RAPI_VERSION, node)), query, None)
1600 def GetGroups(self, bulk=False):
1601 """Gets all node groups in the cluster.
1604 @param bulk: whether to return all information about the groups
1606 @rtype: list of dict or str
1607 @return: if bulk is true, a list of dictionaries with info about all node
1608 groups in the cluster, else a list of names of those node groups
1613 query.append(("bulk", 1))
1615 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1620 return [g["name"] for g in groups]
1622 def GetGroup(self, group):
1623 """Gets information about a node group.
1626 @param group: name of the node group whose info to return
1629 @return: info about the node group
1632 return self._SendRequest(HTTP_GET,
1633 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1636 def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1637 """Creates a new node group.
1640 @param name: the name of node group to create
1641 @type alloc_policy: str
1642 @param alloc_policy: the desired allocation policy for the group, if any
1644 @param dry_run: whether to peform a dry run
1652 query.append(("dry-run", 1))
1656 "alloc_policy": alloc_policy
1659 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1662 def ModifyGroup(self, group, **kwargs):
1663 """Modifies a node group.
1665 More details for parameters can be found in the RAPI documentation.
1668 @param group: Node group name
1673 return self._SendRequest(HTTP_PUT,
1674 ("/%s/groups/%s/modify" %
1675 (GANETI_RAPI_VERSION, group)), None, kwargs)
1677 def DeleteGroup(self, group, dry_run=False):
1678 """Deletes a node group.
1681 @param group: the node group to delete
1683 @param dry_run: whether to peform a dry run
1691 query.append(("dry-run", 1))
1693 return self._SendRequest(HTTP_DELETE,
1695 (GANETI_RAPI_VERSION, group)), query, None)
1697 def RenameGroup(self, group, new_name):
1698 """Changes the name of a node group.
1701 @param group: Node group name
1702 @type new_name: string
1703 @param new_name: New node group name
1710 "new_name": new_name,
1713 return self._SendRequest(HTTP_PUT,
1714 ("/%s/groups/%s/rename" %
1715 (GANETI_RAPI_VERSION, group)), None, body)
1717 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1718 """Assigns nodes to a group.
1721 @param group: Node gropu name
1722 @type nodes: list of strings
1723 @param nodes: List of nodes to assign to the group
1732 query.append(("force", 1))
1735 query.append(("dry-run", 1))
1741 return self._SendRequest(HTTP_PUT,
1742 ("/%s/groups/%s/assign-nodes" %
1743 (GANETI_RAPI_VERSION, group)), query, body)
1745 def GetGroupTags(self, group):
1746 """Gets tags for a node group.
1749 @param group: Node group whose tags to return
1751 @rtype: list of strings
1752 @return: tags for the group
1755 return self._SendRequest(HTTP_GET,
1756 ("/%s/groups/%s/tags" %
1757 (GANETI_RAPI_VERSION, group)), None, None)
1759 def AddGroupTags(self, group, tags, dry_run=False):
1760 """Adds tags to a node group.
1763 @param group: group to add tags to
1764 @type tags: list of string
1765 @param tags: tags to add to the group
1767 @param dry_run: whether to perform a dry run
1773 query = [("tag", t) for t in tags]
1775 query.append(("dry-run", 1))
1777 return self._SendRequest(HTTP_PUT,
1778 ("/%s/groups/%s/tags" %
1779 (GANETI_RAPI_VERSION, group)), query, None)
1781 def DeleteGroupTags(self, group, tags, dry_run=False):
1782 """Deletes tags from a node group.
1785 @param group: group to delete tags from
1786 @type tags: list of string
1787 @param tags: tags to delete
1789 @param dry_run: whether to perform a dry run
1794 query = [("tag", t) for t in tags]
1796 query.append(("dry-run", 1))
1798 return self._SendRequest(HTTP_DELETE,
1799 ("/%s/groups/%s/tags" %
1800 (GANETI_RAPI_VERSION, group)), query, None)
1802 def Query(self, what, fields, filter_=None):
1803 """Retrieves information about resources.
1806 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1807 @type fields: list of string
1808 @param fields: Requested fields
1809 @type filter_: None or list
1810 @param filter_: Query filter
1820 if filter_ is not None:
1821 body["filter"] = filter_
1823 return self._SendRequest(HTTP_PUT,
1825 (GANETI_RAPI_VERSION, what)), None, body)
1827 def QueryFields(self, what, fields=None):
1828 """Retrieves available fields for a resource.
1831 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1832 @type fields: list of string
1833 @param fields: Requested fields
1841 if fields is not None:
1842 query.append(("fields", ",".join(fields)))
1844 return self._SendRequest(HTTP_GET,
1845 ("/%s/query/%s/fields" %
1846 (GANETI_RAPI_VERSION, what)), query, None)