Revision a3ad611d
b/snf-common/synnefo/util/rapi.py | ||
---|---|---|
1 | 1 |
# |
2 | 2 |
# |
3 | 3 |
|
4 |
# Copyright (C) 2010 Google Inc. |
|
4 |
# Copyright (C) 2010, 2011 Google Inc.
|
|
5 | 5 |
# |
6 | 6 |
# This program is free software; you can redistribute it and/or modify |
7 | 7 |
# it under the terms of the GNU General Public License as published by |
... | ... | |
34 | 34 |
# be standalone. |
35 | 35 |
|
36 | 36 |
import logging |
37 |
import simplejson |
|
37 | 38 |
import socket |
38 | 39 |
import urllib |
39 | 40 |
import threading |
40 | 41 |
import pycurl |
41 |
|
|
42 |
try: |
|
43 |
import simplejson as json |
|
44 |
except ImportError: |
|
45 |
import json |
|
42 |
import time |
|
46 | 43 |
|
47 | 44 |
try: |
48 | 45 |
from cStringIO import StringIO |
... | ... | |
66 | 63 |
REPLACE_DISK_CHG = "replace_new_secondary" |
67 | 64 |
REPLACE_DISK_AUTO = "replace_auto" |
68 | 65 |
|
66 |
NODE_EVAC_PRI = "primary-only" |
|
67 |
NODE_EVAC_SEC = "secondary-only" |
|
68 |
NODE_EVAC_ALL = "all" |
|
69 |
|
|
69 | 70 |
NODE_ROLE_DRAINED = "drained" |
70 | 71 |
NODE_ROLE_MASTER_CANDIATE = "master-candidate" |
71 | 72 |
NODE_ROLE_MASTER = "master" |
72 | 73 |
NODE_ROLE_OFFLINE = "offline" |
73 | 74 |
NODE_ROLE_REGULAR = "regular" |
74 | 75 |
|
76 |
JOB_STATUS_QUEUED = "queued" |
|
77 |
JOB_STATUS_WAITING = "waiting" |
|
78 |
JOB_STATUS_CANCELING = "canceling" |
|
79 |
JOB_STATUS_RUNNING = "running" |
|
80 |
JOB_STATUS_CANCELED = "canceled" |
|
81 |
JOB_STATUS_SUCCESS = "success" |
|
82 |
JOB_STATUS_ERROR = "error" |
|
83 |
JOB_STATUS_FINALIZED = frozenset([ |
|
84 |
JOB_STATUS_CANCELED, |
|
85 |
JOB_STATUS_SUCCESS, |
|
86 |
JOB_STATUS_ERROR, |
|
87 |
]) |
|
88 |
JOB_STATUS_ALL = frozenset([ |
|
89 |
JOB_STATUS_QUEUED, |
|
90 |
JOB_STATUS_WAITING, |
|
91 |
JOB_STATUS_CANCELING, |
|
92 |
JOB_STATUS_RUNNING, |
|
93 |
]) | JOB_STATUS_FINALIZED |
|
94 |
|
|
95 |
# Legacy name |
|
96 |
JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING |
|
97 |
|
|
75 | 98 |
# Internal constants |
76 | 99 |
_REQ_DATA_VERSION_FIELD = "__version__" |
77 |
_INST_CREATE_REQV1 = "instance-create-reqv1" |
|
78 |
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" |
|
79 |
_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"]) |
|
80 |
_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"]) |
|
81 |
_INST_CREATE_V0_PARAMS = frozenset([ |
|
82 |
"os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check", |
|
83 |
"hypervisor", "file_storage_dir", "file_driver", "dry_run", |
|
84 |
]) |
|
85 |
_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"]) |
|
100 |
_QPARAM_DRY_RUN = "dry-run" |
|
101 |
_QPARAM_FORCE = "force" |
|
102 |
|
|
103 |
# Feature strings |
|
104 |
INST_CREATE_REQV1 = "instance-create-reqv1" |
|
105 |
INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" |
|
106 |
NODE_MIGRATE_REQV1 = "node-migrate-reqv1" |
|
107 |
NODE_EVAC_RES1 = "node-evac-res1" |
|
108 |
|
|
109 |
# Old feature constant names in case they're references by users of this module |
|
110 |
_INST_CREATE_REQV1 = INST_CREATE_REQV1 |
|
111 |
_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1 |
|
112 |
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1 |
|
113 |
_NODE_EVAC_RES1 = NODE_EVAC_RES1 |
|
86 | 114 |
|
87 | 115 |
# Older pycURL versions don't have all error constants |
88 | 116 |
try: |
... | ... | |
105 | 133 |
pass |
106 | 134 |
|
107 | 135 |
|
108 |
class CertificateError(Error): |
|
136 |
class GanetiApiError(Error): |
|
137 |
"""Generic error raised from Ganeti API. |
|
138 |
|
|
139 |
""" |
|
140 |
def __init__(self, msg, code=None): |
|
141 |
Error.__init__(self, msg) |
|
142 |
self.code = code |
|
143 |
|
|
144 |
|
|
145 |
class CertificateError(GanetiApiError): |
|
109 | 146 |
"""Raised when a problem is found with the SSL certificate. |
110 | 147 |
|
111 | 148 |
""" |
112 | 149 |
pass |
113 | 150 |
|
114 | 151 |
|
115 |
class GanetiApiError(Error):
|
|
116 |
"""Generic error raised from Ganeti API.
|
|
152 |
def _AppendIf(container, condition, value):
|
|
153 |
"""Appends to a list if a condition evaluates to truth.
|
|
117 | 154 |
|
118 | 155 |
""" |
119 |
def __init__(self, msg, code=None): |
|
120 |
Error.__init__(self, msg) |
|
121 |
self.code = code |
|
156 |
if condition: |
|
157 |
container.append(value) |
|
158 |
|
|
159 |
return condition |
|
160 |
|
|
161 |
|
|
162 |
def _AppendDryRunIf(container, condition): |
|
163 |
"""Appends a "dry-run" parameter if a condition evaluates to truth. |
|
164 |
|
|
165 |
""" |
|
166 |
return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1)) |
|
167 |
|
|
168 |
|
|
169 |
def _AppendForceIf(container, condition): |
|
170 |
"""Appends a "force" parameter if a condition evaluates to truth. |
|
171 |
|
|
172 |
""" |
|
173 |
return _AppendIf(container, condition, (_QPARAM_FORCE, 1)) |
|
174 |
|
|
175 |
|
|
176 |
def _SetItemIf(container, condition, item, value): |
|
177 |
"""Sets an item if a condition evaluates to truth. |
|
178 |
|
|
179 |
""" |
|
180 |
if condition: |
|
181 |
container[item] = value |
|
182 |
|
|
183 |
return condition |
|
122 | 184 |
|
123 | 185 |
|
124 | 186 |
def UsesRapiClient(fn): |
... | ... | |
240 | 302 |
return _ConfigCurl |
241 | 303 |
|
242 | 304 |
|
243 |
class GanetiRapiClient(object): # pylint: disable-msg=R0904
|
|
305 |
class GanetiRapiClient(object): # pylint: disable=R0904 |
|
244 | 306 |
"""Ganeti RAPI client. |
245 | 307 |
|
246 | 308 |
""" |
247 | 309 |
USER_AGENT = "Ganeti RAPI Client" |
248 |
_json_encoder = json.JSONEncoder(sort_keys=True) |
|
310 |
_json_encoder = simplejson.JSONEncoder(sort_keys=True)
|
|
249 | 311 |
|
250 | 312 |
def __init__(self, host, port=GANETI_RAPI_PORT, |
251 |
username=None, password=None, |
|
252 |
logger=logging.getLogger('synnefo.util'), |
|
313 |
username=None, password=None, logger=logging, |
|
253 | 314 |
curl_config_fn=None, curl_factory=None): |
254 | 315 |
"""Initializes this class. |
255 | 316 |
|
... | ... | |
409 | 470 |
curl.perform() |
410 | 471 |
except pycurl.error, err: |
411 | 472 |
if err.args[0] in _CURL_SSL_CERT_ERRORS: |
412 |
raise CertificateError("SSL certificate error %s" % err) |
|
473 |
raise CertificateError("SSL certificate error %s" % err, |
|
474 |
code=err.args[0]) |
|
413 | 475 |
|
414 |
raise GanetiApiError(str(err)) |
|
476 |
raise GanetiApiError(str(err), code=err.args[0])
|
|
415 | 477 |
finally: |
416 | 478 |
# Reset settings to not keep references to large objects in memory |
417 | 479 |
# between requests |
... | ... | |
423 | 485 |
|
424 | 486 |
# Was anything written to the response buffer? |
425 | 487 |
if encoded_resp_body.tell(): |
426 |
response_content = json.loads(encoded_resp_body.getvalue()) |
|
488 |
response_content = simplejson.loads(encoded_resp_body.getvalue())
|
|
427 | 489 |
else: |
428 | 490 |
response_content = None |
429 | 491 |
|
... | ... | |
489 | 551 |
def RedistributeConfig(self): |
490 | 552 |
"""Tells the cluster to redistribute its configuration files. |
491 | 553 |
|
554 |
@rtype: string |
|
492 | 555 |
@return: job id |
493 | 556 |
|
494 | 557 |
""" |
... | ... | |
501 | 564 |
|
502 | 565 |
More details for parameters can be found in the RAPI documentation. |
503 | 566 |
|
504 |
@rtype: int
|
|
567 |
@rtype: string
|
|
505 | 568 |
@return: job id |
506 | 569 |
|
507 | 570 |
""" |
... | ... | |
528 | 591 |
@type dry_run: bool |
529 | 592 |
@param dry_run: whether to perform a dry run |
530 | 593 |
|
531 |
@rtype: int
|
|
594 |
@rtype: string
|
|
532 | 595 |
@return: job id |
533 | 596 |
|
534 | 597 |
""" |
535 | 598 |
query = [("tag", t) for t in tags] |
536 |
if dry_run: |
|
537 |
query.append(("dry-run", 1)) |
|
599 |
_AppendDryRunIf(query, dry_run) |
|
538 | 600 |
|
539 | 601 |
return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION, |
540 | 602 |
query, None) |
... | ... | |
546 | 608 |
@param tags: tags to delete |
547 | 609 |
@type dry_run: bool |
548 | 610 |
@param dry_run: whether to perform a dry run |
611 |
@rtype: string |
|
612 |
@return: job id |
|
549 | 613 |
|
550 | 614 |
""" |
551 | 615 |
query = [("tag", t) for t in tags] |
552 |
if dry_run: |
|
553 |
query.append(("dry-run", 1)) |
|
616 |
_AppendDryRunIf(query, dry_run) |
|
554 | 617 |
|
555 | 618 |
return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION, |
556 | 619 |
query, None) |
... | ... | |
566 | 629 |
|
567 | 630 |
""" |
568 | 631 |
query = [] |
569 |
if bulk: |
|
570 |
query.append(("bulk", 1)) |
|
632 |
_AppendIf(query, bulk, ("bulk", 1)) |
|
571 | 633 |
|
572 | 634 |
instances = self._SendRequest(HTTP_GET, |
573 | 635 |
"/%s/instances" % GANETI_RAPI_VERSION, |
... | ... | |
629 | 691 |
@type dry_run: bool |
630 | 692 |
@keyword dry_run: whether to perform a dry run |
631 | 693 |
|
632 |
@rtype: int
|
|
694 |
@rtype: string
|
|
633 | 695 |
@return: job id |
634 | 696 |
|
635 | 697 |
""" |
636 | 698 |
query = [] |
637 | 699 |
|
638 |
if kwargs.get("dry_run"): |
|
639 |
query.append(("dry-run", 1)) |
|
700 |
_AppendDryRunIf(query, kwargs.get("dry_run")) |
|
640 | 701 |
|
641 | 702 |
if _INST_CREATE_REQV1 in self.GetFeatures(): |
642 | 703 |
# All required fields for request data version 1 |
... | ... | |
657 | 718 |
body.update((key, value) for key, value in kwargs.iteritems() |
658 | 719 |
if key != "dry_run") |
659 | 720 |
else: |
660 |
# Old request format (version 0) |
|
661 |
|
|
662 |
# The following code must make sure that an exception is raised when an |
|
663 |
# unsupported setting is requested by the caller. Otherwise this can lead |
|
664 |
# to bugs difficult to find. The interface of this function must stay |
|
665 |
# exactly the same for version 0 and 1 (e.g. they aren't allowed to |
|
666 |
# require different data types). |
|
667 |
|
|
668 |
# Validate disks |
|
669 |
for idx, disk in enumerate(disks): |
|
670 |
unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS |
|
671 |
if unsupported: |
|
672 |
raise GanetiApiError("Server supports request version 0 only, but" |
|
673 |
" disk %s specifies the unsupported parameters" |
|
674 |
" %s, allowed are %s" % |
|
675 |
(idx, unsupported, |
|
676 |
list(_INST_CREATE_V0_DISK_PARAMS))) |
|
677 |
|
|
678 |
assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and |
|
679 |
"size" in _INST_CREATE_V0_DISK_PARAMS) |
|
680 |
disk_sizes = [disk["size"] for disk in disks] |
|
681 |
|
|
682 |
# Validate NICs |
|
683 |
if not nics: |
|
684 |
raise GanetiApiError("Server supports request version 0 only, but" |
|
685 |
" no NIC specified") |
|
686 |
elif len(nics) > 1: |
|
687 |
raise GanetiApiError("Server supports request version 0 only, but" |
|
688 |
" more than one NIC specified") |
|
689 |
|
|
690 |
assert len(nics) == 1 |
|
691 |
|
|
692 |
unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS |
|
693 |
if unsupported: |
|
694 |
raise GanetiApiError("Server supports request version 0 only, but" |
|
695 |
" NIC 0 specifies the unsupported parameters %s," |
|
696 |
" allowed are %s" % |
|
697 |
(unsupported, list(_INST_NIC_PARAMS))) |
|
698 |
|
|
699 |
# Validate other parameters |
|
700 |
unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS - |
|
701 |
_INST_CREATE_V0_DPARAMS) |
|
702 |
if unsupported: |
|
703 |
allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS) |
|
704 |
raise GanetiApiError("Server supports request version 0 only, but" |
|
705 |
" the following unsupported parameters are" |
|
706 |
" specified: %s, allowed are %s" % |
|
707 |
(unsupported, list(allowed))) |
|
708 |
|
|
709 |
# All required fields for request data version 0 |
|
710 |
body = { |
|
711 |
_REQ_DATA_VERSION_FIELD: 0, |
|
712 |
"name": name, |
|
713 |
"disk_template": disk_template, |
|
714 |
"disks": disk_sizes, |
|
715 |
} |
|
716 |
|
|
717 |
# NIC fields |
|
718 |
assert len(nics) == 1 |
|
719 |
assert not (set(body.keys()) & set(nics[0].keys())) |
|
720 |
body.update(nics[0]) |
|
721 |
|
|
722 |
# Copy supported fields |
|
723 |
assert not (set(body.keys()) & set(kwargs.keys())) |
|
724 |
body.update(dict((key, value) for key, value in kwargs.items() |
|
725 |
if key in _INST_CREATE_V0_PARAMS)) |
|
726 |
|
|
727 |
# Merge dictionaries |
|
728 |
for i in (value for key, value in kwargs.items() |
|
729 |
if key in _INST_CREATE_V0_DPARAMS): |
|
730 |
assert not (set(body.keys()) & set(i.keys())) |
|
731 |
body.update(i) |
|
732 |
|
|
733 |
assert not (set(kwargs.keys()) - |
|
734 |
(_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS)) |
|
735 |
assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS) |
|
721 |
raise GanetiApiError("Server does not support new-style (version 1)" |
|
722 |
" instance creation requests") |
|
736 | 723 |
|
737 | 724 |
return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION, |
738 | 725 |
query, body) |
... | ... | |
743 | 730 |
@type instance: str |
744 | 731 |
@param instance: the instance to delete |
745 | 732 |
|
746 |
@rtype: int
|
|
733 |
@rtype: string
|
|
747 | 734 |
@return: job id |
748 | 735 |
|
749 | 736 |
""" |
750 | 737 |
query = [] |
751 |
if dry_run: |
|
752 |
query.append(("dry-run", 1)) |
|
738 |
_AppendDryRunIf(query, dry_run) |
|
753 | 739 |
|
754 | 740 |
return self._SendRequest(HTTP_DELETE, |
755 | 741 |
("/%s/instances/%s" % |
... | ... | |
762 | 748 |
|
763 | 749 |
@type instance: string |
764 | 750 |
@param instance: Instance name |
765 |
@rtype: int
|
|
751 |
@rtype: string
|
|
766 | 752 |
@return: job id |
767 | 753 |
|
768 | 754 |
""" |
... | ... | |
779 | 765 |
@param instance: Instance name |
780 | 766 |
@type ignore_size: bool |
781 | 767 |
@param ignore_size: Whether to ignore recorded size |
768 |
@rtype: string |
|
782 | 769 |
@return: job id |
783 | 770 |
|
784 | 771 |
""" |
785 | 772 |
query = [] |
786 |
if ignore_size: |
|
787 |
query.append(("ignore_size", 1)) |
|
773 |
_AppendIf(query, ignore_size, ("ignore_size", 1)) |
|
788 | 774 |
|
789 | 775 |
return self._SendRequest(HTTP_PUT, |
790 | 776 |
("/%s/instances/%s/activate-disks" % |
... | ... | |
795 | 781 |
|
796 | 782 |
@type instance: string |
797 | 783 |
@param instance: Instance name |
784 |
@rtype: string |
|
798 | 785 |
@return: job id |
799 | 786 |
|
800 | 787 |
""" |
... | ... | |
802 | 789 |
("/%s/instances/%s/deactivate-disks" % |
803 | 790 |
(GANETI_RAPI_VERSION, instance)), None, None) |
804 | 791 |
|
792 |
def RecreateInstanceDisks(self, instance, disks=None, nodes=None): |
|
793 |
"""Recreate an instance's disks. |
|
794 |
|
|
795 |
@type instance: string |
|
796 |
@param instance: Instance name |
|
797 |
@type disks: list of int |
|
798 |
@param disks: List of disk indexes |
|
799 |
@type nodes: list of string |
|
800 |
@param nodes: New instance nodes, if relocation is desired |
|
801 |
@rtype: string |
|
802 |
@return: job id |
|
803 |
|
|
804 |
""" |
|
805 |
body = {} |
|
806 |
_SetItemIf(body, disks is not None, "disks", disks) |
|
807 |
_SetItemIf(body, nodes is not None, "nodes", nodes) |
|
808 |
|
|
809 |
return self._SendRequest(HTTP_POST, |
|
810 |
("/%s/instances/%s/recreate-disks" % |
|
811 |
(GANETI_RAPI_VERSION, instance)), None, body) |
|
812 |
|
|
805 | 813 |
def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None): |
806 | 814 |
"""Grows a disk of an instance. |
807 | 815 |
|
... | ... | |
815 | 823 |
@param amount: Grow disk by this amount (MiB) |
816 | 824 |
@type wait_for_sync: bool |
817 | 825 |
@param wait_for_sync: Wait for disk to synchronize |
818 |
@rtype: int
|
|
826 |
@rtype: string
|
|
819 | 827 |
@return: job id |
820 | 828 |
|
821 | 829 |
""" |
... | ... | |
823 | 831 |
"amount": amount, |
824 | 832 |
} |
825 | 833 |
|
826 |
if wait_for_sync is not None: |
|
827 |
body["wait_for_sync"] = wait_for_sync |
|
834 |
_SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync) |
|
828 | 835 |
|
829 | 836 |
return self._SendRequest(HTTP_POST, |
830 | 837 |
("/%s/instances/%s/disk/%s/grow" % |
... | ... | |
855 | 862 |
@type dry_run: bool |
856 | 863 |
@param dry_run: whether to perform a dry run |
857 | 864 |
|
858 |
@rtype: int
|
|
865 |
@rtype: string
|
|
859 | 866 |
@return: job id |
860 | 867 |
|
861 | 868 |
""" |
862 | 869 |
query = [("tag", t) for t in tags] |
863 |
if dry_run: |
|
864 |
query.append(("dry-run", 1)) |
|
870 |
_AppendDryRunIf(query, dry_run) |
|
865 | 871 |
|
866 | 872 |
return self._SendRequest(HTTP_PUT, |
867 | 873 |
("/%s/instances/%s/tags" % |
... | ... | |
876 | 882 |
@param tags: tags to delete |
877 | 883 |
@type dry_run: bool |
878 | 884 |
@param dry_run: whether to perform a dry run |
885 |
@rtype: string |
|
886 |
@return: job id |
|
879 | 887 |
|
880 | 888 |
""" |
881 | 889 |
query = [("tag", t) for t in tags] |
882 |
if dry_run: |
|
883 |
query.append(("dry-run", 1)) |
|
890 |
_AppendDryRunIf(query, dry_run) |
|
884 | 891 |
|
885 | 892 |
return self._SendRequest(HTTP_DELETE, |
886 | 893 |
("/%s/instances/%s/tags" % |
... | ... | |
899 | 906 |
while re-assembling disks (in hard-reboot mode only) |
900 | 907 |
@type dry_run: bool |
901 | 908 |
@param dry_run: whether to perform a dry run |
909 |
@rtype: string |
|
910 |
@return: job id |
|
902 | 911 |
|
903 | 912 |
""" |
904 | 913 |
query = [] |
905 |
if reboot_type: |
|
906 |
query.append(("type", reboot_type)) |
|
907 |
if ignore_secondaries is not None: |
|
908 |
query.append(("ignore_secondaries", ignore_secondaries)) |
|
909 |
if dry_run: |
|
910 |
query.append(("dry-run", 1)) |
|
914 |
_AppendDryRunIf(query, dry_run) |
|
915 |
_AppendIf(query, reboot_type, ("type", reboot_type)) |
|
916 |
_AppendIf(query, ignore_secondaries is not None, |
|
917 |
("ignore_secondaries", ignore_secondaries)) |
|
911 | 918 |
|
912 | 919 |
return self._SendRequest(HTTP_POST, |
913 | 920 |
("/%s/instances/%s/reboot" % |
914 | 921 |
(GANETI_RAPI_VERSION, instance)), query, None) |
915 | 922 |
|
916 |
def ShutdownInstance(self, instance, dry_run=False): |
|
923 |
def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
|
|
917 | 924 |
"""Shuts down an instance. |
918 | 925 |
|
919 | 926 |
@type instance: str |
920 | 927 |
@param instance: the instance to shut down |
921 | 928 |
@type dry_run: bool |
922 | 929 |
@param dry_run: whether to perform a dry run |
930 |
@type no_remember: bool |
|
931 |
@param no_remember: if true, will not record the state change |
|
932 |
@rtype: string |
|
933 |
@return: job id |
|
923 | 934 |
|
924 | 935 |
""" |
925 | 936 |
query = [] |
926 |
if dry_run:
|
|
927 |
query.append(("dry-run", 1))
|
|
937 |
_AppendDryRunIf(query, dry_run)
|
|
938 |
_AppendIf(query, no_remember, ("no-remember", 1))
|
|
928 | 939 |
|
929 | 940 |
return self._SendRequest(HTTP_PUT, |
930 | 941 |
("/%s/instances/%s/shutdown" % |
931 | 942 |
(GANETI_RAPI_VERSION, instance)), query, None) |
932 | 943 |
|
933 |
def StartupInstance(self, instance, dry_run=False): |
|
944 |
def StartupInstance(self, instance, dry_run=False, no_remember=False):
|
|
934 | 945 |
"""Starts up an instance. |
935 | 946 |
|
936 | 947 |
@type instance: str |
937 | 948 |
@param instance: the instance to start up |
938 | 949 |
@type dry_run: bool |
939 | 950 |
@param dry_run: whether to perform a dry run |
951 |
@type no_remember: bool |
|
952 |
@param no_remember: if true, will not record the state change |
|
953 |
@rtype: string |
|
954 |
@return: job id |
|
940 | 955 |
|
941 | 956 |
""" |
942 | 957 |
query = [] |
943 |
if dry_run:
|
|
944 |
query.append(("dry-run", 1))
|
|
958 |
_AppendDryRunIf(query, dry_run)
|
|
959 |
_AppendIf(query, no_remember, ("no-remember", 1))
|
|
945 | 960 |
|
946 | 961 |
return self._SendRequest(HTTP_PUT, |
947 | 962 |
("/%s/instances/%s/startup" % |
... | ... | |
958 | 973 |
current operating system will be installed again |
959 | 974 |
@type no_startup: bool |
960 | 975 |
@param no_startup: Whether to start the instance automatically |
976 |
@rtype: string |
|
977 |
@return: job id |
|
961 | 978 |
|
962 | 979 |
""" |
963 | 980 |
if _INST_REINSTALL_REQV1 in self.GetFeatures(): |
964 | 981 |
body = { |
965 | 982 |
"start": not no_startup, |
966 | 983 |
} |
967 |
if os is not None: |
|
968 |
body["os"] = os |
|
969 |
if osparams is not None: |
|
970 |
body["osparams"] = osparams |
|
984 |
_SetItemIf(body, os is not None, "os", os) |
|
985 |
_SetItemIf(body, osparams is not None, "osparams", osparams) |
|
971 | 986 |
return self._SendRequest(HTTP_POST, |
972 | 987 |
("/%s/instances/%s/reinstall" % |
973 | 988 |
(GANETI_RAPI_VERSION, instance)), None, body) |
... | ... | |
978 | 993 |
" for instance reinstallation") |
979 | 994 |
|
980 | 995 |
query = [] |
981 |
if os: |
|
982 |
query.append(("os", os)) |
|
983 |
if no_startup: |
|
984 |
query.append(("nostartup", 1)) |
|
996 |
_AppendIf(query, os, ("os", os)) |
|
997 |
_AppendIf(query, no_startup, ("nostartup", 1)) |
|
998 |
|
|
985 | 999 |
return self._SendRequest(HTTP_POST, |
986 | 1000 |
("/%s/instances/%s/reinstall" % |
987 | 1001 |
(GANETI_RAPI_VERSION, instance)), query, None) |
988 | 1002 |
|
989 | 1003 |
def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO, |
990 |
remote_node=None, iallocator=None, dry_run=False):
|
|
1004 |
remote_node=None, iallocator=None): |
|
991 | 1005 |
"""Replaces disks on an instance. |
992 | 1006 |
|
993 | 1007 |
@type instance: str |
... | ... | |
1002 | 1016 |
@type iallocator: str or None |
1003 | 1017 |
@param iallocator: instance allocator plugin to use (for use with |
1004 | 1018 |
replace_auto mode) |
1005 |
@type dry_run: bool |
|
1006 |
@param dry_run: whether to perform a dry run |
|
1007 | 1019 |
|
1008 |
@rtype: int
|
|
1020 |
@rtype: string
|
|
1009 | 1021 |
@return: job id |
1010 | 1022 |
|
1011 | 1023 |
""" |
... | ... | |
1013 | 1025 |
("mode", mode), |
1014 | 1026 |
] |
1015 | 1027 |
|
1016 |
if disks: |
|
1017 |
query.append(("disks", ",".join(str(idx) for idx in disks))) |
|
1018 |
|
|
1019 |
if remote_node: |
|
1020 |
query.append(("remote_node", remote_node)) |
|
1028 |
# TODO: Convert to body parameters |
|
1021 | 1029 |
|
1022 |
if iallocator: |
|
1023 |
query.append(("iallocator", iallocator)) |
|
1030 |
if disks is not None: |
|
1031 |
_AppendIf(query, True, |
|
1032 |
("disks", ",".join(str(idx) for idx in disks))) |
|
1024 | 1033 |
|
1025 |
if dry_run:
|
|
1026 |
query.append(("dry-run", 1))
|
|
1034 |
_AppendIf(query, remote_node is not None, ("remote_node", remote_node))
|
|
1035 |
_AppendIf(query, iallocator is not None, ("iallocator", iallocator))
|
|
1027 | 1036 |
|
1028 | 1037 |
return self._SendRequest(HTTP_POST, |
1029 | 1038 |
("/%s/instances/%s/replace-disks" % |
... | ... | |
1063 | 1072 |
"mode": mode, |
1064 | 1073 |
} |
1065 | 1074 |
|
1066 |
if shutdown is not None: |
|
1067 |
body["shutdown"] = shutdown |
|
1068 |
|
|
1069 |
if remove_instance is not None: |
|
1070 |
body["remove_instance"] = remove_instance |
|
1071 |
|
|
1072 |
if x509_key_name is not None: |
|
1073 |
body["x509_key_name"] = x509_key_name |
|
1074 |
|
|
1075 |
if destination_x509_ca is not None: |
|
1076 |
body["destination_x509_ca"] = destination_x509_ca |
|
1075 |
_SetItemIf(body, shutdown is not None, "shutdown", shutdown) |
|
1076 |
_SetItemIf(body, remove_instance is not None, |
|
1077 |
"remove_instance", remove_instance) |
|
1078 |
_SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name) |
|
1079 |
_SetItemIf(body, destination_x509_ca is not None, |
|
1080 |
"destination_x509_ca", destination_x509_ca) |
|
1077 | 1081 |
|
1078 | 1082 |
return self._SendRequest(HTTP_PUT, |
1079 | 1083 |
("/%s/instances/%s/export" % |
... | ... | |
1088 | 1092 |
@param mode: Migration mode |
1089 | 1093 |
@type cleanup: bool |
1090 | 1094 |
@param cleanup: Whether to clean up a previously failed migration |
1095 |
@rtype: string |
|
1096 |
@return: job id |
|
1091 | 1097 |
|
1092 | 1098 |
""" |
1093 | 1099 |
body = {} |
1100 |
_SetItemIf(body, mode is not None, "mode", mode) |
|
1101 |
_SetItemIf(body, cleanup is not None, "cleanup", cleanup) |
|
1094 | 1102 |
|
1095 |
if mode is not None: |
|
1096 |
body["mode"] = mode |
|
1103 |
return self._SendRequest(HTTP_PUT, |
|
1104 |
("/%s/instances/%s/migrate" % |
|
1105 |
(GANETI_RAPI_VERSION, instance)), None, body) |
|
1097 | 1106 |
|
1098 |
if cleanup is not None: |
|
1099 |
body["cleanup"] = cleanup |
|
1107 |
def FailoverInstance(self, instance, iallocator=None, |
|
1108 |
ignore_consistency=None, target_node=None): |
|
1109 |
"""Does a failover of an instance. |
|
1110 |
|
|
1111 |
@type instance: string |
|
1112 |
@param instance: Instance name |
|
1113 |
@type iallocator: string |
|
1114 |
@param iallocator: Iallocator for deciding the target node for |
|
1115 |
shared-storage instances |
|
1116 |
@type ignore_consistency: bool |
|
1117 |
@param ignore_consistency: Whether to ignore disk consistency |
|
1118 |
@type target_node: string |
|
1119 |
@param target_node: Target node for shared-storage instances |
|
1120 |
@rtype: string |
|
1121 |
@return: job id |
|
1122 |
|
|
1123 |
""" |
|
1124 |
body = {} |
|
1125 |
_SetItemIf(body, iallocator is not None, "iallocator", iallocator) |
|
1126 |
_SetItemIf(body, ignore_consistency is not None, |
|
1127 |
"ignore_consistency", ignore_consistency) |
|
1128 |
_SetItemIf(body, target_node is not None, "target_node", target_node) |
|
1100 | 1129 |
|
1101 | 1130 |
return self._SendRequest(HTTP_PUT, |
1102 |
("/%s/instances/%s/migrate" %
|
|
1131 |
("/%s/instances/%s/failover" %
|
|
1103 | 1132 |
(GANETI_RAPI_VERSION, instance)), None, body) |
1104 | 1133 |
|
1105 | 1134 |
def RenameInstance(self, instance, new_name, ip_check=None, name_check=None): |
... | ... | |
1113 | 1142 |
@param ip_check: Whether to ensure instance's IP address is inactive |
1114 | 1143 |
@type name_check: bool |
1115 | 1144 |
@param name_check: Whether to ensure instance's name is resolvable |
1145 |
@rtype: string |
|
1146 |
@return: job id |
|
1116 | 1147 |
|
1117 | 1148 |
""" |
1118 | 1149 |
body = { |
1119 | 1150 |
"new_name": new_name, |
1120 | 1151 |
} |
1121 | 1152 |
|
1122 |
if ip_check is not None: |
|
1123 |
body["ip_check"] = ip_check |
|
1124 |
|
|
1125 |
if name_check is not None: |
|
1126 |
body["name_check"] = name_check |
|
1153 |
_SetItemIf(body, ip_check is not None, "ip_check", ip_check) |
|
1154 |
_SetItemIf(body, name_check is not None, "name_check", name_check) |
|
1127 | 1155 |
|
1128 | 1156 |
return self._SendRequest(HTTP_PUT, |
1129 | 1157 |
("/%s/instances/%s/rename" % |
... | ... | |
1134 | 1162 |
|
1135 | 1163 |
@type instance: string |
1136 | 1164 |
@param instance: Instance name |
1165 |
@rtype: dict |
|
1166 |
@return: dictionary containing information about instance's console |
|
1137 | 1167 |
|
1138 | 1168 |
""" |
1139 | 1169 |
return self._SendRequest(HTTP_GET, |
... | ... | |
1155 | 1185 |
def GetJobStatus(self, job_id): |
1156 | 1186 |
"""Gets the status of a job. |
1157 | 1187 |
|
1158 |
@type job_id: int
|
|
1188 |
@type job_id: string
|
|
1159 | 1189 |
@param job_id: job id whose status to query |
1160 | 1190 |
|
1161 | 1191 |
@rtype: dict |
... | ... | |
1166 | 1196 |
"/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), |
1167 | 1197 |
None, None) |
1168 | 1198 |
|
1199 |
def WaitForJobCompletion(self, job_id, period=5, retries=-1): |
|
1200 |
"""Polls cluster for job status until completion. |
|
1201 |
|
|
1202 |
Completion is defined as any of the following states listed in |
|
1203 |
L{JOB_STATUS_FINALIZED}. |
|
1204 |
|
|
1205 |
@type job_id: string |
|
1206 |
@param job_id: job id to watch |
|
1207 |
@type period: int |
|
1208 |
@param period: how often to poll for status (optional, default 5s) |
|
1209 |
@type retries: int |
|
1210 |
@param retries: how many time to poll before giving up |
|
1211 |
(optional, default -1 means unlimited) |
|
1212 |
|
|
1213 |
@rtype: bool |
|
1214 |
@return: C{True} if job succeeded or C{False} if failed/status timeout |
|
1215 |
@deprecated: It is recommended to use L{WaitForJobChange} wherever |
|
1216 |
possible; L{WaitForJobChange} returns immediately after a job changed and |
|
1217 |
does not use polling |
|
1218 |
|
|
1219 |
""" |
|
1220 |
while retries != 0: |
|
1221 |
job_result = self.GetJobStatus(job_id) |
|
1222 |
|
|
1223 |
if job_result and job_result["status"] == JOB_STATUS_SUCCESS: |
|
1224 |
return True |
|
1225 |
elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED: |
|
1226 |
return False |
|
1227 |
|
|
1228 |
if period: |
|
1229 |
time.sleep(period) |
|
1230 |
|
|
1231 |
if retries > 0: |
|
1232 |
retries -= 1 |
|
1233 |
|
|
1234 |
return False |
|
1235 |
|
|
1169 | 1236 |
def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial): |
1170 | 1237 |
"""Waits for job changes. |
1171 | 1238 |
|
1172 |
@type job_id: int
|
|
1239 |
@type job_id: string
|
|
1173 | 1240 |
@param job_id: Job ID for which to wait |
1241 |
@return: C{None} if no changes have been detected and a dict with two keys, |
|
1242 |
C{job_info} and C{log_entries} otherwise. |
|
1243 |
@rtype: dict |
|
1174 | 1244 |
|
1175 | 1245 |
""" |
1176 | 1246 |
body = { |
... | ... | |
1186 | 1256 |
def CancelJob(self, job_id, dry_run=False): |
1187 | 1257 |
"""Cancels a job. |
1188 | 1258 |
|
1189 |
@type job_id: int
|
|
1259 |
@type job_id: string
|
|
1190 | 1260 |
@param job_id: id of the job to delete |
1191 | 1261 |
@type dry_run: bool |
1192 | 1262 |
@param dry_run: whether to perform a dry run |
1263 |
@rtype: tuple |
|
1264 |
@return: tuple containing the result, and a message (bool, string) |
|
1193 | 1265 |
|
1194 | 1266 |
""" |
1195 | 1267 |
query = [] |
1196 |
if dry_run: |
|
1197 |
query.append(("dry-run", 1)) |
|
1268 |
_AppendDryRunIf(query, dry_run) |
|
1198 | 1269 |
|
1199 | 1270 |
return self._SendRequest(HTTP_DELETE, |
1200 | 1271 |
"/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), |
... | ... | |
1212 | 1283 |
|
1213 | 1284 |
""" |
1214 | 1285 |
query = [] |
1215 |
if bulk: |
|
1216 |
query.append(("bulk", 1)) |
|
1286 |
_AppendIf(query, bulk, ("bulk", 1)) |
|
1217 | 1287 |
|
1218 | 1288 |
nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION, |
1219 | 1289 |
query, None) |
... | ... | |
1237 | 1307 |
None, None) |
1238 | 1308 |
|
1239 | 1309 |
def EvacuateNode(self, node, iallocator=None, remote_node=None, |
1240 |
dry_run=False, early_release=False): |
|
1310 |
dry_run=False, early_release=None, |
|
1311 |
mode=None, accept_old=False): |
|
1241 | 1312 |
"""Evacuates instances from a Ganeti node. |
1242 | 1313 |
|
1243 | 1314 |
@type node: str |
... | ... | |
1250 | 1321 |
@param dry_run: whether to perform a dry run |
1251 | 1322 |
@type early_release: bool |
1252 | 1323 |
@param early_release: whether to enable parallelization |
1324 |
@type mode: string |
|
1325 |
@param mode: Node evacuation mode |
|
1326 |
@type accept_old: bool |
|
1327 |
@param accept_old: Whether caller is ready to accept old-style (pre-2.5) |
|
1328 |
results |
|
1253 | 1329 |
|
1254 |
@rtype: list |
|
1255 |
@return: list of (job ID, instance name, new secondary node); if |
|
1256 |
dry_run was specified, then the actual move jobs were not |
|
1257 |
submitted and the job IDs will be C{None} |
|
1330 |
@rtype: string, or a list for pre-2.5 results |
|
1331 |
@return: Job ID or, if C{accept_old} is set and server is pre-2.5, |
|
1332 |
list of (job ID, instance name, new secondary node); if dry_run was |
|
1333 |
specified, then the actual move jobs were not submitted and the job IDs |
|
1334 |
will be C{None} |
|
1258 | 1335 |
|
1259 | 1336 |
@raises GanetiApiError: if an iallocator and remote_node are both |
1260 | 1337 |
specified |
... | ... | |
1264 | 1341 |
raise GanetiApiError("Only one of iallocator or remote_node can be used") |
1265 | 1342 |
|
1266 | 1343 |
query = [] |
1267 |
if iallocator: |
|
1268 |
query.append(("iallocator", iallocator)) |
|
1269 |
if remote_node: |
|
1270 |
query.append(("remote_node", remote_node)) |
|
1271 |
if dry_run: |
|
1272 |
query.append(("dry-run", 1)) |
|
1273 |
if early_release: |
|
1274 |
query.append(("early_release", 1)) |
|
1344 |
_AppendDryRunIf(query, dry_run) |
|
1345 |
|
|
1346 |
if _NODE_EVAC_RES1 in self.GetFeatures(): |
|
1347 |
# Server supports body parameters |
|
1348 |
body = {} |
|
1349 |
|
|
1350 |
_SetItemIf(body, iallocator is not None, "iallocator", iallocator) |
|
1351 |
_SetItemIf(body, remote_node is not None, "remote_node", remote_node) |
|
1352 |
_SetItemIf(body, early_release is not None, |
|
1353 |
"early_release", early_release) |
|
1354 |
_SetItemIf(body, mode is not None, "mode", mode) |
|
1355 |
else: |
|
1356 |
# Pre-2.5 request format |
|
1357 |
body = None |
|
1358 |
|
|
1359 |
if not accept_old: |
|
1360 |
raise GanetiApiError("Server is version 2.4 or earlier and caller does" |
|
1361 |
" not accept old-style results (parameter" |
|
1362 |
" accept_old)") |
|
1363 |
|
|
1364 |
# Pre-2.5 servers can only evacuate secondaries |
|
1365 |
if mode is not None and mode != NODE_EVAC_SEC: |
|
1366 |
raise GanetiApiError("Server can only evacuate secondary instances") |
|
1367 |
|
|
1368 |
_AppendIf(query, iallocator, ("iallocator", iallocator)) |
|
1369 |
_AppendIf(query, remote_node, ("remote_node", remote_node)) |
|
1370 |
_AppendIf(query, early_release, ("early_release", 1)) |
|
1275 | 1371 |
|
1276 | 1372 |
return self._SendRequest(HTTP_POST, |
1277 | 1373 |
("/%s/nodes/%s/evacuate" % |
1278 |
(GANETI_RAPI_VERSION, node)), query, None)
|
|
1374 |
(GANETI_RAPI_VERSION, node)), query, body)
|
|
1279 | 1375 |
|
1280 |
def MigrateNode(self, node, mode=None, dry_run=False): |
|
1376 |
def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None, |
|
1377 |
target_node=None): |
|
1281 | 1378 |
"""Migrates all primary instances from a node. |
1282 | 1379 |
|
1283 | 1380 |
@type node: str |
... | ... | |
1287 | 1384 |
otherwise the hypervisor default will be used |
1288 | 1385 |
@type dry_run: bool |
1289 | 1386 |
@param dry_run: whether to perform a dry run |
1387 |
@type iallocator: string |
|
1388 |
@param iallocator: instance allocator to use |
|
1389 |
@type target_node: string |
|
1390 |
@param target_node: Target node for shared-storage instances |
|
1290 | 1391 |
|
1291 |
@rtype: int
|
|
1392 |
@rtype: string
|
|
1292 | 1393 |
@return: job id |
1293 | 1394 |
|
1294 | 1395 |
""" |
1295 | 1396 |
query = [] |
1296 |
if mode is not None: |
|
1297 |
query.append(("mode", mode)) |
|
1298 |
if dry_run: |
|
1299 |
query.append(("dry-run", 1)) |
|
1397 |
_AppendDryRunIf(query, dry_run) |
|
1300 | 1398 |
|
1301 |
return self._SendRequest(HTTP_POST, |
|
1302 |
("/%s/nodes/%s/migrate" % |
|
1303 |
(GANETI_RAPI_VERSION, node)), query, None) |
|
1399 |
if _NODE_MIGRATE_REQV1 in self.GetFeatures(): |
|
1400 |
body = {} |
|
1401 |
|
|
1402 |
_SetItemIf(body, mode is not None, "mode", mode) |
|
1403 |
_SetItemIf(body, iallocator is not None, "iallocator", iallocator) |
|
1404 |
_SetItemIf(body, target_node is not None, "target_node", target_node) |
|
1405 |
|
|
1406 |
assert len(query) <= 1 |
|
1407 |
|
|
1408 |
return self._SendRequest(HTTP_POST, |
|
1409 |
("/%s/nodes/%s/migrate" % |
|
1410 |
(GANETI_RAPI_VERSION, node)), query, body) |
|
1411 |
else: |
|
1412 |
# Use old request format |
|
1413 |
if target_node is not None: |
|
1414 |
raise GanetiApiError("Server does not support specifying target node" |
|
1415 |
" for node migration") |
|
1416 |
|
|
1417 |
_AppendIf(query, mode is not None, ("mode", mode)) |
|
1418 |
|
|
1419 |
return self._SendRequest(HTTP_POST, |
|
1420 |
("/%s/nodes/%s/migrate" % |
|
1421 |
(GANETI_RAPI_VERSION, node)), query, None) |
|
1304 | 1422 |
|
1305 | 1423 |
def GetNodeRole(self, node): |
1306 | 1424 |
"""Gets the current role for a node. |
... | ... | |
1316 | 1434 |
("/%s/nodes/%s/role" % |
1317 | 1435 |
(GANETI_RAPI_VERSION, node)), None, None) |
1318 | 1436 |
|
1319 |
def SetNodeRole(self, node, role, force=False): |
|
1437 |
def SetNodeRole(self, node, role, force=False, auto_promote=None):
|
|
1320 | 1438 |
"""Sets the role for a node. |
1321 | 1439 |
|
1322 | 1440 |
@type node: str |
... | ... | |
1325 | 1443 |
@param role: the role to set for the node |
1326 | 1444 |
@type force: bool |
1327 | 1445 |
@param force: whether to force the role change |
1446 |
@type auto_promote: bool |
|
1447 |
@param auto_promote: Whether node(s) should be promoted to master candidate |
|
1448 |
if necessary |
|
1328 | 1449 |
|
1329 |
@rtype: int
|
|
1450 |
@rtype: string
|
|
1330 | 1451 |
@return: job id |
1331 | 1452 |
|
1332 | 1453 |
""" |
1333 |
query = [ |
|
1334 |
("force", force),
|
|
1335 |
]
|
|
1454 |
query = []
|
|
1455 |
_AppendForceIf(query, force)
|
|
1456 |
_AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
|
|
1336 | 1457 |
|
1337 | 1458 |
return self._SendRequest(HTTP_PUT, |
1338 | 1459 |
("/%s/nodes/%s/role" % |
1339 | 1460 |
(GANETI_RAPI_VERSION, node)), query, role) |
1340 | 1461 |
|
1462 |
def PowercycleNode(self, node, force=False): |
|
1463 |
"""Powercycles a node. |
|
1464 |
|
|
1465 |
@type node: string |
|
1466 |
@param node: Node name |
|
1467 |
@type force: bool |
|
1468 |
@param force: Whether to force the operation |
|
1469 |
@rtype: string |
|
1470 |
@return: job id |
|
1471 |
|
|
1472 |
""" |
|
1473 |
query = [] |
|
1474 |
_AppendForceIf(query, force) |
|
1475 |
|
|
1476 |
return self._SendRequest(HTTP_POST, |
|
1477 |
("/%s/nodes/%s/powercycle" % |
|
1478 |
(GANETI_RAPI_VERSION, node)), query, None) |
|
1479 |
|
|
1480 |
def ModifyNode(self, node, **kwargs): |
|
1481 |
"""Modifies a node. |
|
1482 |
|
|
1483 |
More details for parameters can be found in the RAPI documentation. |
|
1484 |
|
|
1485 |
@type node: string |
|
1486 |
@param node: Node name |
|
1487 |
@rtype: string |
|
1488 |
@return: job id |
|
1489 |
|
|
1490 |
""" |
|
1491 |
return self._SendRequest(HTTP_POST, |
|
1492 |
("/%s/nodes/%s/modify" % |
|
1493 |
(GANETI_RAPI_VERSION, node)), None, kwargs) |
|
1494 |
|
|
1341 | 1495 |
def GetNodeStorageUnits(self, node, storage_type, output_fields): |
1342 | 1496 |
"""Gets the storage units for a node. |
1343 | 1497 |
|
... | ... | |
1348 | 1502 |
@type output_fields: str |
1349 | 1503 |
@param output_fields: storage type fields to return |
1350 | 1504 |
|
1351 |
@rtype: int
|
|
1505 |
@rtype: string
|
|
1352 | 1506 |
@return: job id where results can be retrieved |
1353 | 1507 |
|
1354 | 1508 |
""" |
... | ... | |
1374 | 1528 |
@param allocatable: Whether to set the "allocatable" flag on the storage |
1375 | 1529 |
unit (None=no modification, True=set, False=unset) |
1376 | 1530 |
|
1377 |
@rtype: int
|
|
1531 |
@rtype: string
|
|
1378 | 1532 |
@return: job id |
1379 | 1533 |
|
1380 | 1534 |
""" |
... | ... | |
1383 | 1537 |
("name", name), |
1384 | 1538 |
] |
1385 | 1539 |
|
1386 |
if allocatable is not None: |
|
1387 |
query.append(("allocatable", allocatable)) |
|
1540 |
_AppendIf(query, allocatable is not None, ("allocatable", allocatable)) |
|
1388 | 1541 |
|
1389 | 1542 |
return self._SendRequest(HTTP_PUT, |
1390 | 1543 |
("/%s/nodes/%s/storage/modify" % |
... | ... | |
1400 | 1553 |
@type name: str |
1401 | 1554 |
@param name: name of the storage unit to repair |
1402 | 1555 |
|
1403 |
@rtype: int
|
|
1556 |
@rtype: string
|
|
1404 | 1557 |
@return: job id |
1405 | 1558 |
|
1406 | 1559 |
""" |
... | ... | |
1437 | 1590 |
@type dry_run: bool |
1438 | 1591 |
@param dry_run: whether to perform a dry run |
1439 | 1592 |
|
1440 |
@rtype: int
|
|
1593 |
@rtype: string
|
|
1441 | 1594 |
@return: job id |
1442 | 1595 |
|
1443 | 1596 |
""" |
1444 | 1597 |
query = [("tag", t) for t in tags] |
1445 |
if dry_run: |
|
1446 |
query.append(("dry-run", 1)) |
|
1598 |
_AppendDryRunIf(query, dry_run) |
|
1447 | 1599 |
|
1448 | 1600 |
return self._SendRequest(HTTP_PUT, |
1449 | 1601 |
("/%s/nodes/%s/tags" % |
... | ... | |
1459 | 1611 |
@type dry_run: bool |
1460 | 1612 |
@param dry_run: whether to perform a dry run |
1461 | 1613 |
|
1462 |
@rtype: int
|
|
1614 |
@rtype: string
|
|
1463 | 1615 |
@return: job id |
1464 | 1616 |
|
1465 | 1617 |
""" |
1466 | 1618 |
query = [("tag", t) for t in tags] |
1467 |
if dry_run: |
|
1468 |
query.append(("dry-run", 1)) |
|
1619 |
_AppendDryRunIf(query, dry_run) |
|
1469 | 1620 |
|
1470 | 1621 |
return self._SendRequest(HTTP_DELETE, |
1471 | 1622 |
("/%s/nodes/%s/tags" % |
1472 | 1623 |
(GANETI_RAPI_VERSION, node)), query, None) |
1473 | 1624 |
|
1625 |
def GetNetworks(self, bulk=False): |
|
1626 |
"""Gets all networks in the cluster. |
|
1627 |
|
|
1628 |
@type bulk: bool |
|
1629 |
@param bulk: whether to return all information about the networks |
|
1630 |
|
|
1631 |
@rtype: list of dict or str |
|
1632 |
@return: if bulk is true, a list of dictionaries with info about all |
|
1633 |
networks in the cluster, else a list of names of those networks |
|
1634 |
|
|
1635 |
""" |
|
1636 |
query = [] |
|
1637 |
_AppendIf(query, bulk, ("bulk", 1)) |
|
1638 |
|
|
1639 |
networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION, |
|
1640 |
query, None) |
|
1641 |
if bulk: |
|
1642 |
return networks |
|
1643 |
else: |
|
1644 |
return [n["name"] for n in networks] |
|
1645 |
|
|
1646 |
def GetNetwork(self, network): |
|
1647 |
"""Gets information about a network. |
|
1648 |
|
|
1649 |
@type group: str |
|
1650 |
@param group: name of the network whose info to return |
|
1651 |
|
|
1652 |
@rtype: dict |
|
1653 |
@return: info about the network |
|
1654 |
|
|
1655 |
""" |
|
1656 |
return self._SendRequest(HTTP_GET, |
|
1657 |
"/%s/networks/%s" % (GANETI_RAPI_VERSION, network), |
|
1658 |
None, None) |
|
1659 |
|
|
1660 |
def CreateNetwork(self, network_name, network, gateway=None, network6=None, |
|
1661 |
gateway6=None, mac_prefix=None, network_type="private", |
|
1662 |
reserved_ips=None, dry_run=False): |
|
1663 |
"""Creates a new network. |
|
1664 |
|
|
1665 |
@type name: str |
|
1666 |
@param name: the name of network to create |
|
1667 |
@type dry_run: bool |
|
1668 |
@param dry_run: whether to peform a dry run |
|
1669 |
|
|
1670 |
@rtype: string |
|
1671 |
@return: job id |
|
1672 |
|
|
1673 |
""" |
|
1674 |
query = [] |
|
1675 |
_AppendDryRunIf(query, dry_run) |
|
1676 |
|
|
1677 |
body = { |
|
1678 |
"network_name": network_name, |
|
1679 |
"gateway": gateway, |
|
1680 |
"network": network, |
|
1681 |
"gateway6": gateway6, |
|
1682 |
"network6": network6, |
|
1683 |
"mac_prefix": mac_prefix, |
|
1684 |
"network_type": network_type, |
|
1685 |
"reserved_ips": reserved_ips |
|
1686 |
} |
|
1687 |
|
|
1688 |
return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION, |
|
1689 |
query, body) |
|
1690 |
|
|
1691 |
def ConnectNetwork(self, network_name, group_name, mode, link, depends=None): |
|
1692 |
"""Connects a Network to a NodeGroup with the given netparams |
|
1693 |
|
|
1694 |
""" |
|
1695 |
body = { |
|
1696 |
"group_name": group_name, |
|
1697 |
"network_mode": mode, |
|
1698 |
"network_link": link |
|
1699 |
} |
|
1700 |
|
|
1701 |
if depends: |
|
1702 |
body['depends'] = [] |
|
1703 |
for d in depends: |
|
1704 |
body['depends'].append([d, ["success"]]) |
|
1705 |
|
|
1706 |
|
|
1707 |
return self._SendRequest(HTTP_PUT, |
|
1708 |
("/%s/networks/%s/connect" % |
|
1709 |
(GANETI_RAPI_VERSION, network_name)), None, body) |
|
1710 |
|
|
1711 |
def DisconnectNetwork(self, network_name, group_name, depends=None): |
|
1712 |
"""Connects a Network to a NodeGroup with the given netparams |
|
1713 |
|
|
1714 |
""" |
|
1715 |
body = { |
|
1716 |
"group_name": group_name |
|
1717 |
} |
|
1718 |
|
|
1719 |
if depends: |
|
1720 |
body['depends'] = [] |
|
1721 |
for d in depends: |
|
1722 |
body['depends'].append([d, ["success"]]) |
|
1723 |
|
|
1724 |
return self._SendRequest(HTTP_PUT, |
|
1725 |
("/%s/networks/%s/disconnect" % |
|
1726 |
(GANETI_RAPI_VERSION, network_name)), None, body) |
|
1727 |
|
|
1728 |
def ConnectNetworkAll(self, network_name, mode, link, depends=None): |
|
1729 |
"""Connects a Network to a NodeGroup with the given netparams |
|
1730 |
|
|
1731 |
""" |
|
1732 |
body = { |
|
1733 |
"network_mode": mode, |
|
1734 |
"network_link": link |
|
1735 |
} |
|
1736 |
|
|
1737 |
if depends: |
|
1738 |
body['depends'] = [] |
|
1739 |
for d in depends: |
|
1740 |
body['depends'].append([d, ["success"]]) |
|
1741 |
|
|
1742 |
return self._SendRequest(HTTP_PUT, |
|
1743 |
("/%s/networks/%s/connectall" % |
|
1744 |
(GANETI_RAPI_VERSION, network_name)), None, body) |
|
1745 |
|
|
1746 |
def DisconnectNetworkAll(self, network_name, depends=None): |
|
1747 |
"""Connects a Network to a NodeGroup with the given netparams |
|
1748 |
|
|
1749 |
""" |
|
1750 |
body = {} |
|
1751 |
if depends: |
|
1752 |
body['depends'] = [] |
|
1753 |
for d in depends: |
|
1754 |
body['depends'].append([d, ["success"]]) |
|
1755 |
|
|
1756 |
return self._SendRequest(HTTP_PUT, |
|
1757 |
("/%s/networks/%s/disconnectall" % |
|
1758 |
(GANETI_RAPI_VERSION, network_name)), None, body) |
|
1759 |
|
|
1760 |
def DeleteNetwork(self, network, depends=None): |
|
1761 |
"""Deletes a network. |
|
1762 |
|
|
1763 |
@type group: str |
|
1764 |
@param group: the network to delete |
|
1765 |
@type dry_run: bool |
|
1766 |
@param dry_run: whether to peform a dry run |
|
1767 |
|
|
1768 |
@rtype: string |
|
1769 |
@return: job id |
|
1770 |
|
|
1771 |
""" |
|
1772 |
body = {} |
|
1773 |
if depends: |
|
1774 |
body['depends'] = [] |
|
1775 |
for d in depends: |
|
1776 |
body['depends'].append([d, ["success"]]) |
|
1777 |
|
|
1778 |
|
|
1779 |
return self._SendRequest(HTTP_DELETE, |
|
1780 |
("/%s/networks/%s" % |
|
1781 |
(GANETI_RAPI_VERSION, network)), None, body) |
|
1782 |
|
|
1474 | 1783 |
def GetGroups(self, bulk=False): |
1475 | 1784 |
"""Gets all node groups in the cluster. |
1476 | 1785 |
|
... | ... | |
1483 | 1792 |
|
1484 | 1793 |
""" |
1485 | 1794 |
query = [] |
1486 |
if bulk: |
|
1487 |
query.append(("bulk", 1)) |
|
1795 |
_AppendIf(query, bulk, ("bulk", 1)) |
|
1488 | 1796 |
|
1489 | 1797 |
groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION, |
1490 | 1798 |
query, None) |
... | ... | |
1517 | 1825 |
@type dry_run: bool |
1518 | 1826 |
@param dry_run: whether to peform a dry run |
1519 | 1827 |
|
1520 |
@rtype: int
|
|
1828 |
@rtype: string
|
|
1521 | 1829 |
@return: job id |
1522 | 1830 |
|
1523 | 1831 |
""" |
1524 | 1832 |
query = [] |
1525 |
if dry_run: |
|
1526 |
query.append(("dry-run", 1)) |
|
1833 |
_AppendDryRunIf(query, dry_run) |
|
1527 | 1834 |
|
1528 | 1835 |
body = { |
1529 | 1836 |
"name": name, |
... | ... | |
1540 | 1847 |
|
1541 | 1848 |
@type group: string |
1542 | 1849 |
@param group: Node group name |
1543 |
@rtype: int
|
|
1850 |
@rtype: string
|
|
1544 | 1851 |
@return: job id |
1545 | 1852 |
|
1546 | 1853 |
""" |
... | ... | |
1556 | 1863 |
@type dry_run: bool |
1557 | 1864 |
@param dry_run: whether to peform a dry run |
1558 | 1865 |
|
1559 |
@rtype: int
|
|
1866 |
@rtype: string
|
|
1560 | 1867 |
@return: job id |
1561 | 1868 |
|
1562 | 1869 |
""" |
1563 | 1870 |
query = [] |
1564 |
if dry_run: |
|
1565 |
query.append(("dry-run", 1)) |
|
1871 |
_AppendDryRunIf(query, dry_run) |
|
1566 | 1872 |
|
1567 | 1873 |
return self._SendRequest(HTTP_DELETE, |
1568 | 1874 |
("/%s/groups/%s" % |
... | ... | |
1576 | 1882 |
@type new_name: string |
1577 | 1883 |
@param new_name: New node group name |
1578 | 1884 |
|
1579 |
@rtype: int
|
|
1885 |
@rtype: string
|
|
1580 | 1886 |
@return: job id |
1581 | 1887 |
|
1582 | 1888 |
""" |
... | ... | |
1588 | 1894 |
("/%s/groups/%s/rename" % |
1589 | 1895 |
(GANETI_RAPI_VERSION, group)), None, body) |
1590 | 1896 |
|
1591 |
|
|
1592 | 1897 |
def AssignGroupNodes(self, group, nodes, force=False, dry_run=False): |
1593 | 1898 |
"""Assigns nodes to a group. |
1594 | 1899 |
|
... | ... | |
1597 | 1902 |
@type nodes: list of strings |
1598 | 1903 |
@param nodes: List of nodes to assign to the group |
1599 | 1904 |
|
1600 |
@rtype: int
|
|
1905 |
@rtype: string
|
|
1601 | 1906 |
@return: job id |
1602 | 1907 |
|
1603 | 1908 |
""" |
1604 | 1909 |
query = [] |
1605 |
|
|
1606 |
if force: |
|
1607 |
query.append(("force", 1)) |
|
1608 |
|
|
1609 |
if dry_run: |
|
1610 |
query.append(("dry-run", 1)) |
|
1910 |
_AppendForceIf(query, force) |
|
1911 |
_AppendDryRunIf(query, dry_run) |
|
1611 | 1912 |
|
1612 | 1913 |
body = { |
1613 | 1914 |
"nodes": nodes, |
... | ... | |
1616 | 1917 |
return self._SendRequest(HTTP_PUT, |
1617 | 1918 |
("/%s/groups/%s/assign-nodes" % |
1618 | 1919 |
(GANETI_RAPI_VERSION, group)), query, body) |
1920 |
|
|
1921 |
def GetGroupTags(self, group): |
|
1922 |
"""Gets tags for a node group. |
|
1923 |
|
|
1924 |
@type group: string |
|
1925 |
@param group: Node group whose tags to return |
|
1926 |
|
|
1927 |
@rtype: list of strings |
|
1928 |
@return: tags for the group |
|
1929 |
|
|
1930 |
""" |
|
1931 |
return self._SendRequest(HTTP_GET, |
|
1932 |
("/%s/groups/%s/tags" % |
|
1933 |
(GANETI_RAPI_VERSION, group)), None, None) |
|
1934 |
|
|
1935 |
def AddGroupTags(self, group, tags, dry_run=False): |
|
1936 |
"""Adds tags to a node group. |
|
1937 |
|
|
1938 |
@type group: str |
|
1939 |
@param group: group to add tags to |
|
1940 |
@type tags: list of string |
|
1941 |
@param tags: tags to add to the group |
|
1942 |
@type dry_run: bool |
|
1943 |
@param dry_run: whether to perform a dry run |
|
1944 |
|
|
1945 |
@rtype: string |
|
1946 |
@return: job id |
|
1947 |
|
|
1948 |
""" |
|
1949 |
query = [("tag", t) for t in tags] |
|
1950 |
_AppendDryRunIf(query, dry_run) |
|
1951 |
|
|
1952 |
return self._SendRequest(HTTP_PUT, |
|
1953 |
("/%s/groups/%s/tags" % |
|
1954 |
(GANETI_RAPI_VERSION, group)), query, None) |
|
1955 |
|
|
1956 |
def DeleteGroupTags(self, group, tags, dry_run=False): |
|
1957 |
"""Deletes tags from a node group. |
|
1958 |
|
|
1959 |
@type group: str |
|
1960 |
@param group: group to delete tags from |
|
1961 |
@type tags: list of string |
|
1962 |
@param tags: tags to delete |
|
1963 |
@type dry_run: bool |
|
1964 |
@param dry_run: whether to perform a dry run |
|
1965 |
@rtype: string |
|
1966 |
@return: job id |
|
1967 |
|
|
1968 |
""" |
|
1969 |
query = [("tag", t) for t in tags] |
|
1970 |
_AppendDryRunIf(query, dry_run) |
|
1971 |
|
|
1972 |
return self._SendRequest(HTTP_DELETE, |
|
1973 |
("/%s/groups/%s/tags" % |
|
1974 |
(GANETI_RAPI_VERSION, group)), query, None) |
|
1975 |
|
|
1976 |
def Query(self, what, fields, qfilter=None): |
|
1977 |
"""Retrieves information about resources. |
|
1978 |
|
|
1979 |
@type what: string |
|
1980 |
@param what: Resource name, one of L{constants.QR_VIA_RAPI} |
|
1981 |
@type fields: list of string |
|
1982 |
@param fields: Requested fields |
|
1983 |
@type qfilter: None or list |
|
1984 |
@param qfilter: Query filter |
|
1985 |
|
|
1986 |
@rtype: string |
|
1987 |
@return: job id |
|
1988 |
|
|
1989 |
""" |
|
1990 |
body = { |
|
1991 |
"fields": fields, |
|
1992 |
} |
|
1993 |
|
|
1994 |
_SetItemIf(body, qfilter is not None, "qfilter", qfilter) |
|
1995 |
# TODO: remove "filter" after 2.7 |
|
1996 |
_SetItemIf(body, qfilter is not None, "filter", qfilter) |
|
1997 |
|
|
1998 |
return self._SendRequest(HTTP_PUT, |
|
1999 |
("/%s/query/%s" % |
|
2000 |
(GANETI_RAPI_VERSION, what)), None, body) |
|
2001 |
|
|
2002 |
def QueryFields(self, what, fields=None): |
|
2003 |
"""Retrieves available fields for a resource. |
|
2004 |
|
|
2005 |
@type what: string |
|
2006 |
@param what: Resource name, one of L{constants.QR_VIA_RAPI} |
|
2007 |
@type fields: list of string |
|
2008 |
@param fields: Requested fields |
|
2009 |
|
|
2010 |
@rtype: string |
|
2011 |
@return: job id |
|
2012 |
|
|
2013 |
""" |
|
2014 |
query = [] |
|
2015 |
|
|
2016 |
if fields is not None: |
|
2017 |
_AppendIf(query, True, ("fields", ",".join(fields))) |
|
2018 |
|
|
2019 |
return self._SendRequest(HTTP_GET, |
|
2020 |
("/%s/query/%s/fields" % |
|
2021 |
(GANETI_RAPI_VERSION, what)), query, None) |
Also available in: Unified diff