4 # Copyright (C) 2010 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
43 from cStringIO import StringIO
45 from StringIO import StringIO
48 GANETI_RAPI_PORT = 5080
49 GANETI_RAPI_VERSION = 2
51 HTTP_DELETE = "DELETE"
57 HTTP_APP_JSON = "application/json"
59 REPLACE_DISK_PRI = "replace_on_primary"
60 REPLACE_DISK_SECONDARY = "replace_on_secondary"
61 REPLACE_DISK_CHG = "replace_new_secondary"
62 REPLACE_DISK_AUTO = "replace_auto"
64 NODE_ROLE_DRAINED = "drained"
65 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
66 NODE_ROLE_MASTER = "master"
67 NODE_ROLE_OFFLINE = "offline"
68 NODE_ROLE_REGULAR = "regular"
71 _REQ_DATA_VERSION_FIELD = "__version__"
72 _INST_CREATE_REQV1 = "instance-create-reqv1"
73 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
74 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
75 _INST_CREATE_V0_PARAMS = frozenset([
76 "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
77 "hypervisor", "file_storage_dir", "file_driver", "dry_run",
79 _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
81 # Older pycURL versions don't have all error constants
83 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
84 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
85 except AttributeError:
86 _CURLE_SSL_CACERT = 60
87 _CURLE_SSL_CACERT_BADFILE = 77
89 _CURL_SSL_CERT_ERRORS = frozenset([
91 _CURLE_SSL_CACERT_BADFILE,
95 class Error(Exception):
96 """Base error class for this module.
102 class CertificateError(Error):
103 """Raised when a problem is found with the SSL certificate.
109 class GanetiApiError(Error):
110 """Generic error raised from Ganeti API.
113 def __init__(self, msg, code=None):
114 Error.__init__(self, msg)
118 def UsesRapiClient(fn):
119 """Decorator for code using RAPI client to initialize pycURL.
122 def wrapper(*args, **kwargs):
123 # curl_global_init(3) and curl_global_cleanup(3) must be called with only
124 # one thread running. This check is just a safety measure -- it doesn't
126 assert threading.activeCount() == 1, \
127 "Found active threads when initializing pycURL"
129 pycurl.global_init(pycurl.GLOBAL_ALL)
131 return fn(*args, **kwargs)
133 pycurl.global_cleanup()
138 def GenericCurlConfig(verbose=False, use_signal=False,
139 use_curl_cabundle=False, cafile=None, capath=None,
140 proxy=None, verify_hostname=False,
141 connect_timeout=None, timeout=None,
142 _pycurl_version_fn=pycurl.version_info):
143 """Curl configuration function generator.
146 @param verbose: Whether to set cURL to verbose mode
147 @type use_signal: bool
148 @param use_signal: Whether to allow cURL to use signals
149 @type use_curl_cabundle: bool
150 @param use_curl_cabundle: Whether to use cURL's default CA bundle
152 @param cafile: In which file we can find the certificates
154 @param capath: In which directory we can find the certificates
156 @param proxy: Proxy to use, None for default behaviour and empty string for
157 disabling proxies (see curl_easy_setopt(3))
158 @type verify_hostname: bool
159 @param verify_hostname: Whether to verify the remote peer certificate's
161 @type connect_timeout: number
162 @param connect_timeout: Timeout for establishing connection in seconds
163 @type timeout: number
164 @param timeout: Timeout for complete transfer in seconds (see
165 curl_easy_setopt(3)).
168 if use_curl_cabundle and (cafile or capath):
169 raise Error("Can not use default CA bundle when CA file or path is set")
171 def _ConfigCurl(curl, logger):
172 """Configures a cURL object
174 @type curl: pycurl.Curl
175 @param curl: cURL object
178 logger.debug("Using cURL version %s", pycurl.version)
180 # pycurl.version_info returns a tuple with information about the used
181 # version of libcurl. Item 5 is the SSL library linked to it.
182 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
184 sslver = _pycurl_version_fn()[5]
186 raise Error("No SSL support in cURL")
188 lcsslver = sslver.lower()
189 if lcsslver.startswith("openssl/"):
191 elif lcsslver.startswith("gnutls/"):
193 raise Error("cURL linked against GnuTLS has no support for a"
194 " CA path (%s)" % (pycurl.version, ))
196 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
199 curl.setopt(pycurl.VERBOSE, verbose)
200 curl.setopt(pycurl.NOSIGNAL, not use_signal)
202 # Whether to verify remote peer's CN
204 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
205 # certificate must indicate that the server is the server to which you
206 # meant to connect, or the connection fails. [...] When the value is 1,
207 # the certificate must contain a Common Name field, but it doesn't matter
208 # what name it says. [...]"
209 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
211 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
213 if cafile or capath or use_curl_cabundle:
214 # Require certificates to be checked
215 curl.setopt(pycurl.SSL_VERIFYPEER, True)
217 curl.setopt(pycurl.CAINFO, str(cafile))
219 curl.setopt(pycurl.CAPATH, str(capath))
220 # Not changing anything for using default CA bundle
222 # Disable SSL certificate verification
223 curl.setopt(pycurl.SSL_VERIFYPEER, False)
225 if proxy is not None:
226 curl.setopt(pycurl.PROXY, str(proxy))
229 if connect_timeout is not None:
230 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
231 if timeout is not None:
232 curl.setopt(pycurl.TIMEOUT, timeout)
237 class GanetiRapiClient(object):
238 """Ganeti RAPI client.
241 USER_AGENT = "Ganeti RAPI Client"
242 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
244 def __init__(self, host, port=GANETI_RAPI_PORT,
245 username=None, password=None, logger=logging,
246 curl_config_fn=None, curl_factory=None):
247 """Initializes this class.
250 @param host: the ganeti cluster master to interact with
252 @param port: the port on which the RAPI is running (default is 5080)
253 @type username: string
254 @param username: the username to connect with
255 @type password: string
256 @param password: the password to connect with
257 @type curl_config_fn: callable
258 @param curl_config_fn: Function to configure C{pycurl.Curl} object
259 @param logger: Logging object
262 self._username = username
263 self._password = password
264 self._logger = logger
265 self._curl_config_fn = curl_config_fn
266 self._curl_factory = curl_factory
268 self._base_url = "https://%s:%s" % (host, port)
270 if username is not None:
272 raise Error("Password not specified")
274 raise Error("Specified password without username")
276 def _CreateCurl(self):
277 """Creates a cURL object.
280 # Create pycURL object if no factory is provided
281 if self._curl_factory:
282 curl = self._curl_factory()
286 # Default cURL settings
287 curl.setopt(pycurl.VERBOSE, False)
288 curl.setopt(pycurl.FOLLOWLOCATION, False)
289 curl.setopt(pycurl.MAXREDIRS, 5)
290 curl.setopt(pycurl.NOSIGNAL, True)
291 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
292 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
293 curl.setopt(pycurl.SSL_VERIFYPEER, False)
294 curl.setopt(pycurl.HTTPHEADER, [
295 "Accept: %s" % HTTP_APP_JSON,
296 "Content-type: %s" % HTTP_APP_JSON,
299 assert ((self._username is None and self._password is None) ^
300 (self._username is not None and self._password is not None))
303 # Setup authentication
304 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
305 curl.setopt(pycurl.USERPWD,
306 str("%s:%s" % (self._username, self._password)))
308 # Call external configuration function
309 if self._curl_config_fn:
310 self._curl_config_fn(curl, self._logger)
315 def _EncodeQuery(query):
316 """Encode query values for RAPI URL.
318 @type query: list of two-tuples
319 @param query: Query arguments
321 @return: Query list with encoded values
326 for name, value in query:
328 result.append((name, ""))
330 elif isinstance(value, bool):
331 # Boolean values must be encoded as 0 or 1
332 result.append((name, int(value)))
334 elif isinstance(value, (list, tuple, dict)):
335 raise ValueError("Invalid query data type %r" % type(value).__name__)
338 result.append((name, value))
342 def _SendRequest(self, method, path, query, content):
343 """Sends an HTTP request.
345 This constructs a full URL, encodes and decodes HTTP bodies, and
346 handles invalid responses in a pythonic way.
349 @param method: HTTP method to use
351 @param path: HTTP URL path
352 @type query: list of two-tuples
353 @param query: query arguments to pass to urllib.urlencode
354 @type content: str or None
355 @param content: HTTP body content
358 @return: JSON-Decoded response
360 @raises CertificateError: If an invalid SSL certificate is found
361 @raises GanetiApiError: If an invalid response is returned
364 assert path.startswith("/")
366 curl = self._CreateCurl()
368 if content is not None:
369 encoded_content = self._json_encoder.encode(content)
374 urlparts = [self._base_url, path]
377 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
379 url = "".join(urlparts)
381 self._logger.debug("Sending request %s %s (content=%r)",
382 method, url, encoded_content)
384 # Buffer for response
385 encoded_resp_body = StringIO()
388 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
389 curl.setopt(pycurl.URL, str(url))
390 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
391 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
394 # Send request and wait for response
397 except pycurl.error, err:
398 if err.args[0] in _CURL_SSL_CERT_ERRORS:
399 raise CertificateError("SSL certificate error %s" % err)
401 raise GanetiApiError(str(err))
403 # Reset settings to not keep references to large objects in memory
405 curl.setopt(pycurl.POSTFIELDS, "")
406 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
408 # Get HTTP response code
409 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
411 # Was anything written to the response buffer?
412 if encoded_resp_body.tell():
413 response_content = simplejson.loads(encoded_resp_body.getvalue())
415 response_content = None
417 if http_code != HTTP_OK:
418 if isinstance(response_content, dict):
420 (response_content["code"],
421 response_content["message"],
422 response_content["explain"]))
424 msg = str(response_content)
426 raise GanetiApiError(msg, code=http_code)
428 return response_content
430 def GetVersion(self):
431 """Gets the Remote API version running on the cluster.
434 @return: Ganeti Remote API version
437 return self._SendRequest(HTTP_GET, "/version", None, None)
439 def GetFeatures(self):
440 """Gets the list of optional features supported by RAPI server.
443 @return: List of optional features
447 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
449 except GanetiApiError, err:
450 # Older RAPI servers don't support this resource
451 if err.code == HTTP_NOT_FOUND:
456 def GetOperatingSystems(self):
457 """Gets the Operating Systems running in the Ganeti cluster.
460 @return: operating systems
463 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
467 """Gets info about the cluster.
470 @return: information about the cluster
473 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
476 def GetClusterTags(self):
477 """Gets the cluster tags.
480 @return: cluster tags
483 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
486 def AddClusterTags(self, tags, dry_run=False):
487 """Adds tags to the cluster.
489 @type tags: list of str
490 @param tags: tags to add to the cluster
492 @param dry_run: whether to perform a dry run
498 query = [("tag", t) for t in tags]
500 query.append(("dry-run", 1))
502 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
505 def DeleteClusterTags(self, tags, dry_run=False):
506 """Deletes tags from the cluster.
508 @type tags: list of str
509 @param tags: tags to delete
511 @param dry_run: whether to perform a dry run
514 query = [("tag", t) for t in tags]
516 query.append(("dry-run", 1))
518 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
521 def GetInstances(self, bulk=False):
522 """Gets information about instances on the cluster.
525 @param bulk: whether to return all information about all instances
527 @rtype: list of dict or list of str
528 @return: if bulk is True, info about the instances, else a list of instances
533 query.append(("bulk", 1))
535 instances = self._SendRequest(HTTP_GET,
536 "/%s/instances" % GANETI_RAPI_VERSION,
541 return [i["id"] for i in instances]
543 def GetInstance(self, instance):
544 """Gets information about an instance.
547 @param instance: instance whose info to return
550 @return: info about the instance
553 return self._SendRequest(HTTP_GET,
554 ("/%s/instances/%s" %
555 (GANETI_RAPI_VERSION, instance)), None, None)
557 def GetInstanceInfo(self, instance, static=None):
558 """Gets information about an instance.
560 @type instance: string
561 @param instance: Instance name
566 if static is not None:
567 query = [("static", static)]
571 return self._SendRequest(HTTP_GET,
572 ("/%s/instances/%s/info" %
573 (GANETI_RAPI_VERSION, instance)), query, None)
575 def CreateInstance(self, mode, name, disk_template, disks, nics,
577 """Creates a new instance.
579 More details for parameters can be found in the RAPI documentation.
582 @param mode: Instance creation mode
584 @param name: Hostname of the instance to create
585 @type disk_template: string
586 @param disk_template: Disk template for instance (e.g. plain, diskless,
588 @type disks: list of dicts
589 @param disks: List of disk definitions
590 @type nics: list of dicts
591 @param nics: List of NIC definitions
593 @keyword dry_run: whether to perform a dry run
601 if kwargs.get("dry_run"):
602 query.append(("dry-run", 1))
604 if _INST_CREATE_REQV1 in self.GetFeatures():
605 # All required fields for request data version 1
607 _REQ_DATA_VERSION_FIELD: 1,
610 "disk_template": disk_template,
615 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
617 raise GanetiApiError("Required fields can not be specified as"
618 " keywords: %s" % ", ".join(conflicts))
620 body.update((key, value) for key, value in kwargs.iteritems()
623 # Old request format (version 0)
625 # The following code must make sure that an exception is raised when an
626 # unsupported setting is requested by the caller. Otherwise this can lead
627 # to bugs difficult to find. The interface of this function must stay
628 # exactly the same for version 0 and 1 (e.g. they aren't allowed to
629 # require different data types).
632 for idx, disk in enumerate(disks):
633 unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS
635 raise GanetiApiError("Server supports request version 0 only, but"
636 " disk %s specifies the unsupported parameters"
637 " %s, allowed are %s" %
639 list(_INST_CREATE_V0_DISK_PARAMS)))
641 assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and
642 "size" in _INST_CREATE_V0_DISK_PARAMS)
643 disk_sizes = [disk["size"] for disk in disks]
647 raise GanetiApiError("Server supports request version 0 only, but"
650 raise GanetiApiError("Server supports request version 0 only, but"
651 " more than one NIC specified")
653 assert len(nics) == 1
655 unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS
657 raise GanetiApiError("Server supports request version 0 only, but"
658 " NIC 0 specifies the unsupported parameters %s,"
660 (unsupported, list(_INST_NIC_PARAMS)))
662 # Validate other parameters
663 unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS -
664 _INST_CREATE_V0_DPARAMS)
666 allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS)
667 raise GanetiApiError("Server supports request version 0 only, but"
668 " the following unsupported parameters are"
669 " specified: %s, allowed are %s" %
670 (unsupported, list(allowed)))
672 # All required fields for request data version 0
674 _REQ_DATA_VERSION_FIELD: 0,
676 "disk_template": disk_template,
681 assert len(nics) == 1
682 assert not (set(body.keys()) & set(nics[0].keys()))
685 # Copy supported fields
686 assert not (set(body.keys()) & set(kwargs.keys()))
687 body.update(dict((key, value) for key, value in kwargs.items()
688 if key in _INST_CREATE_V0_PARAMS))
691 for i in (value for key, value in kwargs.items()
692 if key in _INST_CREATE_V0_DPARAMS):
693 assert not (set(body.keys()) & set(i.keys()))
696 assert not (set(kwargs.keys()) -
697 (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS))
698 assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS)
700 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
703 def DeleteInstance(self, instance, dry_run=False):
704 """Deletes an instance.
707 @param instance: the instance to delete
715 query.append(("dry-run", 1))
717 return self._SendRequest(HTTP_DELETE,
718 ("/%s/instances/%s" %
719 (GANETI_RAPI_VERSION, instance)), query, None)
721 def GetInstanceTags(self, instance):
722 """Gets tags for an instance.
725 @param instance: instance whose tags to return
728 @return: tags for the instance
731 return self._SendRequest(HTTP_GET,
732 ("/%s/instances/%s/tags" %
733 (GANETI_RAPI_VERSION, instance)), None, None)
735 def AddInstanceTags(self, instance, tags, dry_run=False):
736 """Adds tags to an instance.
739 @param instance: instance to add tags to
740 @type tags: list of str
741 @param tags: tags to add to the instance
743 @param dry_run: whether to perform a dry run
749 query = [("tag", t) for t in tags]
751 query.append(("dry-run", 1))
753 return self._SendRequest(HTTP_PUT,
754 ("/%s/instances/%s/tags" %
755 (GANETI_RAPI_VERSION, instance)), query, None)
757 def DeleteInstanceTags(self, instance, tags, dry_run=False):
758 """Deletes tags from an instance.
761 @param instance: instance to delete tags from
762 @type tags: list of str
763 @param tags: tags to delete
765 @param dry_run: whether to perform a dry run
768 query = [("tag", t) for t in tags]
770 query.append(("dry-run", 1))
772 return self._SendRequest(HTTP_DELETE,
773 ("/%s/instances/%s/tags" %
774 (GANETI_RAPI_VERSION, instance)), query, None)
776 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
778 """Reboots an instance.
781 @param instance: instance to rebot
782 @type reboot_type: str
783 @param reboot_type: one of: hard, soft, full
784 @type ignore_secondaries: bool
785 @param ignore_secondaries: if True, ignores errors for the secondary node
786 while re-assembling disks (in hard-reboot mode only)
788 @param dry_run: whether to perform a dry run
793 query.append(("type", reboot_type))
794 if ignore_secondaries is not None:
795 query.append(("ignore_secondaries", ignore_secondaries))
797 query.append(("dry-run", 1))
799 return self._SendRequest(HTTP_POST,
800 ("/%s/instances/%s/reboot" %
801 (GANETI_RAPI_VERSION, instance)), query, None)
803 def ShutdownInstance(self, instance, dry_run=False):
804 """Shuts down an instance.
807 @param instance: the instance to shut down
809 @param dry_run: whether to perform a dry run
814 query.append(("dry-run", 1))
816 return self._SendRequest(HTTP_PUT,
817 ("/%s/instances/%s/shutdown" %
818 (GANETI_RAPI_VERSION, instance)), query, None)
820 def StartupInstance(self, instance, dry_run=False):
821 """Starts up an instance.
824 @param instance: the instance to start up
826 @param dry_run: whether to perform a dry run
831 query.append(("dry-run", 1))
833 return self._SendRequest(HTTP_PUT,
834 ("/%s/instances/%s/startup" %
835 (GANETI_RAPI_VERSION, instance)), query, None)
837 def ReinstallInstance(self, instance, os=None, no_startup=False):
838 """Reinstalls an instance.
841 @param instance: The instance to reinstall
842 @type os: str or None
843 @param os: The operating system to reinstall. If None, the instance's
844 current operating system will be installed again
845 @type no_startup: bool
846 @param no_startup: Whether to start the instance automatically
851 query.append(("os", os))
853 query.append(("nostartup", 1))
854 return self._SendRequest(HTTP_POST,
855 ("/%s/instances/%s/reinstall" %
856 (GANETI_RAPI_VERSION, instance)), query, None)
858 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
859 remote_node=None, iallocator=None, dry_run=False):
860 """Replaces disks on an instance.
863 @param instance: instance whose disks to replace
864 @type disks: list of ints
865 @param disks: Indexes of disks to replace
867 @param mode: replacement mode to use (defaults to replace_auto)
868 @type remote_node: str or None
869 @param remote_node: new secondary node to use (for use with
870 replace_new_secondary mode)
871 @type iallocator: str or None
872 @param iallocator: instance allocator plugin to use (for use with
875 @param dry_run: whether to perform a dry run
886 query.append(("disks", ",".join(str(idx) for idx in disks)))
889 query.append(("remote_node", remote_node))
892 query.append(("iallocator", iallocator))
895 query.append(("dry-run", 1))
897 return self._SendRequest(HTTP_POST,
898 ("/%s/instances/%s/replace-disks" %
899 (GANETI_RAPI_VERSION, instance)), query, None)
901 def PrepareExport(self, instance, mode):
902 """Prepares an instance for an export.
904 @type instance: string
905 @param instance: Instance name
907 @param mode: Export mode
912 query = [("mode", mode)]
913 return self._SendRequest(HTTP_PUT,
914 ("/%s/instances/%s/prepare-export" %
915 (GANETI_RAPI_VERSION, instance)), query, None)
917 def ExportInstance(self, instance, mode, destination, shutdown=None,
918 remove_instance=None,
919 x509_key_name=None, destination_x509_ca=None):
920 """Exports an instance.
922 @type instance: string
923 @param instance: Instance name
925 @param mode: Export mode
931 "destination": destination,
935 if shutdown is not None:
936 body["shutdown"] = shutdown
938 if remove_instance is not None:
939 body["remove_instance"] = remove_instance
941 if x509_key_name is not None:
942 body["x509_key_name"] = x509_key_name
944 if destination_x509_ca is not None:
945 body["destination_x509_ca"] = destination_x509_ca
947 return self._SendRequest(HTTP_PUT,
948 ("/%s/instances/%s/export" %
949 (GANETI_RAPI_VERSION, instance)), None, body)
951 def MigrateInstance(self, instance, mode=None, cleanup=None):
952 """Migrates an instance.
954 @type instance: string
955 @param instance: Instance name
957 @param mode: Migration mode
959 @param cleanup: Whether to clean up a previously failed migration
967 if cleanup is not None:
968 body["cleanup"] = cleanup
970 return self._SendRequest(HTTP_PUT,
971 ("/%s/instances/%s/migrate" %
972 (GANETI_RAPI_VERSION, instance)), None, body)
974 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
975 """Changes the name of an instance.
977 @type instance: string
978 @param instance: Instance name
979 @type new_name: string
980 @param new_name: New instance name
982 @param ip_check: Whether to ensure instance's IP address is inactive
983 @type name_check: bool
984 @param name_check: Whether to ensure instance's name is resolvable
988 "new_name": new_name,
991 if ip_check is not None:
992 body["ip_check"] = ip_check
994 if name_check is not None:
995 body["name_check"] = name_check
997 return self._SendRequest(HTTP_PUT,
998 ("/%s/instances/%s/rename" %
999 (GANETI_RAPI_VERSION, instance)), None, body)
1002 """Gets all jobs for the cluster.
1005 @return: job ids for the cluster
1008 return [int(j["id"])
1009 for j in self._SendRequest(HTTP_GET,
1010 "/%s/jobs" % GANETI_RAPI_VERSION,
1013 def GetJobStatus(self, job_id):
1014 """Gets the status of a job.
1017 @param job_id: job id whose status to query
1023 return self._SendRequest(HTTP_GET,
1024 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1027 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1028 """Waits for job changes.
1031 @param job_id: Job ID for which to wait
1036 "previous_job_info": prev_job_info,
1037 "previous_log_serial": prev_log_serial,
1040 return self._SendRequest(HTTP_GET,
1041 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1044 def CancelJob(self, job_id, dry_run=False):
1048 @param job_id: id of the job to delete
1050 @param dry_run: whether to perform a dry run
1055 query.append(("dry-run", 1))
1057 return self._SendRequest(HTTP_DELETE,
1058 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1061 def GetNodes(self, bulk=False):
1062 """Gets all nodes in the cluster.
1065 @param bulk: whether to return all information about all instances
1067 @rtype: list of dict or str
1068 @return: if bulk is true, info about nodes in the cluster,
1069 else list of nodes in the cluster
1074 query.append(("bulk", 1))
1076 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1081 return [n["id"] for n in nodes]
1083 def GetNode(self, node):
1084 """Gets information about a node.
1087 @param node: node whose info to return
1090 @return: info about the node
1093 return self._SendRequest(HTTP_GET,
1094 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1097 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1098 dry_run=False, early_release=False):
1099 """Evacuates instances from a Ganeti node.
1102 @param node: node to evacuate
1103 @type iallocator: str or None
1104 @param iallocator: instance allocator to use
1105 @type remote_node: str
1106 @param remote_node: node to evaucate to
1108 @param dry_run: whether to perform a dry run
1109 @type early_release: bool
1110 @param early_release: whether to enable parallelization
1113 @return: list of (job ID, instance name, new secondary node); if
1114 dry_run was specified, then the actual move jobs were not
1115 submitted and the job IDs will be C{None}
1117 @raises GanetiApiError: if an iallocator and remote_node are both
1121 if iallocator and remote_node:
1122 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1126 query.append(("iallocator", iallocator))
1128 query.append(("remote_node", remote_node))
1130 query.append(("dry-run", 1))
1132 query.append(("early_release", 1))
1134 return self._SendRequest(HTTP_POST,
1135 ("/%s/nodes/%s/evacuate" %
1136 (GANETI_RAPI_VERSION, node)), query, None)
1138 def MigrateNode(self, node, mode=None, dry_run=False):
1139 """Migrates all primary instances from a node.
1142 @param node: node to migrate
1144 @param mode: if passed, it will overwrite the live migration type,
1145 otherwise the hypervisor default will be used
1147 @param dry_run: whether to perform a dry run
1154 if mode is not None:
1155 query.append(("mode", mode))
1157 query.append(("dry-run", 1))
1159 return self._SendRequest(HTTP_POST,
1160 ("/%s/nodes/%s/migrate" %
1161 (GANETI_RAPI_VERSION, node)), query, None)
1163 def GetNodeRole(self, node):
1164 """Gets the current role for a node.
1167 @param node: node whose role to return
1170 @return: the current role for a node
1173 return self._SendRequest(HTTP_GET,
1174 ("/%s/nodes/%s/role" %
1175 (GANETI_RAPI_VERSION, node)), None, None)
1177 def SetNodeRole(self, node, role, force=False):
1178 """Sets the role for a node.
1181 @param node: the node whose role to set
1183 @param role: the role to set for the node
1185 @param force: whether to force the role change
1195 return self._SendRequest(HTTP_PUT,
1196 ("/%s/nodes/%s/role" %
1197 (GANETI_RAPI_VERSION, node)), query, role)
1199 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1200 """Gets the storage units for a node.
1203 @param node: the node whose storage units to return
1204 @type storage_type: str
1205 @param storage_type: storage type whose units to return
1206 @type output_fields: str
1207 @param output_fields: storage type fields to return
1210 @return: job id where results can be retrieved
1214 ("storage_type", storage_type),
1215 ("output_fields", output_fields),
1218 return self._SendRequest(HTTP_GET,
1219 ("/%s/nodes/%s/storage" %
1220 (GANETI_RAPI_VERSION, node)), query, None)
1222 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1223 """Modifies parameters of storage units on the node.
1226 @param node: node whose storage units to modify
1227 @type storage_type: str
1228 @param storage_type: storage type whose units to modify
1230 @param name: name of the storage unit
1231 @type allocatable: bool or None
1232 @param allocatable: Whether to set the "allocatable" flag on the storage
1233 unit (None=no modification, True=set, False=unset)
1240 ("storage_type", storage_type),
1244 if allocatable is not None:
1245 query.append(("allocatable", allocatable))
1247 return self._SendRequest(HTTP_PUT,
1248 ("/%s/nodes/%s/storage/modify" %
1249 (GANETI_RAPI_VERSION, node)), query, None)
1251 def RepairNodeStorageUnits(self, node, storage_type, name):
1252 """Repairs a storage unit on the node.
1255 @param node: node whose storage units to repair
1256 @type storage_type: str
1257 @param storage_type: storage type to repair
1259 @param name: name of the storage unit to repair
1266 ("storage_type", storage_type),
1270 return self._SendRequest(HTTP_PUT,
1271 ("/%s/nodes/%s/storage/repair" %
1272 (GANETI_RAPI_VERSION, node)), query, None)
1274 def GetNodeTags(self, node):
1275 """Gets the tags for a node.
1278 @param node: node whose tags to return
1281 @return: tags for the node
1284 return self._SendRequest(HTTP_GET,
1285 ("/%s/nodes/%s/tags" %
1286 (GANETI_RAPI_VERSION, node)), None, None)
1288 def AddNodeTags(self, node, tags, dry_run=False):
1289 """Adds tags to a node.
1292 @param node: node to add tags to
1293 @type tags: list of str
1294 @param tags: tags to add to the node
1296 @param dry_run: whether to perform a dry run
1302 query = [("tag", t) for t in tags]
1304 query.append(("dry-run", 1))
1306 return self._SendRequest(HTTP_PUT,
1307 ("/%s/nodes/%s/tags" %
1308 (GANETI_RAPI_VERSION, node)), query, tags)
1310 def DeleteNodeTags(self, node, tags, dry_run=False):
1311 """Delete tags from a node.
1314 @param node: node to remove tags from
1315 @type tags: list of str
1316 @param tags: tags to remove from the node
1318 @param dry_run: whether to perform a dry run
1324 query = [("tag", t) for t in tags]
1326 query.append(("dry-run", 1))
1328 return self._SendRequest(HTTP_DELETE,
1329 ("/%s/nodes/%s/tags" %
1330 (GANETI_RAPI_VERSION, node)), query, None)