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"
74 # Older pycURL versions don't have all error constants
76 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
77 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
78 except AttributeError:
79 _CURLE_SSL_CACERT = 60
80 _CURLE_SSL_CACERT_BADFILE = 77
82 _CURL_SSL_CERT_ERRORS = frozenset([
84 _CURLE_SSL_CACERT_BADFILE,
88 class Error(Exception):
89 """Base error class for this module.
95 class CertificateError(Error):
96 """Raised when a problem is found with the SSL certificate.
102 class GanetiApiError(Error):
103 """Generic error raised from Ganeti API.
106 def __init__(self, msg, code=None):
107 Error.__init__(self, msg)
111 def UsesRapiClient(fn):
112 """Decorator for code using RAPI client to initialize pycURL.
115 def wrapper(*args, **kwargs):
116 # curl_global_init(3) and curl_global_cleanup(3) must be called with only
117 # one thread running. This check is just a safety measure -- it doesn't
119 assert threading.activeCount() == 1, \
120 "Found active threads when initializing pycURL"
122 pycurl.global_init(pycurl.GLOBAL_ALL)
124 return fn(*args, **kwargs)
126 pycurl.global_cleanup()
131 def GenericCurlConfig(verbose=False, use_signal=False,
132 use_curl_cabundle=False, cafile=None, capath=None,
133 proxy=None, verify_hostname=False,
134 connect_timeout=None, timeout=None,
135 _pycurl_version_fn=pycurl.version_info):
136 """Curl configuration function generator.
139 @param verbose: Whether to set cURL to verbose mode
140 @type use_signal: bool
141 @param use_signal: Whether to allow cURL to use signals
142 @type use_curl_cabundle: bool
143 @param use_curl_cabundle: Whether to use cURL's default CA bundle
145 @param cafile: In which file we can find the certificates
147 @param capath: In which directory we can find the certificates
149 @param proxy: Proxy to use, None for default behaviour and empty string for
150 disabling proxies (see curl_easy_setopt(3))
151 @type verify_hostname: bool
152 @param verify_hostname: Whether to verify the remote peer certificate's
154 @type connect_timeout: number
155 @param connect_timeout: Timeout for establishing connection in seconds
156 @type timeout: number
157 @param timeout: Timeout for complete transfer in seconds (see
158 curl_easy_setopt(3)).
161 if use_curl_cabundle and (cafile or capath):
162 raise Error("Can not use default CA bundle when CA file or path is set")
164 def _ConfigCurl(curl, logger):
165 """Configures a cURL object
167 @type curl: pycurl.Curl
168 @param curl: cURL object
171 logger.debug("Using cURL version %s", pycurl.version)
173 # pycurl.version_info returns a tuple with information about the used
174 # version of libcurl. Item 5 is the SSL library linked to it.
175 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
177 sslver = _pycurl_version_fn()[5]
179 raise Error("No SSL support in cURL")
181 lcsslver = sslver.lower()
182 if lcsslver.startswith("openssl/"):
184 elif lcsslver.startswith("gnutls/"):
186 raise Error("cURL linked against GnuTLS has no support for a"
187 " CA path (%s)" % (pycurl.version, ))
189 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
192 curl.setopt(pycurl.VERBOSE, verbose)
193 curl.setopt(pycurl.NOSIGNAL, not use_signal)
195 # Whether to verify remote peer's CN
197 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
198 # certificate must indicate that the server is the server to which you
199 # meant to connect, or the connection fails. [...] When the value is 1,
200 # the certificate must contain a Common Name field, but it doesn't matter
201 # what name it says. [...]"
202 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
204 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
206 if cafile or capath or use_curl_cabundle:
207 # Require certificates to be checked
208 curl.setopt(pycurl.SSL_VERIFYPEER, True)
210 curl.setopt(pycurl.CAINFO, str(cafile))
212 curl.setopt(pycurl.CAPATH, str(capath))
213 # Not changing anything for using default CA bundle
215 # Disable SSL certificate verification
216 curl.setopt(pycurl.SSL_VERIFYPEER, False)
218 if proxy is not None:
219 curl.setopt(pycurl.PROXY, str(proxy))
222 if connect_timeout is not None:
223 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
224 if timeout is not None:
225 curl.setopt(pycurl.TIMEOUT, timeout)
230 class GanetiRapiClient(object):
231 """Ganeti RAPI client.
234 USER_AGENT = "Ganeti RAPI Client"
235 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
237 def __init__(self, host, port=GANETI_RAPI_PORT,
238 username=None, password=None, logger=logging,
239 curl_config_fn=None, curl=None):
240 """Initializes this class.
243 @param host: the ganeti cluster master to interact with
245 @param port: the port on which the RAPI is running (default is 5080)
246 @type username: string
247 @param username: the username to connect with
248 @type password: string
249 @param password: the password to connect with
250 @type curl_config_fn: callable
251 @param curl_config_fn: Function to configure C{pycurl.Curl} object
252 @param logger: Logging object
257 self._logger = logger
259 self._base_url = "https://%s:%s" % (host, port)
261 # Create pycURL object if not supplied
265 # Default cURL settings
266 curl.setopt(pycurl.VERBOSE, False)
267 curl.setopt(pycurl.FOLLOWLOCATION, False)
268 curl.setopt(pycurl.MAXREDIRS, 5)
269 curl.setopt(pycurl.NOSIGNAL, True)
270 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
271 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
272 curl.setopt(pycurl.SSL_VERIFYPEER, False)
273 curl.setopt(pycurl.HTTPHEADER, [
274 "Accept: %s" % HTTP_APP_JSON,
275 "Content-type: %s" % HTTP_APP_JSON,
278 # Setup authentication
279 if username is not None:
281 raise Error("Password not specified")
282 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
283 curl.setopt(pycurl.USERPWD, str("%s:%s" % (username, password)))
285 raise Error("Specified password without username")
287 # Call external configuration function
289 curl_config_fn(curl, logger)
294 def _EncodeQuery(query):
295 """Encode query values for RAPI URL.
297 @type query: list of two-tuples
298 @param query: Query arguments
300 @return: Query list with encoded values
305 for name, value in query:
307 result.append((name, ""))
309 elif isinstance(value, bool):
310 # Boolean values must be encoded as 0 or 1
311 result.append((name, int(value)))
313 elif isinstance(value, (list, tuple, dict)):
314 raise ValueError("Invalid query data type %r" % type(value).__name__)
317 result.append((name, value))
321 def _SendRequest(self, method, path, query, content):
322 """Sends an HTTP request.
324 This constructs a full URL, encodes and decodes HTTP bodies, and
325 handles invalid responses in a pythonic way.
328 @param method: HTTP method to use
330 @param path: HTTP URL path
331 @type query: list of two-tuples
332 @param query: query arguments to pass to urllib.urlencode
333 @type content: str or None
334 @param content: HTTP body content
337 @return: JSON-Decoded response
339 @raises CertificateError: If an invalid SSL certificate is found
340 @raises GanetiApiError: If an invalid response is returned
343 assert path.startswith("/")
348 encoded_content = self._json_encoder.encode(content)
353 urlparts = [self._base_url, path]
356 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
358 url = "".join(urlparts)
360 self._logger.debug("Sending request %s %s to %s:%s (content=%r)",
361 method, url, self._host, self._port, encoded_content)
363 # Buffer for response
364 encoded_resp_body = StringIO()
367 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
368 curl.setopt(pycurl.URL, str(url))
369 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
370 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
373 # Send request and wait for response
376 except pycurl.error, err:
377 if err.args[0] in _CURL_SSL_CERT_ERRORS:
378 raise CertificateError("SSL certificate error %s" % err)
380 raise GanetiApiError(str(err))
382 # Reset settings to not keep references to large objects in memory
384 curl.setopt(pycurl.POSTFIELDS, "")
385 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
387 # Get HTTP response code
388 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
390 # Was anything written to the response buffer?
391 if encoded_resp_body.tell():
392 response_content = simplejson.loads(encoded_resp_body.getvalue())
394 response_content = None
396 if http_code != HTTP_OK:
397 if isinstance(response_content, dict):
399 (response_content["code"],
400 response_content["message"],
401 response_content["explain"]))
403 msg = str(response_content)
405 raise GanetiApiError(msg, code=http_code)
407 return response_content
409 def GetVersion(self):
410 """Gets the Remote API version running on the cluster.
413 @return: Ganeti Remote API version
416 return self._SendRequest(HTTP_GET, "/version", None, None)
418 def GetFeatures(self):
419 """Gets the list of optional features supported by RAPI server.
422 @return: List of optional features
426 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
428 except GanetiApiError, err:
429 # Older RAPI servers don't support this resource
430 if err.code == HTTP_NOT_FOUND:
435 def GetOperatingSystems(self):
436 """Gets the Operating Systems running in the Ganeti cluster.
439 @return: operating systems
442 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
446 """Gets info about the cluster.
449 @return: information about the cluster
452 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
455 def GetClusterTags(self):
456 """Gets the cluster tags.
459 @return: cluster tags
462 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
465 def AddClusterTags(self, tags, dry_run=False):
466 """Adds tags to the cluster.
468 @type tags: list of str
469 @param tags: tags to add to the cluster
471 @param dry_run: whether to perform a dry run
477 query = [("tag", t) for t in tags]
479 query.append(("dry-run", 1))
481 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
484 def DeleteClusterTags(self, tags, dry_run=False):
485 """Deletes tags from the cluster.
487 @type tags: list of str
488 @param tags: tags to delete
490 @param dry_run: whether to perform a dry run
493 query = [("tag", t) for t in tags]
495 query.append(("dry-run", 1))
497 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
500 def GetInstances(self, bulk=False):
501 """Gets information about instances on the cluster.
504 @param bulk: whether to return all information about all instances
506 @rtype: list of dict or list of str
507 @return: if bulk is True, info about the instances, else a list of instances
512 query.append(("bulk", 1))
514 instances = self._SendRequest(HTTP_GET,
515 "/%s/instances" % GANETI_RAPI_VERSION,
520 return [i["id"] for i in instances]
522 def GetInstance(self, instance):
523 """Gets information about an instance.
526 @param instance: instance whose info to return
529 @return: info about the instance
532 return self._SendRequest(HTTP_GET,
533 ("/%s/instances/%s" %
534 (GANETI_RAPI_VERSION, instance)), None, None)
536 def GetInstanceInfo(self, instance, static=None):
537 """Gets information about an instance.
539 @type instance: string
540 @param instance: Instance name
545 if static is not None:
546 query = [("static", static)]
550 return self._SendRequest(HTTP_GET,
551 ("/%s/instances/%s/info" %
552 (GANETI_RAPI_VERSION, instance)), query, None)
554 def CreateInstance(self, mode, name, disk_template, disks, nics,
556 """Creates a new instance.
558 More details for parameters can be found in the RAPI documentation.
561 @param mode: Instance creation mode
563 @param name: Hostname of the instance to create
564 @type disk_template: string
565 @param disk_template: Disk template for instance (e.g. plain, diskless,
567 @type disks: list of dicts
568 @param disks: List of disk definitions
569 @type nics: list of dicts
570 @param nics: List of NIC definitions
572 @keyword dry_run: whether to perform a dry run
580 if kwargs.get("dry_run"):
581 query.append(("dry-run", 1))
583 if _INST_CREATE_REQV1 in self.GetFeatures():
584 # All required fields for request data version 1
586 _REQ_DATA_VERSION_FIELD: 1,
589 "disk_template": disk_template,
594 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
596 raise GanetiApiError("Required fields can not be specified as"
597 " keywords: %s" % ", ".join(conflicts))
599 body.update((key, value) for key, value in kwargs.iteritems()
602 # TODO: Implement instance creation request data version 0
603 # When implementing version 0, care should be taken to refuse unknown
604 # parameters and invalid values. The interface of this function must stay
605 # exactly the same for version 0 and 1 (e.g. they aren't allowed to
606 # require different data types).
607 raise NotImplementedError("Support for instance creation request data"
608 " version 0 is not yet implemented")
610 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
613 def DeleteInstance(self, instance, dry_run=False):
614 """Deletes an instance.
617 @param instance: the instance to delete
625 query.append(("dry-run", 1))
627 return self._SendRequest(HTTP_DELETE,
628 ("/%s/instances/%s" %
629 (GANETI_RAPI_VERSION, instance)), query, None)
631 def GetInstanceTags(self, instance):
632 """Gets tags for an instance.
635 @param instance: instance whose tags to return
638 @return: tags for the instance
641 return self._SendRequest(HTTP_GET,
642 ("/%s/instances/%s/tags" %
643 (GANETI_RAPI_VERSION, instance)), None, None)
645 def AddInstanceTags(self, instance, tags, dry_run=False):
646 """Adds tags to an instance.
649 @param instance: instance to add tags to
650 @type tags: list of str
651 @param tags: tags to add to the instance
653 @param dry_run: whether to perform a dry run
659 query = [("tag", t) for t in tags]
661 query.append(("dry-run", 1))
663 return self._SendRequest(HTTP_PUT,
664 ("/%s/instances/%s/tags" %
665 (GANETI_RAPI_VERSION, instance)), query, None)
667 def DeleteInstanceTags(self, instance, tags, dry_run=False):
668 """Deletes tags from an instance.
671 @param instance: instance to delete tags from
672 @type tags: list of str
673 @param tags: tags to delete
675 @param dry_run: whether to perform a dry run
678 query = [("tag", t) for t in tags]
680 query.append(("dry-run", 1))
682 return self._SendRequest(HTTP_DELETE,
683 ("/%s/instances/%s/tags" %
684 (GANETI_RAPI_VERSION, instance)), query, None)
686 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
688 """Reboots an instance.
691 @param instance: instance to rebot
692 @type reboot_type: str
693 @param reboot_type: one of: hard, soft, full
694 @type ignore_secondaries: bool
695 @param ignore_secondaries: if True, ignores errors for the secondary node
696 while re-assembling disks (in hard-reboot mode only)
698 @param dry_run: whether to perform a dry run
703 query.append(("type", reboot_type))
704 if ignore_secondaries is not None:
705 query.append(("ignore_secondaries", ignore_secondaries))
707 query.append(("dry-run", 1))
709 return self._SendRequest(HTTP_POST,
710 ("/%s/instances/%s/reboot" %
711 (GANETI_RAPI_VERSION, instance)), query, None)
713 def ShutdownInstance(self, instance, dry_run=False):
714 """Shuts down an instance.
717 @param instance: the instance to shut down
719 @param dry_run: whether to perform a dry run
724 query.append(("dry-run", 1))
726 return self._SendRequest(HTTP_PUT,
727 ("/%s/instances/%s/shutdown" %
728 (GANETI_RAPI_VERSION, instance)), query, None)
730 def StartupInstance(self, instance, dry_run=False):
731 """Starts up an instance.
734 @param instance: the instance to start up
736 @param dry_run: whether to perform a dry run
741 query.append(("dry-run", 1))
743 return self._SendRequest(HTTP_PUT,
744 ("/%s/instances/%s/startup" %
745 (GANETI_RAPI_VERSION, instance)), query, None)
747 def ReinstallInstance(self, instance, os, no_startup=False):
748 """Reinstalls an instance.
751 @param instance: the instance to reinstall
753 @param os: the os to reinstall
754 @type no_startup: bool
755 @param no_startup: whether to start the instance automatically
760 query.append(("nostartup", 1))
761 return self._SendRequest(HTTP_POST,
762 ("/%s/instances/%s/reinstall" %
763 (GANETI_RAPI_VERSION, instance)), query, None)
765 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
766 remote_node=None, iallocator=None, dry_run=False):
767 """Replaces disks on an instance.
770 @param instance: instance whose disks to replace
771 @type disks: list of ints
772 @param disks: Indexes of disks to replace
774 @param mode: replacement mode to use (defaults to replace_auto)
775 @type remote_node: str or None
776 @param remote_node: new secondary node to use (for use with
777 replace_new_secondary mode)
778 @type iallocator: str or None
779 @param iallocator: instance allocator plugin to use (for use with
782 @param dry_run: whether to perform a dry run
793 query.append(("disks", ",".join(str(idx) for idx in disks)))
796 query.append(("remote_node", remote_node))
799 query.append(("iallocator", iallocator))
802 query.append(("dry-run", 1))
804 return self._SendRequest(HTTP_POST,
805 ("/%s/instances/%s/replace-disks" %
806 (GANETI_RAPI_VERSION, instance)), query, None)
808 def PrepareExport(self, instance, mode):
809 """Prepares an instance for an export.
811 @type instance: string
812 @param instance: Instance name
814 @param mode: Export mode
819 query = [("mode", mode)]
820 return self._SendRequest(HTTP_PUT,
821 ("/%s/instances/%s/prepare-export" %
822 (GANETI_RAPI_VERSION, instance)), query, None)
824 def ExportInstance(self, instance, mode, destination, shutdown=None,
825 remove_instance=None,
826 x509_key_name=None, destination_x509_ca=None):
827 """Exports an instance.
829 @type instance: string
830 @param instance: Instance name
832 @param mode: Export mode
838 "destination": destination,
842 if shutdown is not None:
843 body["shutdown"] = shutdown
845 if remove_instance is not None:
846 body["remove_instance"] = remove_instance
848 if x509_key_name is not None:
849 body["x509_key_name"] = x509_key_name
851 if destination_x509_ca is not None:
852 body["destination_x509_ca"] = destination_x509_ca
854 return self._SendRequest(HTTP_PUT,
855 ("/%s/instances/%s/export" %
856 (GANETI_RAPI_VERSION, instance)), None, body)
859 """Gets all jobs for the cluster.
862 @return: job ids for the cluster
866 for j in self._SendRequest(HTTP_GET,
867 "/%s/jobs" % GANETI_RAPI_VERSION,
870 def GetJobStatus(self, job_id):
871 """Gets the status of a job.
874 @param job_id: job id whose status to query
880 return self._SendRequest(HTTP_GET,
881 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
884 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
885 """Waits for job changes.
888 @param job_id: Job ID for which to wait
893 "previous_job_info": prev_job_info,
894 "previous_log_serial": prev_log_serial,
897 return self._SendRequest(HTTP_GET,
898 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
901 def CancelJob(self, job_id, dry_run=False):
905 @param job_id: id of the job to delete
907 @param dry_run: whether to perform a dry run
912 query.append(("dry-run", 1))
914 return self._SendRequest(HTTP_DELETE,
915 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
918 def GetNodes(self, bulk=False):
919 """Gets all nodes in the cluster.
922 @param bulk: whether to return all information about all instances
924 @rtype: list of dict or str
925 @return: if bulk is true, info about nodes in the cluster,
926 else list of nodes in the cluster
931 query.append(("bulk", 1))
933 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
938 return [n["id"] for n in nodes]
940 def GetNode(self, node):
941 """Gets information about a node.
944 @param node: node whose info to return
947 @return: info about the node
950 return self._SendRequest(HTTP_GET,
951 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
954 def EvacuateNode(self, node, iallocator=None, remote_node=None,
955 dry_run=False, early_release=False):
956 """Evacuates instances from a Ganeti node.
959 @param node: node to evacuate
960 @type iallocator: str or None
961 @param iallocator: instance allocator to use
962 @type remote_node: str
963 @param remote_node: node to evaucate to
965 @param dry_run: whether to perform a dry run
966 @type early_release: bool
967 @param early_release: whether to enable parallelization
970 @return: list of (job ID, instance name, new secondary node); if
971 dry_run was specified, then the actual move jobs were not
972 submitted and the job IDs will be C{None}
974 @raises GanetiApiError: if an iallocator and remote_node are both
978 if iallocator and remote_node:
979 raise GanetiApiError("Only one of iallocator or remote_node can be used")
983 query.append(("iallocator", iallocator))
985 query.append(("remote_node", remote_node))
987 query.append(("dry-run", 1))
989 query.append(("early_release", 1))
991 return self._SendRequest(HTTP_POST,
992 ("/%s/nodes/%s/evacuate" %
993 (GANETI_RAPI_VERSION, node)), query, None)
995 def MigrateNode(self, node, live=True, dry_run=False):
996 """Migrates all primary instances from a node.
999 @param node: node to migrate
1001 @param live: whether to use live migration
1003 @param dry_run: whether to perform a dry run
1011 query.append(("live", 1))
1013 query.append(("dry-run", 1))
1015 return self._SendRequest(HTTP_POST,
1016 ("/%s/nodes/%s/migrate" %
1017 (GANETI_RAPI_VERSION, node)), query, None)
1019 def GetNodeRole(self, node):
1020 """Gets the current role for a node.
1023 @param node: node whose role to return
1026 @return: the current role for a node
1029 return self._SendRequest(HTTP_GET,
1030 ("/%s/nodes/%s/role" %
1031 (GANETI_RAPI_VERSION, node)), None, None)
1033 def SetNodeRole(self, node, role, force=False):
1034 """Sets the role for a node.
1037 @param node: the node whose role to set
1039 @param role: the role to set for the node
1041 @param force: whether to force the role change
1051 return self._SendRequest(HTTP_PUT,
1052 ("/%s/nodes/%s/role" %
1053 (GANETI_RAPI_VERSION, node)), query, role)
1055 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1056 """Gets the storage units for a node.
1059 @param node: the node whose storage units to return
1060 @type storage_type: str
1061 @param storage_type: storage type whose units to return
1062 @type output_fields: str
1063 @param output_fields: storage type fields to return
1066 @return: job id where results can be retrieved
1070 ("storage_type", storage_type),
1071 ("output_fields", output_fields),
1074 return self._SendRequest(HTTP_GET,
1075 ("/%s/nodes/%s/storage" %
1076 (GANETI_RAPI_VERSION, node)), query, None)
1078 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1079 """Modifies parameters of storage units on the node.
1082 @param node: node whose storage units to modify
1083 @type storage_type: str
1084 @param storage_type: storage type whose units to modify
1086 @param name: name of the storage unit
1087 @type allocatable: bool or None
1088 @param allocatable: Whether to set the "allocatable" flag on the storage
1089 unit (None=no modification, True=set, False=unset)
1096 ("storage_type", storage_type),
1100 if allocatable is not None:
1101 query.append(("allocatable", allocatable))
1103 return self._SendRequest(HTTP_PUT,
1104 ("/%s/nodes/%s/storage/modify" %
1105 (GANETI_RAPI_VERSION, node)), query, None)
1107 def RepairNodeStorageUnits(self, node, storage_type, name):
1108 """Repairs a storage unit on the node.
1111 @param node: node whose storage units to repair
1112 @type storage_type: str
1113 @param storage_type: storage type to repair
1115 @param name: name of the storage unit to repair
1122 ("storage_type", storage_type),
1126 return self._SendRequest(HTTP_PUT,
1127 ("/%s/nodes/%s/storage/repair" %
1128 (GANETI_RAPI_VERSION, node)), query, None)
1130 def GetNodeTags(self, node):
1131 """Gets the tags for a node.
1134 @param node: node whose tags to return
1137 @return: tags for the node
1140 return self._SendRequest(HTTP_GET,
1141 ("/%s/nodes/%s/tags" %
1142 (GANETI_RAPI_VERSION, node)), None, None)
1144 def AddNodeTags(self, node, tags, dry_run=False):
1145 """Adds tags to a node.
1148 @param node: node to add tags to
1149 @type tags: list of str
1150 @param tags: tags to add to the node
1152 @param dry_run: whether to perform a dry run
1158 query = [("tag", t) for t in tags]
1160 query.append(("dry-run", 1))
1162 return self._SendRequest(HTTP_PUT,
1163 ("/%s/nodes/%s/tags" %
1164 (GANETI_RAPI_VERSION, node)), query, tags)
1166 def DeleteNodeTags(self, node, tags, dry_run=False):
1167 """Delete tags from a node.
1170 @param node: node to remove tags from
1171 @type tags: list of str
1172 @param tags: tags to remove from the node
1174 @param dry_run: whether to perform a dry run
1180 query = [("tag", t) for t in tags]
1182 query.append(("dry-run", 1))
1184 return self._SendRequest(HTTP_DELETE,
1185 ("/%s/nodes/%s/tags" %
1186 (GANETI_RAPI_VERSION, node)), query, None)