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