Fix error when sorting Job list by object.
[ganeti_webmgr.git] / ganeti_web / util / client.py
blob9dce6e6d9ed87dd5d3db340fd40015d9ef08c490
1 # Copyright (C) 2010, 2011 Google Inc.
2 # Copyright (c) 2012 Oregon State University Open Source Lab
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 # 02110-1301, USA.
20 """
21 Ganeti RAPI client.
22 """
24 # No Ganeti-specific modules should be imported. The RAPI client is supposed
25 # to be standalone.
27 import logging
28 import simplejson as json
29 import socket
31 import requests
34 GANETI_RAPI_PORT = 5080
35 GANETI_RAPI_VERSION = 2
37 REPLACE_DISK_PRI = "replace_on_primary"
38 REPLACE_DISK_SECONDARY = "replace_on_secondary"
39 REPLACE_DISK_CHG = "replace_new_secondary"
40 REPLACE_DISK_AUTO = "replace_auto"
42 NODE_EVAC_PRI = "primary-only"
43 NODE_EVAC_SEC = "secondary-only"
44 NODE_EVAC_ALL = "all"
46 NODE_ROLE_DRAINED = "drained"
47 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
48 NODE_ROLE_MASTER = "master"
49 NODE_ROLE_OFFLINE = "offline"
50 NODE_ROLE_REGULAR = "regular"
52 JOB_STATUS_QUEUED = "queued"
53 JOB_STATUS_WAITING = "waiting"
54 JOB_STATUS_CANCELING = "canceling"
55 JOB_STATUS_RUNNING = "running"
56 JOB_STATUS_CANCELED = "canceled"
57 JOB_STATUS_SUCCESS = "success"
58 JOB_STATUS_ERROR = "error"
59 JOB_STATUS_FINALIZED = frozenset([
60 JOB_STATUS_CANCELED,
61 JOB_STATUS_SUCCESS,
62 JOB_STATUS_ERROR,
64 JOB_STATUS_ALL = frozenset([
65 JOB_STATUS_QUEUED,
66 JOB_STATUS_WAITING,
67 JOB_STATUS_CANCELING,
68 JOB_STATUS_RUNNING,
69 ]) | JOB_STATUS_FINALIZED
71 # Legacy name
72 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
74 # Internal constants
75 _REQ_DATA_VERSION_FIELD = "__version__"
76 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"])
77 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
78 _INST_CREATE_V0_PARAMS = frozenset([
79 "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
80 "hypervisor", "file_storage_dir", "file_driver", "dry_run",
82 _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
84 # Feature strings
85 INST_CREATE_REQV1 = "instance-create-reqv1"
86 INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
87 NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
88 NODE_EVAC_RES1 = "node-evac-res1"
90 # Old feature constant names in case they're references by users of this module
91 _INST_CREATE_REQV1 = INST_CREATE_REQV1
92 _INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
93 _NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
94 _NODE_EVAC_RES1 = NODE_EVAC_RES1
96 headers = {
97 "accept": "application/json",
98 "content-type": "application/json",
99 "user-agent": "Ganeti RAPI Client",
103 class ClientError(Exception):
105 Base error class for this module.
109 class CertificateError(ClientError):
111 Raised when a problem is found with the SSL certificate.
115 class GanetiApiError(ClientError):
117 Generic error raised from Ganeti API.
120 def __init__(self, msg, code=None):
121 ClientError.__init__(self, msg)
122 self.code = code
125 def prepare_query(query):
127 Prepare a query object for the RAPI.
129 RAPI has lots of curious rules for coercing values.
131 This function operates on dicts in-place and has no return value.
133 :type query: dict
134 :param query: Query arguments
137 for name in query:
138 value = query[name]
140 # None is sent as an empty string.
141 if value is None:
142 query[name] = ""
144 # Booleans are sent as 0 or 1.
145 elif isinstance(value, bool):
146 query[name] = int(value)
148 # XXX shouldn't this just check for basestring instead?
149 elif isinstance(value, dict):
150 raise ValueError("Invalid query data type %r" %
151 type(value).__name__)
154 class GanetiRapiClient(object): # pylint: disable-msg=R0904
156 Ganeti RAPI client.
159 _json_encoder = json.JSONEncoder(sort_keys=True)
161 def __init__(self, host, port=GANETI_RAPI_PORT, username=None,
162 password=None, timeout=60, logger=logging):
164 Initializes this class.
166 :type host: string
167 :param host: the ganeti cluster master to interact with
168 :type port: int
169 :param port: the port on which the RAPI is running (default is 5080)
170 :type username: string
171 :param username: the username to connect with
172 :type password: string
173 :param password: the password to connect with
174 :param logger: Logging object
177 if username is not None and password is None:
178 raise ClientError("Password not specified")
179 elif password is not None and username is None:
180 raise ClientError("Specified password without username")
182 self.username = username
183 self.password = password
184 self.timeout = timeout
185 self._logger = logger
187 try:
188 socket.inet_pton(socket.AF_INET6, host)
189 address = "[%s]:%s" % (host, port)
190 # ValueError can happen too, so catch it as well for the IPv4
191 # fallback.
192 except (socket.error, ValueError):
193 address = "%s:%s" % (host, port)
195 self._base_url = "https://%s" % address
197 def _SendRequest(self, method, path, query=None, content=None):
199 Sends an HTTP request.
201 This constructs a full URL, encodes and decodes HTTP bodies, and
202 handles invalid responses in a pythonic way.
204 :type method: string
205 :param method: HTTP method to use
206 :type path: string
207 :param path: HTTP URL path
208 :type query: list of two-tuples
209 :param query: query arguments to pass to urllib.urlencode
210 :type content: str or None
211 :param content: HTTP body content
213 :rtype: object
214 :return: JSON-Decoded response
216 :raises GanetiApiError: If an invalid response is returned
219 if not path.startswith("/"):
220 raise ClientError("Implementation error: Called with bad path %s"
221 % path)
223 kwargs = {
224 "headers": headers,
225 "timeout": self.timeout,
226 "verify": False,
229 if self.username and self.password:
230 kwargs["auth"] = self.username, self.password
232 if content is not None:
233 kwargs["data"] = self._json_encoder.encode(content)
235 if query:
236 prepare_query(query)
237 kwargs["params"] = query
239 url = self._base_url + path
241 self._logger.debug("Sending request to %s %s", url, kwargs)
242 # print "Sending request to %s %s" % (url, kwargs)
244 try:
245 r = requests.request(method, url, **kwargs)
246 except requests.ConnectionError:
247 raise GanetiApiError("Couldn't connect to %s" % self._base_url)
248 except requests.Timeout:
249 raise GanetiApiError("Timed out connecting to %s" %
250 self._base_url)
252 if r.status_code != requests.codes.ok:
253 raise GanetiApiError(str(r.status_code), code=r.status_code)
255 if r.content:
256 return json.loads(r.content)
257 else:
258 return None
260 def GetVersion(self):
262 Gets the Remote API version running on the cluster.
264 :rtype: int
265 :return: Ganeti Remote API version
268 return self._SendRequest("get", "/version")
270 def GetFeatures(self):
272 Gets the list of optional features supported by RAPI server.
274 :rtype: list
275 :return: List of optional features
278 try:
279 return self._SendRequest("get",
280 "/%s/features" % GANETI_RAPI_VERSION)
281 except GanetiApiError, err:
282 # Older RAPI servers don't support this resource. Just return an
283 # empty list.
284 if err.code == requests.codes.not_found:
285 return []
286 else:
287 raise
289 def GetOperatingSystems(self):
291 Gets the Operating Systems running in the Ganeti cluster.
293 :rtype: list of str
294 :return: operating systems
297 return self._SendRequest("get", "/%s/os" % GANETI_RAPI_VERSION)
299 def GetInfo(self):
301 Gets info about the cluster.
303 :rtype: dict
304 :return: information about the cluster
307 return self._SendRequest("get", "/%s/info" % GANETI_RAPI_VERSION,
308 None, None)
310 def RedistributeConfig(self):
312 Tells the cluster to redistribute its configuration files.
314 :return: job id
317 return self._SendRequest("put", "/%s/redistribute-config" %
318 GANETI_RAPI_VERSION)
320 def ModifyCluster(self, **kwargs):
322 Modifies cluster parameters.
324 More details for parameters can be found in the RAPI documentation.
326 :rtype: int
327 :return: job id
330 return self._SendRequest("put", "/%s/modify" % GANETI_RAPI_VERSION,
331 content=kwargs)
333 def GetClusterTags(self):
335 Gets the cluster tags.
337 :rtype: list of str
338 :return: cluster tags
341 return self._SendRequest("get", "/%s/tags" % GANETI_RAPI_VERSION)
343 def AddClusterTags(self, tags, dry_run=False):
345 Adds tags to the cluster.
347 :type tags: list of str
348 :param tags: tags to add to the cluster
349 :type dry_run: bool
350 :param dry_run: whether to perform a dry run
352 :rtype: int
353 :return: job id
356 query = {
357 "dry-run": dry_run,
358 "tag": tags,
361 return self._SendRequest("put", "/%s/tags" % GANETI_RAPI_VERSION,
362 query=query)
364 def DeleteClusterTags(self, tags, dry_run=False):
366 Deletes tags from the cluster.
368 :type tags: list of str
369 :param tags: tags to delete
370 :type dry_run: bool
371 :param dry_run: whether to perform a dry run
374 query = {
375 "dry-run": dry_run,
376 "tag": tags,
379 return self._SendRequest("delete", "/%s/tags" % GANETI_RAPI_VERSION,
380 query=query)
382 def GetInstances(self, bulk=False):
384 Gets information about instances on the cluster.
386 :type bulk: bool
387 :param bulk: whether to return all information about all instances
389 :rtype: list of dict or list of str
390 :return: if bulk is True, info about the instances,
391 else a list of instances
394 if bulk:
395 return self._SendRequest("get", "/%s/instances" %
396 GANETI_RAPI_VERSION, query={"bulk": 1})
397 else:
398 instances = self._SendRequest("get", "/%s/instances" %
399 GANETI_RAPI_VERSION)
400 return [i["id"] for i in instances]
402 def GetInstance(self, instance):
404 Gets information about an instance.
406 :type instance: str
407 :param instance: instance whose info to return
409 :rtype: dict
410 :return: info about the instance
413 return self._SendRequest("get", ("/%s/instances/%s" %
414 (GANETI_RAPI_VERSION, instance)))
416 def GetInstanceInfo(self, instance, static=None):
418 Gets information about an instance.
420 :type instance: string
421 :param instance: Instance name
422 :rtype: string
423 :return: Job ID
426 if static is None:
427 return self._SendRequest("get", ("/%s/instances/%s/info" %
428 (GANETI_RAPI_VERSION, instance)))
429 else:
430 return self._SendRequest("get", ("/%s/instances/%s/info" %
431 (GANETI_RAPI_VERSION, instance)),
432 query={"static": static})
434 def CreateInstance(self, mode, name, disk_template, disks, nics,
435 **kwargs):
437 Creates a new instance.
439 More details for parameters can be found in the RAPI documentation.
441 :type mode: string
442 :param mode: Instance creation mode
443 :type name: string
444 :param name: Hostname of the instance to create
445 :type disk_template: string
446 :param disk_template: Disk template for instance (e.g. plain, diskless,
447 file, or drbd)
448 :type disks: list of dicts
449 :param disks: List of disk definitions
450 :type nics: list of dicts
451 :param nics: List of NIC definitions
452 :type dry_run: bool
453 :keyword dry_run: whether to perform a dry run
454 :type no_install: bool
455 :keyword no_install: whether to create
456 without installing OS(true=don't install)
458 :rtype: int
459 :return: job id
462 if _INST_CREATE_REQV1 not in self.GetFeatures():
463 raise GanetiApiError("Cannot create Ganeti 2.1-style instances")
465 query = {}
467 if kwargs.get("dry_run"):
468 query["dry-run"] = 1
469 if kwargs.get("no_install"):
470 query["no-install"] = 1
472 # Make a version 1 request.
473 body = {
474 _REQ_DATA_VERSION_FIELD: 1,
475 "mode": mode,
476 "name": name,
477 "disk_template": disk_template,
478 "disks": disks,
479 "nics": nics,
482 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
483 if conflicts:
484 raise GanetiApiError("Required fields can not be specified as"
485 " keywords: %s" % ", ".join(conflicts))
487 kwargs.pop("dry_run", None)
488 body.update(kwargs)
490 return self._SendRequest("post", "/%s/instances" %
491 GANETI_RAPI_VERSION, query=query,
492 content=body)
494 def DeleteInstance(self, instance, dry_run=False):
496 Deletes an instance.
498 :type instance: str
499 :param instance: the instance to delete
501 :rtype: int
502 :return: job id
505 return self._SendRequest("delete", ("/%s/instances/%s" %
506 (GANETI_RAPI_VERSION, instance)),
507 query={"dry-run": dry_run})
509 def ModifyInstance(self, instance, **kwargs):
511 Modifies an instance.
513 More details for parameters can be found in the RAPI documentation.
515 :type instance: string
516 :param instance: Instance name
517 :rtype: int
518 :return: job id
521 return self._SendRequest("put", ("/%s/instances/%s/modify" %
522 (GANETI_RAPI_VERSION, instance)),
523 content=kwargs)
525 def ActivateInstanceDisks(self, instance, ignore_size=False):
527 Activates an instance's disks.
529 :type instance: string
530 :param instance: Instance name
531 :type ignore_size: bool
532 :param ignore_size: Whether to ignore recorded size
533 :return: job id
536 return self._SendRequest("put", ("/%s/instances/%s/activate-disks" %
537 (GANETI_RAPI_VERSION, instance)),
538 query={"ignore_size": ignore_size})
540 def DeactivateInstanceDisks(self, instance):
542 Deactivates an instance's disks.
544 :type instance: string
545 :param instance: Instance name
546 :return: job id
549 return self._SendRequest("put", ("/%s/instances/%s/deactivate-disks" %
550 (GANETI_RAPI_VERSION, instance)))
552 def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
553 """Recreate an instance's disks.
555 :type instance: string
556 :param instance: Instance name
557 :type disks: list of int
558 :param disks: List of disk indexes
559 :type nodes: list of string
560 :param nodes: New instance nodes, if relocation is desired
561 :rtype: string
562 :return: job id
565 body = {}
567 if disks is not None:
568 body["disks"] = disks
569 if nodes is not None:
570 body["nodes"] = nodes
572 return self._SendRequest("post", ("/%s/instances/%s/recreate-disks" %
573 (GANETI_RAPI_VERSION, instance)),
574 content=body)
576 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=False):
578 Grows a disk of an instance.
580 More details for parameters can be found in the RAPI documentation.
582 :type instance: string
583 :param instance: Instance name
584 :type disk: integer
585 :param disk: Disk index
586 :type amount: integer
587 :param amount: Grow disk by this amount (MiB)
588 :type wait_for_sync: bool
589 :param wait_for_sync: Wait for disk to synchronize
590 :rtype: int
591 :return: job id
594 body = {
595 "amount": amount,
596 "wait_for_sync": wait_for_sync,
599 return self._SendRequest("post", ("/%s/instances/%s/disk/%s/grow" %
600 (GANETI_RAPI_VERSION, instance,
601 disk)), content=body)
603 def GetInstanceTags(self, instance):
605 Gets tags for an instance.
607 :type instance: str
608 :param instance: instance whose tags to return
610 :rtype: list of str
611 :return: tags for the instance
614 return self._SendRequest("get", ("/%s/instances/%s/tags" %
615 (GANETI_RAPI_VERSION, instance)))
617 def AddInstanceTags(self, instance, tags, dry_run=False):
619 Adds tags to an instance.
621 :type instance: str
622 :param instance: instance to add tags to
623 :type tags: list of str
624 :param tags: tags to add to the instance
625 :type dry_run: bool
626 :param dry_run: whether to perform a dry run
628 :rtype: int
629 :return: job id
632 query = {
633 "tag": tags,
634 "dry-run": dry_run,
637 return self._SendRequest("put", ("/%s/instances/%s/tags" %
638 (GANETI_RAPI_VERSION, instance)),
639 query=query)
641 def DeleteInstanceTags(self, instance, tags, dry_run=False):
643 Deletes tags from an instance.
645 :type instance: str
646 :param instance: instance to delete tags from
647 :type tags: list of str
648 :param tags: tags to delete
649 :type dry_run: bool
650 :param dry_run: whether to perform a dry run
653 query = {
654 "tag": tags,
655 "dry-run": dry_run,
658 return self._SendRequest("delete", ("/%s/instances/%s/tags" %
659 (GANETI_RAPI_VERSION, instance)),
660 query=query)
662 def RebootInstance(self, instance, reboot_type=None,
663 ignore_secondaries=False, dry_run=False):
665 Reboots an instance.
667 :type instance: str
668 :param instance: instance to rebot
669 :type reboot_type: str
670 :param reboot_type: one of: hard, soft, full
671 :type ignore_secondaries: bool
672 :param ignore_secondaries: if True, ignores errors
673 for the secondary node
674 while re-assembling disks (in hard-reboot mode only)
675 :type dry_run: bool
676 :param dry_run: whether to perform a dry run
679 query = {
680 "ignore_secondaries": ignore_secondaries,
681 "dry-run": dry_run,
684 if reboot_type:
685 if reboot_type not in ("hard", "soft", "full"):
686 raise GanetiApiError("reboot_type must be one of 'hard',"
687 " 'soft', or 'full'")
688 query["type"] = reboot_type
690 return self._SendRequest("post", ("/%s/instances/%s/reboot" %
691 (GANETI_RAPI_VERSION, instance)),
692 query=query)
694 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
695 timeout=120):
697 Shuts down an instance.
699 :type instance: str
700 :param instance: the instance to shut down
701 :type dry_run: bool
702 :param dry_run: whether to perform a dry run
703 :type no_remember: bool
704 :param no_remember: if true, will not record the state change
705 :rtype: string
706 :return: job id
709 query = {
710 "dry-run": dry_run,
711 "no-remember": no_remember,
714 content = {
715 "timeout": timeout,
718 return self._SendRequest("put", ("/%s/instances/%s/shutdown" %
719 (GANETI_RAPI_VERSION, instance)),
720 query=query, content=content)
722 def StartupInstance(self, instance, dry_run=False, no_remember=False):
724 Starts up an instance.
726 :type instance: str
727 :param instance: the instance to start up
728 :type dry_run: bool
729 :param dry_run: whether to perform a dry run
730 :type no_remember: bool
731 :param no_remember: if true, will not record the state change
732 :rtype: string
733 :return: job id
736 query = {
737 "dry-run": dry_run,
738 "no-remember": no_remember,
741 return self._SendRequest("put", ("/%s/instances/%s/startup" %
742 (GANETI_RAPI_VERSION, instance)),
743 query=query)
745 def ReinstallInstance(self, instance, os=None, no_startup=False,
746 osparams=None):
748 Reinstalls an instance.
750 :type instance: str
751 :param instance: The instance to reinstall
752 :type os: str or None
753 :param os: The operating system to reinstall. If None, the instance's
754 current operating system will be installed again
755 :type no_startup: bool
756 :param no_startup: Whether to start the instance automatically
759 if _INST_REINSTALL_REQV1 in self.GetFeatures():
760 body = {
761 "start": not no_startup,
763 if os is not None:
764 body["os"] = os
765 if osparams is not None:
766 body["osparams"] = osparams
767 return self._SendRequest("post", ("/%s/instances/%s/reinstall" %
768 (GANETI_RAPI_VERSION,
769 instance)), content=body)
771 # Use old request format
772 if osparams:
773 raise GanetiApiError("Server does not support specifying OS"
774 " parameters for instance reinstallation")
776 query = {
777 "nostartup": no_startup,
780 if os:
781 query["os"] = os
783 return self._SendRequest("post", ("/%s/instances/%s/reinstall" %
784 (GANETI_RAPI_VERSION, instance)),
785 query=query)
787 def ReplaceInstanceDisks(self, instance, disks=None,
788 mode=REPLACE_DISK_AUTO, remote_node=None,
789 iallocator=None, dry_run=False):
791 Replaces disks on an instance.
793 :type instance: str
794 :param instance: instance whose disks to replace
795 :type disks: list of ints
796 :param disks: Indexes of disks to replace
797 :type mode: str
798 :param mode: replacement mode to use (defaults to replace_auto)
799 :type remote_node: str or None
800 :param remote_node: new secondary node to use (for use with
801 replace_new_secondary mode)
802 :type iallocator: str or None
803 :param iallocator: instance allocator plugin to use (for use with
804 replace_auto mode)
805 :type dry_run: bool
806 :param dry_run: whether to perform a dry run
808 :rtype: int
809 :return: job id
812 query = {
813 "mode": mode,
814 "dry-run": dry_run,
817 if disks:
818 query["disks"] = ",".join(str(idx) for idx in disks)
820 if remote_node:
821 query["remote_node"] = remote_node
823 if iallocator:
824 query["iallocator"] = iallocator
826 return self._SendRequest("post", ("/%s/instances/%s/replace-disks" %
827 (GANETI_RAPI_VERSION, instance)),
828 query=query)
830 def PrepareExport(self, instance, mode):
832 Prepares an instance for an export.
834 :type instance: string
835 :param instance: Instance name
836 :type mode: string
837 :param mode: Export mode
838 :rtype: string
839 :return: Job ID
842 return self._SendRequest("put", ("/%s/instances/%s/prepare-export" %
843 (GANETI_RAPI_VERSION, instance)),
844 query={"mode": mode})
846 def ExportInstance(self, instance, mode, destination, shutdown=None,
847 remove_instance=None, x509_key_name=None,
848 destination_x509_ca=None):
850 Exports an instance.
852 :type instance: string
853 :param instance: Instance name
854 :type mode: string
855 :param mode: Export mode
856 :rtype: string
857 :return: Job ID
860 body = {
861 "destination": destination,
862 "mode": mode,
865 if shutdown is not None:
866 body["shutdown"] = shutdown
868 if remove_instance is not None:
869 body["remove_instance"] = remove_instance
871 if x509_key_name is not None:
872 body["x509_key_name"] = x509_key_name
874 if destination_x509_ca is not None:
875 body["destination_x509_ca"] = destination_x509_ca
877 return self._SendRequest("put", ("/%s/instances/%s/export" %
878 (GANETI_RAPI_VERSION, instance)),
879 content=body)
881 def MigrateInstance(self, instance, mode=None, cleanup=None):
883 Migrates an instance.
885 :type instance: string
886 :param instance: Instance name
887 :type mode: string
888 :param mode: Migration mode
889 :type cleanup: bool
890 :param cleanup: Whether to clean up a previously failed migration
893 body = {}
895 if mode is not None:
896 body["mode"] = mode
898 if cleanup is not None:
899 body["cleanup"] = cleanup
901 return self._SendRequest("put", ("/%s/instances/%s/migrate" %
902 (GANETI_RAPI_VERSION, instance)),
903 content=body)
905 def FailoverInstance(self, instance, iallocator=None,
906 ignore_consistency=False, target_node=None):
907 """Does a failover of an instance.
909 :type instance: string
910 :param instance: Instance name
911 :type iallocator: string
912 :param iallocator: Iallocator for deciding the target node for
913 shared-storage instances
914 :type ignore_consistency: bool
915 :param ignore_consistency: Whether to ignore disk consistency
916 :type target_node: string
917 :param target_node: Target node for shared-storage instances
918 :rtype: string
919 :return: job id
922 body = {
923 "ignore_consistency": ignore_consistency,
926 if iallocator is not None:
927 body["iallocator"] = iallocator
928 if target_node is not None:
929 body["target_node"] = target_node
931 return self._SendRequest("put", ("/%s/instances/%s/failover" %
932 (GANETI_RAPI_VERSION, instance)),
933 content=body)
935 def RenameInstance(self, instance, new_name, ip_check,
936 name_check=None):
938 Changes the name of an instance.
940 :type instance: string
941 :param instance: Instance name
942 :type new_name: string
943 :param new_name: New instance name
944 :type ip_check: bool
945 :param ip_check: Whether to ensure instance's IP address is inactive
946 :type name_check: bool
947 :param name_check: Whether to ensure instance's name is resolvable
950 body = {
951 "ip_check": ip_check,
952 "new_name": new_name,
955 if name_check is not None:
956 body["name_check"] = name_check
958 return self._SendRequest("put", ("/%s/instances/%s/rename" %
959 (GANETI_RAPI_VERSION, instance)),
960 content=body)
962 def GetInstanceConsole(self, instance):
964 Request information for connecting to instance's console.
966 :type instance: string
967 :param instance: Instance name
970 return self._SendRequest("get", ("/%s/instances/%s/console" %
971 (GANETI_RAPI_VERSION, instance)))
973 def GetJobs(self):
975 Gets all jobs for the cluster.
977 :rtype: list of int
978 :return: job ids for the cluster
981 jobs = self._SendRequest("get", "/%s/jobs" % GANETI_RAPI_VERSION)
983 return [int(job["id"]) for job in jobs]
985 def GetJobStatus(self, job_id):
987 Gets the status of a job.
989 :type job_id: int
990 :param job_id: job id whose status to query
992 :rtype: dict
993 :return: job status
996 return self._SendRequest("get", "/%s/jobs/%s" % (GANETI_RAPI_VERSION,
997 job_id))
999 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1001 Waits for job changes.
1003 :type job_id: int
1004 :param job_id: Job ID for which to wait
1007 body = {
1008 "fields": fields,
1009 "previous_job_info": prev_job_info,
1010 "previous_log_serial": prev_log_serial,
1013 return self._SendRequest("get", "/%s/jobs/%s/wait" %
1014 (GANETI_RAPI_VERSION, job_id), content=body)
1016 def CancelJob(self, job_id, dry_run=False):
1018 Cancels a job.
1020 :type job_id: int
1021 :param job_id: id of the job to delete
1022 :type dry_run: bool
1023 :param dry_run: whether to perform a dry run
1026 return self._SendRequest("delete", "/%s/jobs/%s" %
1027 (GANETI_RAPI_VERSION, job_id),
1028 query={"dry-run": dry_run})
1030 def GetNodes(self, bulk=False):
1032 Gets all nodes in the cluster.
1034 :type bulk: bool
1035 :param bulk: whether to return all information about all instances
1037 :rtype: list of dict or str
1038 :return: if bulk is true, info about nodes in the cluster,
1039 else list of nodes in the cluster
1042 if bulk:
1043 return self._SendRequest("get", "/%s/nodes" % GANETI_RAPI_VERSION,
1044 query={"bulk": 1})
1045 else:
1046 nodes = self._SendRequest("get", "/%s/nodes" %
1047 GANETI_RAPI_VERSION)
1048 return [n["id"] for n in nodes]
1050 def GetNode(self, node):
1052 Gets information about a node.
1054 :type node: str
1055 :param node: node whose info to return
1057 :rtype: dict
1058 :return: info about the node
1061 return self._SendRequest("get", "/%s/nodes/%s" % (GANETI_RAPI_VERSION,
1062 node))
1064 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1065 dry_run=False, early_release=False, mode=None,
1066 accept_old=False):
1068 Evacuates instances from a Ganeti node.
1070 :type node: str
1071 :param node: node to evacuate
1072 :type iallocator: str or None
1073 :param iallocator: instance allocator to use
1074 :type remote_node: str
1075 :param remote_node: node to evaucate to
1076 :type dry_run: bool
1077 :param dry_run: whether to perform a dry run
1078 :type early_release: bool
1079 :param early_release: whether to enable parallelization
1080 :type accept_old: bool
1081 :param accept_old: Whether caller is ready to accept old-style
1082 (pre-2.5) results
1084 :rtype: string, or a list for pre-2.5 results
1085 :return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1086 list of (job ID, instance name, new secondary node); if dry_run
1087 was specified, then the actual move jobs were not submitted and
1088 the job IDs will be C{None}
1090 :raises GanetiApiError: if an iallocator and remote_node are both
1091 specified
1094 if iallocator and remote_node:
1095 raise GanetiApiError("Only one of iallocator or remote_node can"
1096 " be used")
1098 query = {
1099 "dry-run": dry_run,
1102 if iallocator:
1103 query["iallocator"] = iallocator
1104 if remote_node:
1105 query["remote_node"] = remote_node
1107 if _NODE_EVAC_RES1 in self.GetFeatures():
1108 # Server supports body parameters
1109 body = {
1110 "early_release": early_release,
1113 if iallocator is not None:
1114 body["iallocator"] = iallocator
1115 if remote_node is not None:
1116 body["remote_node"] = remote_node
1117 if mode is not None:
1118 body["mode"] = mode
1119 else:
1120 # Pre-2.5 request format
1121 body = None
1123 if not accept_old:
1124 raise GanetiApiError("Server is version 2.4 or earlier and"
1125 " caller does not accept old-style"
1126 " results (parameter accept_old)")
1128 # Pre-2.5 servers can only evacuate secondaries
1129 if mode is not None and mode != NODE_EVAC_SEC:
1130 raise GanetiApiError("Server can only evacuate "
1131 "secondary instances")
1133 if iallocator is not None:
1134 query["iallocator"] = iallocator
1135 if remote_node is not None:
1136 query["remote_node"] = remote_node
1137 if query:
1138 query["early_release"] = 1
1140 return self._SendRequest("post", ("/%s/nodes/%s/evacuate" %
1141 (GANETI_RAPI_VERSION, node)),
1142 query=query, content=body)
1144 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1145 target_node=None):
1147 Migrates all primary instances from a node.
1149 :type node: str
1150 :param node: node to migrate
1151 :type mode: string
1152 :param mode: if passed, it will overwrite the live migration type,
1153 otherwise the hypervisor default will be used
1154 :type dry_run: bool
1155 :param dry_run: whether to perform a dry run
1156 :type iallocator: string
1157 :param iallocator: instance allocator to use
1158 :type target_node: string
1159 :param target_node: Target node for shared-storage instances
1161 :rtype: int
1162 :return: job id
1165 query = {
1166 "dry-run": dry_run,
1169 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1170 body = {}
1172 if mode is not None:
1173 body["mode"] = mode
1174 if iallocator is not None:
1175 body["iallocator"] = iallocator
1176 if target_node is not None:
1177 body["target_node"] = target_node
1179 else:
1180 # Use old request format
1181 if target_node is not None:
1182 raise GanetiApiError("Server does not support specifying"
1183 " target node for node migration")
1185 body = None
1187 if mode is not None:
1188 query["mode"] = mode
1190 return self._SendRequest("post", ("/%s/nodes/%s/migrate" %
1191 (GANETI_RAPI_VERSION, node)),
1192 query=query, content=body)
1194 def GetNodeRole(self, node):
1196 Gets the current role for a node.
1198 :type node: str
1199 :param node: node whose role to return
1201 :rtype: str
1202 :return: the current role for a node
1205 return self._SendRequest("get", ("/%s/nodes/%s/role" %
1206 (GANETI_RAPI_VERSION, node)))
1208 def SetNodeRole(self, node, role, force=False, auto_promote=False):
1210 Sets the role for a node.
1212 :type node: str
1213 :param node: the node whose role to set
1214 :type role: str
1215 :param role: the role to set for the node
1216 :type force: bool
1217 :param force: whether to force the role change
1218 :type auto_promote: bool
1219 :param auto_promote: Whether node(s) should be promoted to master
1220 candidate if necessary
1222 :rtype: int
1223 :return: job id
1226 query = {
1227 "force": force,
1228 "auto_promote": auto_promote,
1231 return self._SendRequest("put", ("/%s/nodes/%s/role" %
1232 (GANETI_RAPI_VERSION, node)),
1233 query=query, content=role)
1235 def PowercycleNode(self, node, force=False):
1237 Powercycles a node.
1239 :type node: string
1240 :param node: Node name
1241 :type force: bool
1242 :param force: Whether to force the operation
1243 :rtype: string
1244 :return: job id
1247 query = {
1248 "force": force,
1251 return self._SendRequest("post", ("/%s/nodes/%s/powercycle" %
1252 (GANETI_RAPI_VERSION, node)),
1253 query=query)
1255 def ModifyNode(self, node, **kwargs):
1257 Modifies a node.
1259 More details for parameters can be found in the RAPI documentation.
1261 :type node: string
1262 :param node: Node name
1263 :rtype: string
1264 :return: job id
1267 return self._SendRequest("post", ("/%s/nodes/%s/modify" %
1268 (GANETI_RAPI_VERSION, node)),
1269 content=kwargs)
1271 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1273 Gets the storage units for a node.
1275 :type node: str
1276 :param node: the node whose storage units to return
1277 :type storage_type: str
1278 :param storage_type: storage type whose units to return
1279 :type output_fields: str
1280 :param output_fields: storage type fields to return
1282 :rtype: int
1283 :return: job id where results can be retrieved
1286 query = {
1287 "storage_type": storage_type,
1288 "output_fields": output_fields,
1291 return self._SendRequest("get", ("/%s/nodes/%s/storage" %
1292 (GANETI_RAPI_VERSION, node)),
1293 query=query)
1295 def ModifyNodeStorageUnits(self, node, storage_type, name,
1296 allocatable=None):
1298 Modifies parameters of storage units on the node.
1300 :type node: str
1301 :param node: node whose storage units to modify
1302 :type storage_type: str
1303 :param storage_type: storage type whose units to modify
1304 :type name: str
1305 :param name: name of the storage unit
1306 :type allocatable: bool or None
1307 :param allocatable: Whether to set the "allocatable"
1308 flag on the storage
1309 unit (None=no modification, True=set, False=unset)
1311 :rtype: int
1312 :return: job id
1315 query = {
1316 "storage_type": storage_type,
1317 "name": name,
1320 if allocatable is not None:
1321 query["allocatable"] = allocatable
1323 return self._SendRequest("put", ("/%s/nodes/%s/storage/modify" %
1324 (GANETI_RAPI_VERSION, node)),
1325 query=query)
1327 def RepairNodeStorageUnits(self, node, storage_type, name):
1329 Repairs a storage unit on the node.
1331 :type node: str
1332 :param node: node whose storage units to repair
1333 :type storage_type: str
1334 :param storage_type: storage type to repair
1335 :type name: str
1336 :param name: name of the storage unit to repair
1338 :rtype: int
1339 :return: job id
1342 query = {
1343 "storage_type": storage_type,
1344 "name": name,
1347 return self._SendRequest("put", ("/%s/nodes/%s/storage/repair" %
1348 (GANETI_RAPI_VERSION, node)),
1349 query=query)
1351 def GetNodeTags(self, node):
1353 Gets the tags for a node.
1355 :type node: str
1356 :param node: node whose tags to return
1358 :rtype: list of str
1359 :return: tags for the node
1362 return self._SendRequest("get", ("/%s/nodes/%s/tags" %
1363 (GANETI_RAPI_VERSION, node)))
1365 def AddNodeTags(self, node, tags, dry_run=False):
1367 Adds tags to a node.
1369 :type node: str
1370 :param node: node to add tags to
1371 :type tags: list of str
1372 :param tags: tags to add to the node
1373 :type dry_run: bool
1374 :param dry_run: whether to perform a dry run
1376 :rtype: int
1377 :return: job id
1380 query = {
1381 "tag": tags,
1382 "dry-run": dry_run,
1385 return self._SendRequest("put", ("/%s/nodes/%s/tags" %
1386 (GANETI_RAPI_VERSION, node)),
1387 query=query, content=tags)
1389 def DeleteNodeTags(self, node, tags, dry_run=False):
1391 Delete tags from a node.
1393 :type node: str
1394 :param node: node to remove tags from
1395 :type tags: list of str
1396 :param tags: tags to remove from the node
1397 :type dry_run: bool
1398 :param dry_run: whether to perform a dry run
1400 :rtype: int
1401 :return: job id
1404 query = {
1405 "tag": tags,
1406 "dry-run": dry_run,
1409 return self._SendRequest("delete", ("/%s/nodes/%s/tags" %
1410 (GANETI_RAPI_VERSION, node)),
1411 query=query)
1413 def GetGroups(self, bulk=False):
1415 Gets all node groups in the cluster.
1417 :type bulk: bool
1418 :param bulk: whether to return all information about the groups
1420 :rtype: list of dict or str
1421 :return: if bulk is true, a list of dictionaries
1422 with info about all node groups
1423 in the cluster, else a list of names of those node groups
1426 if bulk:
1427 return self._SendRequest("get", "/%s/groups" %
1428 GANETI_RAPI_VERSION, query={"bulk": 1})
1429 else:
1430 groups = self._SendRequest("get", "/%s/groups" %
1431 GANETI_RAPI_VERSION)
1432 return [g["name"] for g in groups]
1434 def GetGroup(self, group):
1436 Gets information about a node group.
1438 :type group: str
1439 :param group: name of the node group whose info to return
1441 :rtype: dict
1442 :return: info about the node group
1445 return self._SendRequest("get", "/%s/groups/%s" %
1446 (GANETI_RAPI_VERSION, group))
1448 def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1450 Creates a new node group.
1452 :type name: str
1453 :param name: the name of node group to create
1454 :type alloc_policy: str
1455 :param alloc_policy: the desired allocation
1456 policy for the group, if any
1457 :type dry_run: bool
1458 :param dry_run: whether to peform a dry run
1460 :rtype: int
1461 :return: job id
1464 query = {
1465 "dry-run": dry_run,
1468 body = {
1469 "name": name,
1470 "alloc_policy": alloc_policy
1473 return self._SendRequest("post", "/%s/groups" % GANETI_RAPI_VERSION,
1474 query=query, content=body)
1476 def ModifyGroup(self, group, **kwargs):
1478 Modifies a node group.
1480 More details for parameters can be found in the RAPI documentation.
1482 :type group: string
1483 :param group: Node group name
1484 :rtype: int
1485 :return: job id
1488 return self._SendRequest("put", ("/%s/groups/%s/modify" %
1489 (GANETI_RAPI_VERSION, group)),
1490 content=kwargs)
1492 def DeleteGroup(self, group, dry_run=False):
1494 Deletes a node group.
1496 :type group: str
1497 :param group: the node group to delete
1498 :type dry_run: bool
1499 :param dry_run: whether to peform a dry run
1501 :rtype: int
1502 :return: job id
1505 query = {
1506 "dry-run": dry_run,
1509 return self._SendRequest("delete", ("/%s/groups/%s" %
1510 (GANETI_RAPI_VERSION, group)),
1511 query=query)
1513 def RenameGroup(self, group, new_name):
1515 Changes the name of a node group.
1517 :type group: string
1518 :param group: Node group name
1519 :type new_name: string
1520 :param new_name: New node group name
1522 :rtype: int
1523 :return: job id
1526 body = {
1527 "new_name": new_name,
1530 return self._SendRequest("put", ("/%s/groups/%s/rename" %
1531 (GANETI_RAPI_VERSION, group)),
1532 content=body)
1534 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1536 Assigns nodes to a group.
1538 :type group: string
1539 :param group: Node gropu name
1540 :type nodes: list of strings
1541 :param nodes: List of nodes to assign to the group
1543 :rtype: int
1544 :return: job id
1548 query = {
1549 "force": force,
1550 "dry-run": dry_run,
1553 body = {
1554 "nodes": nodes,
1557 return self._SendRequest("put", ("/%s/groups/%s/assign-nodes" %
1558 (GANETI_RAPI_VERSION, group)),
1559 query=query, content=body)
1561 def GetGroupTags(self, group):
1563 Gets tags for a node group.
1565 :type group: string
1566 :param group: Node group whose tags to return
1568 :rtype: list of strings
1569 :return: tags for the group
1572 return self._SendRequest("get", ("/%s/groups/%s/tags" %
1573 (GANETI_RAPI_VERSION, group)))
1575 def AddGroupTags(self, group, tags, dry_run=False):
1577 Adds tags to a node group.
1579 :type group: str
1580 :param group: group to add tags to
1581 :type tags: list of string
1582 :param tags: tags to add to the group
1583 :type dry_run: bool
1584 :param dry_run: whether to perform a dry run
1586 :rtype: string
1587 :return: job id
1590 query = {
1591 "dry-run": dry_run,
1592 "tag": tags,
1595 return self._SendRequest("put", ("/%s/groups/%s/tags" %
1596 (GANETI_RAPI_VERSION, group)),
1597 query=query)
1599 def DeleteGroupTags(self, group, tags, dry_run=False):
1601 Deletes tags from a node group.
1603 :type group: str
1604 :param group: group to delete tags from
1605 :type tags: list of string
1606 :param tags: tags to delete
1607 :type dry_run: bool
1608 :param dry_run: whether to perform a dry run
1609 :rtype: string
1610 :return: job id
1613 query = {
1614 "dry-run": dry_run,
1615 "tag": tags,
1618 return self._SendRequest("delete", ("/%s/groups/%s/tags" %
1619 (GANETI_RAPI_VERSION, group)),
1620 query=query)
1622 def Query(self, what, fields, qfilter=None):
1624 Retrieves information about resources.
1626 :type what: string
1627 :param what: Resource name, one of L{constants.QR_VIA_RAPI}
1628 :type fields: list of string
1629 :param fields: Requested fields
1630 :type qfilter: None or list
1631 :param qfilter: Query filter
1633 :rtype: string
1634 :return: job id
1637 body = {
1638 "fields": fields,
1641 if qfilter is not None:
1642 body["qfilter"] = body["filter"] = qfilter
1644 return self._SendRequest("put", ("/%s/query/%s" %
1645 (GANETI_RAPI_VERSION, what)),
1646 content=body)
1648 def QueryFields(self, what, fields=None):
1650 Retrieves available fields for a resource.
1652 :type what: string
1653 :param what: Resource name, one of L{constants.QR_VIA_RAPI}
1654 :type fields: list of string
1655 :param fields: Requested fields
1657 :rtype: string
1658 :return: job id
1661 query = {}
1663 if fields is not None:
1664 query["fields"] = ",".join(fields)
1666 return self._SendRequest("get", ("/%s/query/%s/fields" %
1667 (GANETI_RAPI_VERSION, what)),
1668 query=query)