1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Ganeti RAPI client.
23
24 @attention: To use the RAPI client, the application B{must} call
25 C{pycurl.global_init} during initialization and
26 C{pycurl.global_cleanup} before exiting the process. This is very
27 important in multi-threaded programs. See curl_global_init(3) and
28 curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
29 can be used.
30
31 """
32
33
34
35
36 import logging
37 import simplejson
38 import socket
39 import urllib
40 import threading
41 import pycurl
42
43 try:
44 from cStringIO import StringIO
45 except ImportError:
46 from StringIO import StringIO
47
48
49 GANETI_RAPI_PORT = 5080
50 GANETI_RAPI_VERSION = 2
51
52 HTTP_DELETE = "DELETE"
53 HTTP_GET = "GET"
54 HTTP_PUT = "PUT"
55 HTTP_POST = "POST"
56 HTTP_OK = 200
57 HTTP_NOT_FOUND = 404
58 HTTP_APP_JSON = "application/json"
59
60 REPLACE_DISK_PRI = "replace_on_primary"
61 REPLACE_DISK_SECONDARY = "replace_on_secondary"
62 REPLACE_DISK_CHG = "replace_new_secondary"
63 REPLACE_DISK_AUTO = "replace_auto"
64
65 NODE_ROLE_DRAINED = "drained"
66 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
67 NODE_ROLE_MASTER = "master"
68 NODE_ROLE_OFFLINE = "offline"
69 NODE_ROLE_REGULAR = "regular"
70
71
72 _REQ_DATA_VERSION_FIELD = "__version__"
73 _INST_CREATE_REQV1 = "instance-create-reqv1"
74 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
75 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
76 _INST_CREATE_V0_PARAMS = frozenset([
77 "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
78 "hypervisor", "file_storage_dir", "file_driver", "dry_run",
79 ])
80 _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
81
82
83 try:
84 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
85 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
86 except AttributeError:
87 _CURLE_SSL_CACERT = 60
88 _CURLE_SSL_CACERT_BADFILE = 77
89
90 _CURL_SSL_CERT_ERRORS = frozenset([
91 _CURLE_SSL_CACERT,
92 _CURLE_SSL_CACERT_BADFILE,
93 ])
94
95
96 -class Error(Exception):
97 """Base error class for this module.
98
99 """
100 pass
101
104 """Raised when a problem is found with the SSL certificate.
105
106 """
107 pass
108
111 """Generic error raised from Ganeti API.
112
113 """
117
120 """Decorator for code using RAPI client to initialize pycURL.
121
122 """
123 def wrapper(*args, **kwargs):
124
125
126
127 assert threading.activeCount() == 1, \
128 "Found active threads when initializing pycURL"
129
130 pycurl.global_init(pycurl.GLOBAL_ALL)
131 try:
132 return fn(*args, **kwargs)
133 finally:
134 pycurl.global_cleanup()
135
136 return wrapper
137
138
139 -def GenericCurlConfig(verbose=False, use_signal=False,
140 use_curl_cabundle=False, cafile=None, capath=None,
141 proxy=None, verify_hostname=False,
142 connect_timeout=None, timeout=None,
143 _pycurl_version_fn=pycurl.version_info):
144 """Curl configuration function generator.
145
146 @type verbose: bool
147 @param verbose: Whether to set cURL to verbose mode
148 @type use_signal: bool
149 @param use_signal: Whether to allow cURL to use signals
150 @type use_curl_cabundle: bool
151 @param use_curl_cabundle: Whether to use cURL's default CA bundle
152 @type cafile: string
153 @param cafile: In which file we can find the certificates
154 @type capath: string
155 @param capath: In which directory we can find the certificates
156 @type proxy: string
157 @param proxy: Proxy to use, None for default behaviour and empty string for
158 disabling proxies (see curl_easy_setopt(3))
159 @type verify_hostname: bool
160 @param verify_hostname: Whether to verify the remote peer certificate's
161 commonName
162 @type connect_timeout: number
163 @param connect_timeout: Timeout for establishing connection in seconds
164 @type timeout: number
165 @param timeout: Timeout for complete transfer in seconds (see
166 curl_easy_setopt(3)).
167
168 """
169 if use_curl_cabundle and (cafile or capath):
170 raise Error("Can not use default CA bundle when CA file or path is set")
171
172 def _ConfigCurl(curl, logger):
173 """Configures a cURL object
174
175 @type curl: pycurl.Curl
176 @param curl: cURL object
177
178 """
179 logger.debug("Using cURL version %s", pycurl.version)
180
181
182
183
184
185 sslver = _pycurl_version_fn()[5]
186 if not sslver:
187 raise Error("No SSL support in cURL")
188
189 lcsslver = sslver.lower()
190 if lcsslver.startswith("openssl/"):
191 pass
192 elif lcsslver.startswith("gnutls/"):
193 if capath:
194 raise Error("cURL linked against GnuTLS has no support for a"
195 " CA path (%s)" % (pycurl.version, ))
196 else:
197 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
198 sslver)
199
200 curl.setopt(pycurl.VERBOSE, verbose)
201 curl.setopt(pycurl.NOSIGNAL, not use_signal)
202
203
204 if verify_hostname:
205
206
207
208
209
210 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
211 else:
212 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
213
214 if cafile or capath or use_curl_cabundle:
215
216 curl.setopt(pycurl.SSL_VERIFYPEER, True)
217 if cafile:
218 curl.setopt(pycurl.CAINFO, str(cafile))
219 if capath:
220 curl.setopt(pycurl.CAPATH, str(capath))
221
222 else:
223
224 curl.setopt(pycurl.SSL_VERIFYPEER, False)
225
226 if proxy is not None:
227 curl.setopt(pycurl.PROXY, str(proxy))
228
229
230 if connect_timeout is not None:
231 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
232 if timeout is not None:
233 curl.setopt(pycurl.TIMEOUT, timeout)
234
235 return _ConfigCurl
236
239 """Ganeti RAPI client.
240
241 """
242 USER_AGENT = "Ganeti RAPI Client"
243 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
244
245 - def __init__(self, host, port=GANETI_RAPI_PORT,
246 username=None, password=None, logger=logging,
247 curl_config_fn=None, curl_factory=None):
248 """Initializes this class.
249
250 @type host: string
251 @param host: the ganeti cluster master to interact with
252 @type port: int
253 @param port: the port on which the RAPI is running (default is 5080)
254 @type username: string
255 @param username: the username to connect with
256 @type password: string
257 @param password: the password to connect with
258 @type curl_config_fn: callable
259 @param curl_config_fn: Function to configure C{pycurl.Curl} object
260 @param logger: Logging object
261
262 """
263 self._username = username
264 self._password = password
265 self._logger = logger
266 self._curl_config_fn = curl_config_fn
267 self._curl_factory = curl_factory
268
269 try:
270 socket.inet_pton(socket.AF_INET6, host)
271 address = "[%s]:%s" % (host, port)
272 except socket.error:
273 address = "%s:%s" % (host, port)
274
275 self._base_url = "https://%s" % address
276
277 if username is not None:
278 if password is None:
279 raise Error("Password not specified")
280 elif password:
281 raise Error("Specified password without username")
282
284 """Creates a cURL object.
285
286 """
287
288 if self._curl_factory:
289 curl = self._curl_factory()
290 else:
291 curl = pycurl.Curl()
292
293
294 curl.setopt(pycurl.VERBOSE, False)
295 curl.setopt(pycurl.FOLLOWLOCATION, False)
296 curl.setopt(pycurl.MAXREDIRS, 5)
297 curl.setopt(pycurl.NOSIGNAL, True)
298 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
299 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
300 curl.setopt(pycurl.SSL_VERIFYPEER, False)
301 curl.setopt(pycurl.HTTPHEADER, [
302 "Accept: %s" % HTTP_APP_JSON,
303 "Content-type: %s" % HTTP_APP_JSON,
304 ])
305
306 assert ((self._username is None and self._password is None) ^
307 (self._username is not None and self._password is not None))
308
309 if self._username:
310
311 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
312 curl.setopt(pycurl.USERPWD,
313 str("%s:%s" % (self._username, self._password)))
314
315
316 if self._curl_config_fn:
317 self._curl_config_fn(curl, self._logger)
318
319 return curl
320
321 @staticmethod
323 """Encode query values for RAPI URL.
324
325 @type query: list of two-tuples
326 @param query: Query arguments
327 @rtype: list
328 @return: Query list with encoded values
329
330 """
331 result = []
332
333 for name, value in query:
334 if value is None:
335 result.append((name, ""))
336
337 elif isinstance(value, bool):
338
339 result.append((name, int(value)))
340
341 elif isinstance(value, (list, tuple, dict)):
342 raise ValueError("Invalid query data type %r" % type(value).__name__)
343
344 else:
345 result.append((name, value))
346
347 return result
348
350 """Sends an HTTP request.
351
352 This constructs a full URL, encodes and decodes HTTP bodies, and
353 handles invalid responses in a pythonic way.
354
355 @type method: string
356 @param method: HTTP method to use
357 @type path: string
358 @param path: HTTP URL path
359 @type query: list of two-tuples
360 @param query: query arguments to pass to urllib.urlencode
361 @type content: str or None
362 @param content: HTTP body content
363
364 @rtype: str
365 @return: JSON-Decoded response
366
367 @raises CertificateError: If an invalid SSL certificate is found
368 @raises GanetiApiError: If an invalid response is returned
369
370 """
371 assert path.startswith("/")
372
373 curl = self._CreateCurl()
374
375 if content is not None:
376 encoded_content = self._json_encoder.encode(content)
377 else:
378 encoded_content = ""
379
380
381 urlparts = [self._base_url, path]
382 if query:
383 urlparts.append("?")
384 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
385
386 url = "".join(urlparts)
387
388 self._logger.debug("Sending request %s %s (content=%r)",
389 method, url, encoded_content)
390
391
392 encoded_resp_body = StringIO()
393
394
395 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
396 curl.setopt(pycurl.URL, str(url))
397 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
398 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
399
400 try:
401
402 try:
403 curl.perform()
404 except pycurl.error, err:
405 if err.args[0] in _CURL_SSL_CERT_ERRORS:
406 raise CertificateError("SSL certificate error %s" % err)
407
408 raise GanetiApiError(str(err))
409 finally:
410
411
412 curl.setopt(pycurl.POSTFIELDS, "")
413 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
414
415
416 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
417
418
419 if encoded_resp_body.tell():
420 response_content = simplejson.loads(encoded_resp_body.getvalue())
421 else:
422 response_content = None
423
424 if http_code != HTTP_OK:
425 if isinstance(response_content, dict):
426 msg = ("%s %s: %s" %
427 (response_content["code"],
428 response_content["message"],
429 response_content["explain"]))
430 else:
431 msg = str(response_content)
432
433 raise GanetiApiError(msg, code=http_code)
434
435 return response_content
436
438 """Gets the Remote API version running on the cluster.
439
440 @rtype: int
441 @return: Ganeti Remote API version
442
443 """
444 return self._SendRequest(HTTP_GET, "/version", None, None)
445
462
464 """Gets the Operating Systems running in the Ganeti cluster.
465
466 @rtype: list of str
467 @return: operating systems
468
469 """
470 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
471 None, None)
472
474 """Gets info about the cluster.
475
476 @rtype: dict
477 @return: information about the cluster
478
479 """
480 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
481 None, None)
482
492
511
527
529 """Gets information about instances on the cluster.
530
531 @type bulk: bool
532 @param bulk: whether to return all information about all instances
533
534 @rtype: list of dict or list of str
535 @return: if bulk is True, info about the instances, else a list of instances
536
537 """
538 query = []
539 if bulk:
540 query.append(("bulk", 1))
541
542 instances = self._SendRequest(HTTP_GET,
543 "/%s/instances" % GANETI_RAPI_VERSION,
544 query, None)
545 if bulk:
546 return instances
547 else:
548 return [i["id"] for i in instances]
549
551 """Gets information about an instance.
552
553 @type instance: str
554 @param instance: instance whose info to return
555
556 @rtype: dict
557 @return: info about the instance
558
559 """
560 return self._SendRequest(HTTP_GET,
561 ("/%s/instances/%s" %
562 (GANETI_RAPI_VERSION, instance)), None, None)
563
565 """Gets information about an instance.
566
567 @type instance: string
568 @param instance: Instance name
569 @rtype: string
570 @return: Job ID
571
572 """
573 if static is not None:
574 query = [("static", static)]
575 else:
576 query = None
577
578 return self._SendRequest(HTTP_GET,
579 ("/%s/instances/%s/info" %
580 (GANETI_RAPI_VERSION, instance)), query, None)
581
582 - def CreateInstance(self, mode, name, disk_template, disks, nics,
583 **kwargs):
584 """Creates a new instance.
585
586 More details for parameters can be found in the RAPI documentation.
587
588 @type mode: string
589 @param mode: Instance creation mode
590 @type name: string
591 @param name: Hostname of the instance to create
592 @type disk_template: string
593 @param disk_template: Disk template for instance (e.g. plain, diskless,
594 file, or drbd)
595 @type disks: list of dicts
596 @param disks: List of disk definitions
597 @type nics: list of dicts
598 @param nics: List of NIC definitions
599 @type dry_run: bool
600 @keyword dry_run: whether to perform a dry run
601
602 @rtype: int
603 @return: job id
604
605 """
606 query = []
607
608 if kwargs.get("dry_run"):
609 query.append(("dry-run", 1))
610
611 if _INST_CREATE_REQV1 in self.GetFeatures():
612
613 body = {
614 _REQ_DATA_VERSION_FIELD: 1,
615 "mode": mode,
616 "name": name,
617 "disk_template": disk_template,
618 "disks": disks,
619 "nics": nics,
620 }
621
622 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
623 if conflicts:
624 raise GanetiApiError("Required fields can not be specified as"
625 " keywords: %s" % ", ".join(conflicts))
626
627 body.update((key, value) for key, value in kwargs.iteritems()
628 if key != "dry_run")
629 else:
630
631
632
633
634
635
636
637
638
639 for idx, disk in enumerate(disks):
640 unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS
641 if unsupported:
642 raise GanetiApiError("Server supports request version 0 only, but"
643 " disk %s specifies the unsupported parameters"
644 " %s, allowed are %s" %
645 (idx, unsupported,
646 list(_INST_CREATE_V0_DISK_PARAMS)))
647
648 assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and
649 "size" in _INST_CREATE_V0_DISK_PARAMS)
650 disk_sizes = [disk["size"] for disk in disks]
651
652
653 if not nics:
654 raise GanetiApiError("Server supports request version 0 only, but"
655 " no NIC specified")
656 elif len(nics) > 1:
657 raise GanetiApiError("Server supports request version 0 only, but"
658 " more than one NIC specified")
659
660 assert len(nics) == 1
661
662 unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS
663 if unsupported:
664 raise GanetiApiError("Server supports request version 0 only, but"
665 " NIC 0 specifies the unsupported parameters %s,"
666 " allowed are %s" %
667 (unsupported, list(_INST_NIC_PARAMS)))
668
669
670 unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS -
671 _INST_CREATE_V0_DPARAMS)
672 if unsupported:
673 allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS)
674 raise GanetiApiError("Server supports request version 0 only, but"
675 " the following unsupported parameters are"
676 " specified: %s, allowed are %s" %
677 (unsupported, list(allowed)))
678
679
680 body = {
681 _REQ_DATA_VERSION_FIELD: 0,
682 "name": name,
683 "disk_template": disk_template,
684 "disks": disk_sizes,
685 }
686
687
688 assert len(nics) == 1
689 assert not (set(body.keys()) & set(nics[0].keys()))
690 body.update(nics[0])
691
692
693 assert not (set(body.keys()) & set(kwargs.keys()))
694 body.update(dict((key, value) for key, value in kwargs.items()
695 if key in _INST_CREATE_V0_PARAMS))
696
697
698 for i in (value for key, value in kwargs.items()
699 if key in _INST_CREATE_V0_DPARAMS):
700 assert not (set(body.keys()) & set(i.keys()))
701 body.update(i)
702
703 assert not (set(kwargs.keys()) -
704 (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS))
705 assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS)
706
707 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
708 query, body)
709
711 """Deletes an instance.
712
713 @type instance: str
714 @param instance: the instance to delete
715
716 @rtype: int
717 @return: job id
718
719 """
720 query = []
721 if dry_run:
722 query.append(("dry-run", 1))
723
724 return self._SendRequest(HTTP_DELETE,
725 ("/%s/instances/%s" %
726 (GANETI_RAPI_VERSION, instance)), query, None)
727
729 """Modifies an instance.
730
731 More details for parameters can be found in the RAPI documentation.
732
733 @type instance: string
734 @param instance: Instance name
735 @rtype: int
736 @return: job id
737
738 """
739 body = kwargs
740
741 return self._SendRequest(HTTP_PUT,
742 ("/%s/instances/%s/modify" %
743 (GANETI_RAPI_VERSION, instance)), None, body)
744
758
780
799
800 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
801 dry_run=False):
802 """Reboots an instance.
803
804 @type instance: str
805 @param instance: instance to rebot
806 @type reboot_type: str
807 @param reboot_type: one of: hard, soft, full
808 @type ignore_secondaries: bool
809 @param ignore_secondaries: if True, ignores errors for the secondary node
810 while re-assembling disks (in hard-reboot mode only)
811 @type dry_run: bool
812 @param dry_run: whether to perform a dry run
813
814 """
815 query = []
816 if reboot_type:
817 query.append(("type", reboot_type))
818 if ignore_secondaries is not None:
819 query.append(("ignore_secondaries", ignore_secondaries))
820 if dry_run:
821 query.append(("dry-run", 1))
822
823 return self._SendRequest(HTTP_POST,
824 ("/%s/instances/%s/reboot" %
825 (GANETI_RAPI_VERSION, instance)), query, None)
826
828 """Shuts down an instance.
829
830 @type instance: str
831 @param instance: the instance to shut down
832 @type dry_run: bool
833 @param dry_run: whether to perform a dry run
834
835 """
836 query = []
837 if dry_run:
838 query.append(("dry-run", 1))
839
840 return self._SendRequest(HTTP_PUT,
841 ("/%s/instances/%s/shutdown" %
842 (GANETI_RAPI_VERSION, instance)), query, None)
843
845 """Starts up an instance.
846
847 @type instance: str
848 @param instance: the instance to start up
849 @type dry_run: bool
850 @param dry_run: whether to perform a dry run
851
852 """
853 query = []
854 if dry_run:
855 query.append(("dry-run", 1))
856
857 return self._SendRequest(HTTP_PUT,
858 ("/%s/instances/%s/startup" %
859 (GANETI_RAPI_VERSION, instance)), query, None)
860
862 """Reinstalls an instance.
863
864 @type instance: str
865 @param instance: The instance to reinstall
866 @type os: str or None
867 @param os: The operating system to reinstall. If None, the instance's
868 current operating system will be installed again
869 @type no_startup: bool
870 @param no_startup: Whether to start the instance automatically
871
872 """
873 query = []
874 if os:
875 query.append(("os", os))
876 if no_startup:
877 query.append(("nostartup", 1))
878 return self._SendRequest(HTTP_POST,
879 ("/%s/instances/%s/reinstall" %
880 (GANETI_RAPI_VERSION, instance)), query, None)
881
884 """Replaces disks on an instance.
885
886 @type instance: str
887 @param instance: instance whose disks to replace
888 @type disks: list of ints
889 @param disks: Indexes of disks to replace
890 @type mode: str
891 @param mode: replacement mode to use (defaults to replace_auto)
892 @type remote_node: str or None
893 @param remote_node: new secondary node to use (for use with
894 replace_new_secondary mode)
895 @type iallocator: str or None
896 @param iallocator: instance allocator plugin to use (for use with
897 replace_auto mode)
898 @type dry_run: bool
899 @param dry_run: whether to perform a dry run
900
901 @rtype: int
902 @return: job id
903
904 """
905 query = [
906 ("mode", mode),
907 ]
908
909 if disks:
910 query.append(("disks", ",".join(str(idx) for idx in disks)))
911
912 if remote_node:
913 query.append(("remote_node", remote_node))
914
915 if iallocator:
916 query.append(("iallocator", iallocator))
917
918 if dry_run:
919 query.append(("dry-run", 1))
920
921 return self._SendRequest(HTTP_POST,
922 ("/%s/instances/%s/replace-disks" %
923 (GANETI_RAPI_VERSION, instance)), query, None)
924
926 """Prepares an instance for an export.
927
928 @type instance: string
929 @param instance: Instance name
930 @type mode: string
931 @param mode: Export mode
932 @rtype: string
933 @return: Job ID
934
935 """
936 query = [("mode", mode)]
937 return self._SendRequest(HTTP_PUT,
938 ("/%s/instances/%s/prepare-export" %
939 (GANETI_RAPI_VERSION, instance)), query, None)
940
941 - def ExportInstance(self, instance, mode, destination, shutdown=None,
942 remove_instance=None,
943 x509_key_name=None, destination_x509_ca=None):
944 """Exports an instance.
945
946 @type instance: string
947 @param instance: Instance name
948 @type mode: string
949 @param mode: Export mode
950 @rtype: string
951 @return: Job ID
952
953 """
954 body = {
955 "destination": destination,
956 "mode": mode,
957 }
958
959 if shutdown is not None:
960 body["shutdown"] = shutdown
961
962 if remove_instance is not None:
963 body["remove_instance"] = remove_instance
964
965 if x509_key_name is not None:
966 body["x509_key_name"] = x509_key_name
967
968 if destination_x509_ca is not None:
969 body["destination_x509_ca"] = destination_x509_ca
970
971 return self._SendRequest(HTTP_PUT,
972 ("/%s/instances/%s/export" %
973 (GANETI_RAPI_VERSION, instance)), None, body)
974
976 """Migrates an instance.
977
978 @type instance: string
979 @param instance: Instance name
980 @type mode: string
981 @param mode: Migration mode
982 @type cleanup: bool
983 @param cleanup: Whether to clean up a previously failed migration
984
985 """
986 body = {}
987
988 if mode is not None:
989 body["mode"] = mode
990
991 if cleanup is not None:
992 body["cleanup"] = cleanup
993
994 return self._SendRequest(HTTP_PUT,
995 ("/%s/instances/%s/migrate" %
996 (GANETI_RAPI_VERSION, instance)), None, body)
997
998 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
999 """Changes the name of an instance.
1000
1001 @type instance: string
1002 @param instance: Instance name
1003 @type new_name: string
1004 @param new_name: New instance name
1005 @type ip_check: bool
1006 @param ip_check: Whether to ensure instance's IP address is inactive
1007 @type name_check: bool
1008 @param name_check: Whether to ensure instance's name is resolvable
1009
1010 """
1011 body = {
1012 "new_name": new_name,
1013 }
1014
1015 if ip_check is not None:
1016 body["ip_check"] = ip_check
1017
1018 if name_check is not None:
1019 body["name_check"] = name_check
1020
1021 return self._SendRequest(HTTP_PUT,
1022 ("/%s/instances/%s/rename" %
1023 (GANETI_RAPI_VERSION, instance)), None, body)
1024
1026 """Gets all jobs for the cluster.
1027
1028 @rtype: list of int
1029 @return: job ids for the cluster
1030
1031 """
1032 return [int(j["id"])
1033 for j in self._SendRequest(HTTP_GET,
1034 "/%s/jobs" % GANETI_RAPI_VERSION,
1035 None, None)]
1036
1038 """Gets the status of a job.
1039
1040 @type job_id: int
1041 @param job_id: job id whose status to query
1042
1043 @rtype: dict
1044 @return: job status
1045
1046 """
1047 return self._SendRequest(HTTP_GET,
1048 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1049 None, None)
1050
1052 """Waits for job changes.
1053
1054 @type job_id: int
1055 @param job_id: Job ID for which to wait
1056
1057 """
1058 body = {
1059 "fields": fields,
1060 "previous_job_info": prev_job_info,
1061 "previous_log_serial": prev_log_serial,
1062 }
1063
1064 return self._SendRequest(HTTP_GET,
1065 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1066 None, body)
1067
1068 - def CancelJob(self, job_id, dry_run=False):
1069 """Cancels a job.
1070
1071 @type job_id: int
1072 @param job_id: id of the job to delete
1073 @type dry_run: bool
1074 @param dry_run: whether to perform a dry run
1075
1076 """
1077 query = []
1078 if dry_run:
1079 query.append(("dry-run", 1))
1080
1081 return self._SendRequest(HTTP_DELETE,
1082 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1083 query, None)
1084
1086 """Gets all nodes in the cluster.
1087
1088 @type bulk: bool
1089 @param bulk: whether to return all information about all instances
1090
1091 @rtype: list of dict or str
1092 @return: if bulk is true, info about nodes in the cluster,
1093 else list of nodes in the cluster
1094
1095 """
1096 query = []
1097 if bulk:
1098 query.append(("bulk", 1))
1099
1100 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1101 query, None)
1102 if bulk:
1103 return nodes
1104 else:
1105 return [n["id"] for n in nodes]
1106
1108 """Gets information about a node.
1109
1110 @type node: str
1111 @param node: node whose info to return
1112
1113 @rtype: dict
1114 @return: info about the node
1115
1116 """
1117 return self._SendRequest(HTTP_GET,
1118 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1119 None, None)
1120
1121 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1122 dry_run=False, early_release=False):
1123 """Evacuates instances from a Ganeti node.
1124
1125 @type node: str
1126 @param node: node to evacuate
1127 @type iallocator: str or None
1128 @param iallocator: instance allocator to use
1129 @type remote_node: str
1130 @param remote_node: node to evaucate to
1131 @type dry_run: bool
1132 @param dry_run: whether to perform a dry run
1133 @type early_release: bool
1134 @param early_release: whether to enable parallelization
1135
1136 @rtype: list
1137 @return: list of (job ID, instance name, new secondary node); if
1138 dry_run was specified, then the actual move jobs were not
1139 submitted and the job IDs will be C{None}
1140
1141 @raises GanetiApiError: if an iallocator and remote_node are both
1142 specified
1143
1144 """
1145 if iallocator and remote_node:
1146 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1147
1148 query = []
1149 if iallocator:
1150 query.append(("iallocator", iallocator))
1151 if remote_node:
1152 query.append(("remote_node", remote_node))
1153 if dry_run:
1154 query.append(("dry-run", 1))
1155 if early_release:
1156 query.append(("early_release", 1))
1157
1158 return self._SendRequest(HTTP_POST,
1159 ("/%s/nodes/%s/evacuate" %
1160 (GANETI_RAPI_VERSION, node)), query, None)
1161
1162 - def MigrateNode(self, node, mode=None, dry_run=False):
1163 """Migrates all primary instances from a node.
1164
1165 @type node: str
1166 @param node: node to migrate
1167 @type mode: string
1168 @param mode: if passed, it will overwrite the live migration type,
1169 otherwise the hypervisor default will be used
1170 @type dry_run: bool
1171 @param dry_run: whether to perform a dry run
1172
1173 @rtype: int
1174 @return: job id
1175
1176 """
1177 query = []
1178 if mode is not None:
1179 query.append(("mode", mode))
1180 if dry_run:
1181 query.append(("dry-run", 1))
1182
1183 return self._SendRequest(HTTP_POST,
1184 ("/%s/nodes/%s/migrate" %
1185 (GANETI_RAPI_VERSION, node)), query, None)
1186
1188 """Gets the current role for a node.
1189
1190 @type node: str
1191 @param node: node whose role to return
1192
1193 @rtype: str
1194 @return: the current role for a node
1195
1196 """
1197 return self._SendRequest(HTTP_GET,
1198 ("/%s/nodes/%s/role" %
1199 (GANETI_RAPI_VERSION, node)), None, None)
1200
1202 """Sets the role for a node.
1203
1204 @type node: str
1205 @param node: the node whose role to set
1206 @type role: str
1207 @param role: the role to set for the node
1208 @type force: bool
1209 @param force: whether to force the role change
1210
1211 @rtype: int
1212 @return: job id
1213
1214 """
1215 query = [
1216 ("force", force),
1217 ]
1218
1219 return self._SendRequest(HTTP_PUT,
1220 ("/%s/nodes/%s/role" %
1221 (GANETI_RAPI_VERSION, node)), query, role)
1222
1224 """Gets the storage units for a node.
1225
1226 @type node: str
1227 @param node: the node whose storage units to return
1228 @type storage_type: str
1229 @param storage_type: storage type whose units to return
1230 @type output_fields: str
1231 @param output_fields: storage type fields to return
1232
1233 @rtype: int
1234 @return: job id where results can be retrieved
1235
1236 """
1237 query = [
1238 ("storage_type", storage_type),
1239 ("output_fields", output_fields),
1240 ]
1241
1242 return self._SendRequest(HTTP_GET,
1243 ("/%s/nodes/%s/storage" %
1244 (GANETI_RAPI_VERSION, node)), query, None)
1245
1247 """Modifies parameters of storage units on the node.
1248
1249 @type node: str
1250 @param node: node whose storage units to modify
1251 @type storage_type: str
1252 @param storage_type: storage type whose units to modify
1253 @type name: str
1254 @param name: name of the storage unit
1255 @type allocatable: bool or None
1256 @param allocatable: Whether to set the "allocatable" flag on the storage
1257 unit (None=no modification, True=set, False=unset)
1258
1259 @rtype: int
1260 @return: job id
1261
1262 """
1263 query = [
1264 ("storage_type", storage_type),
1265 ("name", name),
1266 ]
1267
1268 if allocatable is not None:
1269 query.append(("allocatable", allocatable))
1270
1271 return self._SendRequest(HTTP_PUT,
1272 ("/%s/nodes/%s/storage/modify" %
1273 (GANETI_RAPI_VERSION, node)), query, None)
1274
1276 """Repairs a storage unit on the node.
1277
1278 @type node: str
1279 @param node: node whose storage units to repair
1280 @type storage_type: str
1281 @param storage_type: storage type to repair
1282 @type name: str
1283 @param name: name of the storage unit to repair
1284
1285 @rtype: int
1286 @return: job id
1287
1288 """
1289 query = [
1290 ("storage_type", storage_type),
1291 ("name", name),
1292 ]
1293
1294 return self._SendRequest(HTTP_PUT,
1295 ("/%s/nodes/%s/storage/repair" %
1296 (GANETI_RAPI_VERSION, node)), query, None)
1297
1311
1333
1355