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 import time
43
44 try:
45 from cStringIO import StringIO
46 except ImportError:
47 from StringIO import StringIO
48
49
50 GANETI_RAPI_PORT = 5080
51 GANETI_RAPI_VERSION = 2
52
53 HTTP_DELETE = "DELETE"
54 HTTP_GET = "GET"
55 HTTP_PUT = "PUT"
56 HTTP_POST = "POST"
57 HTTP_OK = 200
58 HTTP_NOT_FOUND = 404
59 HTTP_APP_JSON = "application/json"
60
61 REPLACE_DISK_PRI = "replace_on_primary"
62 REPLACE_DISK_SECONDARY = "replace_on_secondary"
63 REPLACE_DISK_CHG = "replace_new_secondary"
64 REPLACE_DISK_AUTO = "replace_auto"
65
66 NODE_EVAC_PRI = "primary-only"
67 NODE_EVAC_SEC = "secondary-only"
68 NODE_EVAC_ALL = "all"
69
70 NODE_ROLE_DRAINED = "drained"
71 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
72 NODE_ROLE_MASTER = "master"
73 NODE_ROLE_OFFLINE = "offline"
74 NODE_ROLE_REGULAR = "regular"
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
96 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
97
98
99 _REQ_DATA_VERSION_FIELD = "__version__"
100 _QPARAM_DRY_RUN = "dry-run"
101 _QPARAM_FORCE = "force"
102
103
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
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
114
115
116 try:
117 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
118 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
119 except AttributeError:
120 _CURLE_SSL_CACERT = 60
121 _CURLE_SSL_CACERT_BADFILE = 77
122
123 _CURL_SSL_CERT_ERRORS = frozenset([
124 _CURLE_SSL_CACERT,
125 _CURLE_SSL_CACERT_BADFILE,
126 ])
127
128
129 -class Error(Exception):
130 """Base error class for this module.
131
132 """
133 pass
134
137 """Generic error raised from Ganeti API.
138
139 """
143
146 """Raised when a problem is found with the SSL certificate.
147
148 """
149 pass
150
151
152 -def _AppendIf(container, condition, value):
153 """Appends to a list if a condition evaluates to truth.
154
155 """
156 if condition:
157 container.append(value)
158
159 return condition
160
163 """Appends a "dry-run" parameter if a condition evaluates to truth.
164
165 """
166 return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
167
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
184
187 """Decorator for code using RAPI client to initialize pycURL.
188
189 """
190 def wrapper(*args, **kwargs):
191
192
193
194 assert threading.activeCount() == 1, \
195 "Found active threads when initializing pycURL"
196
197 pycurl.global_init(pycurl.GLOBAL_ALL)
198 try:
199 return fn(*args, **kwargs)
200 finally:
201 pycurl.global_cleanup()
202
203 return wrapper
204
205
206 -def GenericCurlConfig(verbose=False, use_signal=False,
207 use_curl_cabundle=False, cafile=None, capath=None,
208 proxy=None, verify_hostname=False,
209 connect_timeout=None, timeout=None,
210 _pycurl_version_fn=pycurl.version_info):
211 """Curl configuration function generator.
212
213 @type verbose: bool
214 @param verbose: Whether to set cURL to verbose mode
215 @type use_signal: bool
216 @param use_signal: Whether to allow cURL to use signals
217 @type use_curl_cabundle: bool
218 @param use_curl_cabundle: Whether to use cURL's default CA bundle
219 @type cafile: string
220 @param cafile: In which file we can find the certificates
221 @type capath: string
222 @param capath: In which directory we can find the certificates
223 @type proxy: string
224 @param proxy: Proxy to use, None for default behaviour and empty string for
225 disabling proxies (see curl_easy_setopt(3))
226 @type verify_hostname: bool
227 @param verify_hostname: Whether to verify the remote peer certificate's
228 commonName
229 @type connect_timeout: number
230 @param connect_timeout: Timeout for establishing connection in seconds
231 @type timeout: number
232 @param timeout: Timeout for complete transfer in seconds (see
233 curl_easy_setopt(3)).
234
235 """
236 if use_curl_cabundle and (cafile or capath):
237 raise Error("Can not use default CA bundle when CA file or path is set")
238
239 def _ConfigCurl(curl, logger):
240 """Configures a cURL object
241
242 @type curl: pycurl.Curl
243 @param curl: cURL object
244
245 """
246 logger.debug("Using cURL version %s", pycurl.version)
247
248
249
250
251
252 sslver = _pycurl_version_fn()[5]
253 if not sslver:
254 raise Error("No SSL support in cURL")
255
256 lcsslver = sslver.lower()
257 if lcsslver.startswith("openssl/"):
258 pass
259 elif lcsslver.startswith("nss/"):
260
261 pass
262 elif lcsslver.startswith("gnutls/"):
263 if capath:
264 raise Error("cURL linked against GnuTLS has no support for a"
265 " CA path (%s)" % (pycurl.version, ))
266 else:
267 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
268 sslver)
269
270 curl.setopt(pycurl.VERBOSE, verbose)
271 curl.setopt(pycurl.NOSIGNAL, not use_signal)
272
273
274 if verify_hostname:
275
276
277
278
279
280 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
281 else:
282 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
283
284 if cafile or capath or use_curl_cabundle:
285
286 curl.setopt(pycurl.SSL_VERIFYPEER, True)
287 if cafile:
288 curl.setopt(pycurl.CAINFO, str(cafile))
289 if capath:
290 curl.setopt(pycurl.CAPATH, str(capath))
291
292 else:
293
294 curl.setopt(pycurl.SSL_VERIFYPEER, False)
295
296 if proxy is not None:
297 curl.setopt(pycurl.PROXY, str(proxy))
298
299
300 if connect_timeout is not None:
301 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
302 if timeout is not None:
303 curl.setopt(pycurl.TIMEOUT, timeout)
304
305 return _ConfigCurl
306
309 """Ganeti RAPI client.
310
311 """
312 USER_AGENT = "Ganeti RAPI Client"
313 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
314
315 - def __init__(self, host, port=GANETI_RAPI_PORT,
316 username=None, password=None, logger=logging,
317 curl_config_fn=None, curl_factory=None):
318 """Initializes this class.
319
320 @type host: string
321 @param host: the ganeti cluster master to interact with
322 @type port: int
323 @param port: the port on which the RAPI is running (default is 5080)
324 @type username: string
325 @param username: the username to connect with
326 @type password: string
327 @param password: the password to connect with
328 @type curl_config_fn: callable
329 @param curl_config_fn: Function to configure C{pycurl.Curl} object
330 @param logger: Logging object
331
332 """
333 self._username = username
334 self._password = password
335 self._logger = logger
336 self._curl_config_fn = curl_config_fn
337 self._curl_factory = curl_factory
338
339 try:
340 socket.inet_pton(socket.AF_INET6, host)
341 address = "[%s]:%s" % (host, port)
342 except socket.error:
343 address = "%s:%s" % (host, port)
344
345 self._base_url = "https://%s" % address
346
347 if username is not None:
348 if password is None:
349 raise Error("Password not specified")
350 elif password:
351 raise Error("Specified password without username")
352
354 """Creates a cURL object.
355
356 """
357
358 if self._curl_factory:
359 curl = self._curl_factory()
360 else:
361 curl = pycurl.Curl()
362
363
364 curl.setopt(pycurl.VERBOSE, False)
365 curl.setopt(pycurl.FOLLOWLOCATION, False)
366 curl.setopt(pycurl.MAXREDIRS, 5)
367 curl.setopt(pycurl.NOSIGNAL, True)
368 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
369 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
370 curl.setopt(pycurl.SSL_VERIFYPEER, False)
371 curl.setopt(pycurl.HTTPHEADER, [
372 "Accept: %s" % HTTP_APP_JSON,
373 "Content-type: %s" % HTTP_APP_JSON,
374 ])
375
376 assert ((self._username is None and self._password is None) ^
377 (self._username is not None and self._password is not None))
378
379 if self._username:
380
381 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
382 curl.setopt(pycurl.USERPWD,
383 str("%s:%s" % (self._username, self._password)))
384
385
386 if self._curl_config_fn:
387 self._curl_config_fn(curl, self._logger)
388
389 return curl
390
391 @staticmethod
393 """Encode query values for RAPI URL.
394
395 @type query: list of two-tuples
396 @param query: Query arguments
397 @rtype: list
398 @return: Query list with encoded values
399
400 """
401 result = []
402
403 for name, value in query:
404 if value is None:
405 result.append((name, ""))
406
407 elif isinstance(value, bool):
408
409 result.append((name, int(value)))
410
411 elif isinstance(value, (list, tuple, dict)):
412 raise ValueError("Invalid query data type %r" % type(value).__name__)
413
414 else:
415 result.append((name, value))
416
417 return result
418
420 """Sends an HTTP request.
421
422 This constructs a full URL, encodes and decodes HTTP bodies, and
423 handles invalid responses in a pythonic way.
424
425 @type method: string
426 @param method: HTTP method to use
427 @type path: string
428 @param path: HTTP URL path
429 @type query: list of two-tuples
430 @param query: query arguments to pass to urllib.urlencode
431 @type content: str or None
432 @param content: HTTP body content
433
434 @rtype: str
435 @return: JSON-Decoded response
436
437 @raises CertificateError: If an invalid SSL certificate is found
438 @raises GanetiApiError: If an invalid response is returned
439
440 """
441 assert path.startswith("/")
442
443 curl = self._CreateCurl()
444
445 if content is not None:
446 encoded_content = self._json_encoder.encode(content)
447 else:
448 encoded_content = ""
449
450
451 urlparts = [self._base_url, path]
452 if query:
453 urlparts.append("?")
454 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
455
456 url = "".join(urlparts)
457
458 self._logger.debug("Sending request %s %s (content=%r)",
459 method, url, encoded_content)
460
461
462 encoded_resp_body = StringIO()
463
464
465 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
466 curl.setopt(pycurl.URL, str(url))
467 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
468 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
469
470 try:
471
472 try:
473 curl.perform()
474 except pycurl.error, err:
475 if err.args[0] in _CURL_SSL_CERT_ERRORS:
476 raise CertificateError("SSL certificate error %s" % err,
477 code=err.args[0])
478
479 raise GanetiApiError(str(err), code=err.args[0])
480 finally:
481
482
483 curl.setopt(pycurl.POSTFIELDS, "")
484 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
485
486
487 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
488
489
490 if encoded_resp_body.tell():
491 response_content = simplejson.loads(encoded_resp_body.getvalue())
492 else:
493 response_content = None
494
495 if http_code != HTTP_OK:
496 if isinstance(response_content, dict):
497 msg = ("%s %s: %s" %
498 (response_content["code"],
499 response_content["message"],
500 response_content["explain"]))
501 else:
502 msg = str(response_content)
503
504 raise GanetiApiError(msg, code=http_code)
505
506 return response_content
507
509 """Gets the Remote API version running on the cluster.
510
511 @rtype: int
512 @return: Ganeti Remote API version
513
514 """
515 return self._SendRequest(HTTP_GET, "/version", None, None)
516
533
535 """Gets the Operating Systems running in the Ganeti cluster.
536
537 @rtype: list of str
538 @return: operating systems
539
540 """
541 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
542 None, None)
543
545 """Gets info about the cluster.
546
547 @rtype: dict
548 @return: information about the cluster
549
550 """
551 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
552 None, None)
553
555 """Tells the cluster to redistribute its configuration files.
556
557 @rtype: string
558 @return: job id
559
560 """
561 return self._SendRequest(HTTP_PUT,
562 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
563 None, None)
564
566 """Modifies cluster parameters.
567
568 More details for parameters can be found in the RAPI documentation.
569
570 @rtype: string
571 @return: job id
572
573 """
574 body = kwargs
575
576 return self._SendRequest(HTTP_PUT,
577 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
578
588
606
623
625 """Gets information about instances on the cluster.
626
627 @type bulk: bool
628 @param bulk: whether to return all information about all instances
629
630 @rtype: list of dict or list of str
631 @return: if bulk is True, info about the instances, else a list of instances
632
633 """
634 query = []
635 _AppendIf(query, bulk, ("bulk", 1))
636
637 instances = self._SendRequest(HTTP_GET,
638 "/%s/instances" % GANETI_RAPI_VERSION,
639 query, None)
640 if bulk:
641 return instances
642 else:
643 return [i["id"] for i in instances]
644
646 """Gets information about an instance.
647
648 @type instance: str
649 @param instance: instance whose info to return
650
651 @rtype: dict
652 @return: info about the instance
653
654 """
655 return self._SendRequest(HTTP_GET,
656 ("/%s/instances/%s" %
657 (GANETI_RAPI_VERSION, instance)), None, None)
658
660 """Gets information about an instance.
661
662 @type instance: string
663 @param instance: Instance name
664 @rtype: string
665 @return: Job ID
666
667 """
668 if static is not None:
669 query = [("static", static)]
670 else:
671 query = None
672
673 return self._SendRequest(HTTP_GET,
674 ("/%s/instances/%s/info" %
675 (GANETI_RAPI_VERSION, instance)), query, None)
676
677 - def CreateInstance(self, mode, name, disk_template, disks, nics,
678 **kwargs):
679 """Creates a new instance.
680
681 More details for parameters can be found in the RAPI documentation.
682
683 @type mode: string
684 @param mode: Instance creation mode
685 @type name: string
686 @param name: Hostname of the instance to create
687 @type disk_template: string
688 @param disk_template: Disk template for instance (e.g. plain, diskless,
689 file, or drbd)
690 @type disks: list of dicts
691 @param disks: List of disk definitions
692 @type nics: list of dicts
693 @param nics: List of NIC definitions
694 @type dry_run: bool
695 @keyword dry_run: whether to perform a dry run
696
697 @rtype: string
698 @return: job id
699
700 """
701 query = []
702
703 _AppendDryRunIf(query, kwargs.get("dry_run"))
704
705 if _INST_CREATE_REQV1 in self.GetFeatures():
706
707 body = {
708 _REQ_DATA_VERSION_FIELD: 1,
709 "mode": mode,
710 "name": name,
711 "disk_template": disk_template,
712 "disks": disks,
713 "nics": nics,
714 }
715
716 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
717 if conflicts:
718 raise GanetiApiError("Required fields can not be specified as"
719 " keywords: %s" % ", ".join(conflicts))
720
721 body.update((key, value) for key, value in kwargs.iteritems()
722 if key != "dry_run")
723 else:
724 raise GanetiApiError("Server does not support new-style (version 1)"
725 " instance creation requests")
726
727 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
728 query, body)
729
746
748 """Modifies an instance.
749
750 More details for parameters can be found in the RAPI documentation.
751
752 @type instance: string
753 @param instance: Instance name
754 @rtype: string
755 @return: job id
756
757 """
758 body = kwargs
759
760 return self._SendRequest(HTTP_PUT,
761 ("/%s/instances/%s/modify" %
762 (GANETI_RAPI_VERSION, instance)), None, body)
763
765 """Activates an instance's disks.
766
767 @type instance: string
768 @param instance: Instance name
769 @type ignore_size: bool
770 @param ignore_size: Whether to ignore recorded size
771 @rtype: string
772 @return: job id
773
774 """
775 query = []
776 _AppendIf(query, ignore_size, ("ignore_size", 1))
777
778 return self._SendRequest(HTTP_PUT,
779 ("/%s/instances/%s/activate-disks" %
780 (GANETI_RAPI_VERSION, instance)), query, None)
781
783 """Deactivates an instance's disks.
784
785 @type instance: string
786 @param instance: Instance name
787 @rtype: string
788 @return: job id
789
790 """
791 return self._SendRequest(HTTP_PUT,
792 ("/%s/instances/%s/deactivate-disks" %
793 (GANETI_RAPI_VERSION, instance)), None, None)
794
796 """Recreate an instance's disks.
797
798 @type instance: string
799 @param instance: Instance name
800 @type disks: list of int
801 @param disks: List of disk indexes
802 @type nodes: list of string
803 @param nodes: New instance nodes, if relocation is desired
804 @rtype: string
805 @return: job id
806
807 """
808 body = {}
809 _SetItemIf(body, disks is not None, "disks", disks)
810 _SetItemIf(body, nodes is not None, "nodes", nodes)
811
812 return self._SendRequest(HTTP_POST,
813 ("/%s/instances/%s/recreate-disks" %
814 (GANETI_RAPI_VERSION, instance)), None, body)
815
817 """Grows a disk of an instance.
818
819 More details for parameters can be found in the RAPI documentation.
820
821 @type instance: string
822 @param instance: Instance name
823 @type disk: integer
824 @param disk: Disk index
825 @type amount: integer
826 @param amount: Grow disk by this amount (MiB)
827 @type wait_for_sync: bool
828 @param wait_for_sync: Wait for disk to synchronize
829 @rtype: string
830 @return: job id
831
832 """
833 body = {
834 "amount": amount,
835 }
836
837 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
838
839 return self._SendRequest(HTTP_POST,
840 ("/%s/instances/%s/disk/%s/grow" %
841 (GANETI_RAPI_VERSION, instance, disk)),
842 None, body)
843
857
878
898
899 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
900 dry_run=False):
901 """Reboots an instance.
902
903 @type instance: str
904 @param instance: instance to rebot
905 @type reboot_type: str
906 @param reboot_type: one of: hard, soft, full
907 @type ignore_secondaries: bool
908 @param ignore_secondaries: if True, ignores errors for the secondary node
909 while re-assembling disks (in hard-reboot mode only)
910 @type dry_run: bool
911 @param dry_run: whether to perform a dry run
912 @rtype: string
913 @return: job id
914
915 """
916 query = []
917 _AppendDryRunIf(query, dry_run)
918 _AppendIf(query, reboot_type, ("type", reboot_type))
919 _AppendIf(query, ignore_secondaries is not None,
920 ("ignore_secondaries", ignore_secondaries))
921
922 return self._SendRequest(HTTP_POST,
923 ("/%s/instances/%s/reboot" %
924 (GANETI_RAPI_VERSION, instance)), query, None)
925
927 """Shuts down an instance.
928
929 @type instance: str
930 @param instance: the instance to shut down
931 @type dry_run: bool
932 @param dry_run: whether to perform a dry run
933 @type no_remember: bool
934 @param no_remember: if true, will not record the state change
935 @rtype: string
936 @return: job id
937
938 """
939 query = []
940 _AppendDryRunIf(query, dry_run)
941 _AppendIf(query, no_remember, ("no-remember", 1))
942
943 return self._SendRequest(HTTP_PUT,
944 ("/%s/instances/%s/shutdown" %
945 (GANETI_RAPI_VERSION, instance)), query, None)
946
948 """Starts up an instance.
949
950 @type instance: str
951 @param instance: the instance to start up
952 @type dry_run: bool
953 @param dry_run: whether to perform a dry run
954 @type no_remember: bool
955 @param no_remember: if true, will not record the state change
956 @rtype: string
957 @return: job id
958
959 """
960 query = []
961 _AppendDryRunIf(query, dry_run)
962 _AppendIf(query, no_remember, ("no-remember", 1))
963
964 return self._SendRequest(HTTP_PUT,
965 ("/%s/instances/%s/startup" %
966 (GANETI_RAPI_VERSION, instance)), query, None)
967
968 - def ReinstallInstance(self, instance, os=None, no_startup=False,
969 osparams=None):
970 """Reinstalls an instance.
971
972 @type instance: str
973 @param instance: The instance to reinstall
974 @type os: str or None
975 @param os: The operating system to reinstall. If None, the instance's
976 current operating system will be installed again
977 @type no_startup: bool
978 @param no_startup: Whether to start the instance automatically
979 @rtype: string
980 @return: job id
981
982 """
983 if _INST_REINSTALL_REQV1 in self.GetFeatures():
984 body = {
985 "start": not no_startup,
986 }
987 _SetItemIf(body, os is not None, "os", os)
988 _SetItemIf(body, osparams is not None, "osparams", osparams)
989 return self._SendRequest(HTTP_POST,
990 ("/%s/instances/%s/reinstall" %
991 (GANETI_RAPI_VERSION, instance)), None, body)
992
993
994 if osparams:
995 raise GanetiApiError("Server does not support specifying OS parameters"
996 " for instance reinstallation")
997
998 query = []
999 _AppendIf(query, os, ("os", os))
1000 _AppendIf(query, no_startup, ("nostartup", 1))
1001
1002 return self._SendRequest(HTTP_POST,
1003 ("/%s/instances/%s/reinstall" %
1004 (GANETI_RAPI_VERSION, instance)), query, None)
1005
1008 """Replaces disks on an instance.
1009
1010 @type instance: str
1011 @param instance: instance whose disks to replace
1012 @type disks: list of ints
1013 @param disks: Indexes of disks to replace
1014 @type mode: str
1015 @param mode: replacement mode to use (defaults to replace_auto)
1016 @type remote_node: str or None
1017 @param remote_node: new secondary node to use (for use with
1018 replace_new_secondary mode)
1019 @type iallocator: str or None
1020 @param iallocator: instance allocator plugin to use (for use with
1021 replace_auto mode)
1022
1023 @rtype: string
1024 @return: job id
1025
1026 """
1027 query = [
1028 ("mode", mode),
1029 ]
1030
1031
1032
1033 if disks is not None:
1034 _AppendIf(query, True,
1035 ("disks", ",".join(str(idx) for idx in disks)))
1036
1037 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1038 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1039
1040 return self._SendRequest(HTTP_POST,
1041 ("/%s/instances/%s/replace-disks" %
1042 (GANETI_RAPI_VERSION, instance)), query, None)
1043
1045 """Prepares an instance for an export.
1046
1047 @type instance: string
1048 @param instance: Instance name
1049 @type mode: string
1050 @param mode: Export mode
1051 @rtype: string
1052 @return: Job ID
1053
1054 """
1055 query = [("mode", mode)]
1056 return self._SendRequest(HTTP_PUT,
1057 ("/%s/instances/%s/prepare-export" %
1058 (GANETI_RAPI_VERSION, instance)), query, None)
1059
1060 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1061 remove_instance=None,
1062 x509_key_name=None, destination_x509_ca=None):
1063 """Exports an instance.
1064
1065 @type instance: string
1066 @param instance: Instance name
1067 @type mode: string
1068 @param mode: Export mode
1069 @rtype: string
1070 @return: Job ID
1071
1072 """
1073 body = {
1074 "destination": destination,
1075 "mode": mode,
1076 }
1077
1078 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1079 _SetItemIf(body, remove_instance is not None,
1080 "remove_instance", remove_instance)
1081 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1082 _SetItemIf(body, destination_x509_ca is not None,
1083 "destination_x509_ca", destination_x509_ca)
1084
1085 return self._SendRequest(HTTP_PUT,
1086 ("/%s/instances/%s/export" %
1087 (GANETI_RAPI_VERSION, instance)), None, body)
1088
1090 """Migrates an instance.
1091
1092 @type instance: string
1093 @param instance: Instance name
1094 @type mode: string
1095 @param mode: Migration mode
1096 @type cleanup: bool
1097 @param cleanup: Whether to clean up a previously failed migration
1098 @rtype: string
1099 @return: job id
1100
1101 """
1102 body = {}
1103 _SetItemIf(body, mode is not None, "mode", mode)
1104 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1105
1106 return self._SendRequest(HTTP_PUT,
1107 ("/%s/instances/%s/migrate" %
1108 (GANETI_RAPI_VERSION, instance)), None, body)
1109
1110 - def FailoverInstance(self, instance, iallocator=None,
1111 ignore_consistency=None, target_node=None):
1112 """Does a failover of an instance.
1113
1114 @type instance: string
1115 @param instance: Instance name
1116 @type iallocator: string
1117 @param iallocator: Iallocator for deciding the target node for
1118 shared-storage instances
1119 @type ignore_consistency: bool
1120 @param ignore_consistency: Whether to ignore disk consistency
1121 @type target_node: string
1122 @param target_node: Target node for shared-storage instances
1123 @rtype: string
1124 @return: job id
1125
1126 """
1127 body = {}
1128 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1129 _SetItemIf(body, ignore_consistency is not None,
1130 "ignore_consistency", ignore_consistency)
1131 _SetItemIf(body, target_node is not None, "target_node", target_node)
1132
1133 return self._SendRequest(HTTP_PUT,
1134 ("/%s/instances/%s/failover" %
1135 (GANETI_RAPI_VERSION, instance)), None, body)
1136
1137 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1138 """Changes the name of an instance.
1139
1140 @type instance: string
1141 @param instance: Instance name
1142 @type new_name: string
1143 @param new_name: New instance name
1144 @type ip_check: bool
1145 @param ip_check: Whether to ensure instance's IP address is inactive
1146 @type name_check: bool
1147 @param name_check: Whether to ensure instance's name is resolvable
1148 @rtype: string
1149 @return: job id
1150
1151 """
1152 body = {
1153 "new_name": new_name,
1154 }
1155
1156 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1157 _SetItemIf(body, name_check is not None, "name_check", name_check)
1158
1159 return self._SendRequest(HTTP_PUT,
1160 ("/%s/instances/%s/rename" %
1161 (GANETI_RAPI_VERSION, instance)), None, body)
1162
1164 """Request information for connecting to instance's console.
1165
1166 @type instance: string
1167 @param instance: Instance name
1168 @rtype: dict
1169 @return: dictionary containing information about instance's console
1170
1171 """
1172 return self._SendRequest(HTTP_GET,
1173 ("/%s/instances/%s/console" %
1174 (GANETI_RAPI_VERSION, instance)), None, None)
1175
1177 """Gets all jobs for the cluster.
1178
1179 @rtype: list of int
1180 @return: job ids for the cluster
1181
1182 """
1183 return [int(j["id"])
1184 for j in self._SendRequest(HTTP_GET,
1185 "/%s/jobs" % GANETI_RAPI_VERSION,
1186 None, None)]
1187
1189 """Gets the status of a job.
1190
1191 @type job_id: string
1192 @param job_id: job id whose status to query
1193
1194 @rtype: dict
1195 @return: job status
1196
1197 """
1198 return self._SendRequest(HTTP_GET,
1199 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1200 None, None)
1201
1203 """Polls cluster for job status until completion.
1204
1205 Completion is defined as any of the following states listed in
1206 L{JOB_STATUS_FINALIZED}.
1207
1208 @type job_id: string
1209 @param job_id: job id to watch
1210 @type period: int
1211 @param period: how often to poll for status (optional, default 5s)
1212 @type retries: int
1213 @param retries: how many time to poll before giving up
1214 (optional, default -1 means unlimited)
1215
1216 @rtype: bool
1217 @return: C{True} if job succeeded or C{False} if failed/status timeout
1218 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1219 possible; L{WaitForJobChange} returns immediately after a job changed and
1220 does not use polling
1221
1222 """
1223 while retries != 0:
1224 job_result = self.GetJobStatus(job_id)
1225
1226 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1227 return True
1228 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1229 return False
1230
1231 if period:
1232 time.sleep(period)
1233
1234 if retries > 0:
1235 retries -= 1
1236
1237 return False
1238
1240 """Waits for job changes.
1241
1242 @type job_id: string
1243 @param job_id: Job ID for which to wait
1244 @return: C{None} if no changes have been detected and a dict with two keys,
1245 C{job_info} and C{log_entries} otherwise.
1246 @rtype: dict
1247
1248 """
1249 body = {
1250 "fields": fields,
1251 "previous_job_info": prev_job_info,
1252 "previous_log_serial": prev_log_serial,
1253 }
1254
1255 return self._SendRequest(HTTP_GET,
1256 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1257 None, body)
1258
1259 - def CancelJob(self, job_id, dry_run=False):
1260 """Cancels a job.
1261
1262 @type job_id: string
1263 @param job_id: id of the job to delete
1264 @type dry_run: bool
1265 @param dry_run: whether to perform a dry run
1266 @rtype: tuple
1267 @return: tuple containing the result, and a message (bool, string)
1268
1269 """
1270 query = []
1271 _AppendDryRunIf(query, dry_run)
1272
1273 return self._SendRequest(HTTP_DELETE,
1274 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1275 query, None)
1276
1278 """Gets all nodes in the cluster.
1279
1280 @type bulk: bool
1281 @param bulk: whether to return all information about all instances
1282
1283 @rtype: list of dict or str
1284 @return: if bulk is true, info about nodes in the cluster,
1285 else list of nodes in the cluster
1286
1287 """
1288 query = []
1289 _AppendIf(query, bulk, ("bulk", 1))
1290
1291 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1292 query, None)
1293 if bulk:
1294 return nodes
1295 else:
1296 return [n["id"] for n in nodes]
1297
1299 """Gets information about a node.
1300
1301 @type node: str
1302 @param node: node whose info to return
1303
1304 @rtype: dict
1305 @return: info about the node
1306
1307 """
1308 return self._SendRequest(HTTP_GET,
1309 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1310 None, None)
1311
1312 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1313 dry_run=False, early_release=None,
1314 mode=None, accept_old=False):
1315 """Evacuates instances from a Ganeti node.
1316
1317 @type node: str
1318 @param node: node to evacuate
1319 @type iallocator: str or None
1320 @param iallocator: instance allocator to use
1321 @type remote_node: str
1322 @param remote_node: node to evaucate to
1323 @type dry_run: bool
1324 @param dry_run: whether to perform a dry run
1325 @type early_release: bool
1326 @param early_release: whether to enable parallelization
1327 @type mode: string
1328 @param mode: Node evacuation mode
1329 @type accept_old: bool
1330 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1331 results
1332
1333 @rtype: string, or a list for pre-2.5 results
1334 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1335 list of (job ID, instance name, new secondary node); if dry_run was
1336 specified, then the actual move jobs were not submitted and the job IDs
1337 will be C{None}
1338
1339 @raises GanetiApiError: if an iallocator and remote_node are both
1340 specified
1341
1342 """
1343 if iallocator and remote_node:
1344 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1345
1346 query = []
1347 _AppendDryRunIf(query, dry_run)
1348
1349 if _NODE_EVAC_RES1 in self.GetFeatures():
1350
1351 body = {}
1352
1353 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1354 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1355 _SetItemIf(body, early_release is not None,
1356 "early_release", early_release)
1357 _SetItemIf(body, mode is not None, "mode", mode)
1358 else:
1359
1360 body = None
1361
1362 if not accept_old:
1363 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1364 " not accept old-style results (parameter"
1365 " accept_old)")
1366
1367
1368 if mode is not None and mode != NODE_EVAC_SEC:
1369 raise GanetiApiError("Server can only evacuate secondary instances")
1370
1371 _AppendIf(query, iallocator, ("iallocator", iallocator))
1372 _AppendIf(query, remote_node, ("remote_node", remote_node))
1373 _AppendIf(query, early_release, ("early_release", 1))
1374
1375 return self._SendRequest(HTTP_POST,
1376 ("/%s/nodes/%s/evacuate" %
1377 (GANETI_RAPI_VERSION, node)), query, body)
1378
1379 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1380 target_node=None):
1381 """Migrates all primary instances from a node.
1382
1383 @type node: str
1384 @param node: node to migrate
1385 @type mode: string
1386 @param mode: if passed, it will overwrite the live migration type,
1387 otherwise the hypervisor default will be used
1388 @type dry_run: bool
1389 @param dry_run: whether to perform a dry run
1390 @type iallocator: string
1391 @param iallocator: instance allocator to use
1392 @type target_node: string
1393 @param target_node: Target node for shared-storage instances
1394
1395 @rtype: string
1396 @return: job id
1397
1398 """
1399 query = []
1400 _AppendDryRunIf(query, dry_run)
1401
1402 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1403 body = {}
1404
1405 _SetItemIf(body, mode is not None, "mode", mode)
1406 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1407 _SetItemIf(body, target_node is not None, "target_node", target_node)
1408
1409 assert len(query) <= 1
1410
1411 return self._SendRequest(HTTP_POST,
1412 ("/%s/nodes/%s/migrate" %
1413 (GANETI_RAPI_VERSION, node)), query, body)
1414 else:
1415
1416 if target_node is not None:
1417 raise GanetiApiError("Server does not support specifying target node"
1418 " for node migration")
1419
1420 _AppendIf(query, mode is not None, ("mode", mode))
1421
1422 return self._SendRequest(HTTP_POST,
1423 ("/%s/nodes/%s/migrate" %
1424 (GANETI_RAPI_VERSION, node)), query, None)
1425
1427 """Gets the current role for a node.
1428
1429 @type node: str
1430 @param node: node whose role to return
1431
1432 @rtype: str
1433 @return: the current role for a node
1434
1435 """
1436 return self._SendRequest(HTTP_GET,
1437 ("/%s/nodes/%s/role" %
1438 (GANETI_RAPI_VERSION, node)), None, None)
1439
1440 - def SetNodeRole(self, node, role, force=False, auto_promote=None):
1441 """Sets the role for a node.
1442
1443 @type node: str
1444 @param node: the node whose role to set
1445 @type role: str
1446 @param role: the role to set for the node
1447 @type force: bool
1448 @param force: whether to force the role change
1449 @type auto_promote: bool
1450 @param auto_promote: Whether node(s) should be promoted to master candidate
1451 if necessary
1452
1453 @rtype: string
1454 @return: job id
1455
1456 """
1457 query = []
1458 _AppendForceIf(query, force)
1459 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1460
1461 return self._SendRequest(HTTP_PUT,
1462 ("/%s/nodes/%s/role" %
1463 (GANETI_RAPI_VERSION, node)), query, role)
1464
1466 """Powercycles a node.
1467
1468 @type node: string
1469 @param node: Node name
1470 @type force: bool
1471 @param force: Whether to force the operation
1472 @rtype: string
1473 @return: job id
1474
1475 """
1476 query = []
1477 _AppendForceIf(query, force)
1478
1479 return self._SendRequest(HTTP_POST,
1480 ("/%s/nodes/%s/powercycle" %
1481 (GANETI_RAPI_VERSION, node)), query, None)
1482
1484 """Modifies a node.
1485
1486 More details for parameters can be found in the RAPI documentation.
1487
1488 @type node: string
1489 @param node: Node name
1490 @rtype: string
1491 @return: job id
1492
1493 """
1494 return self._SendRequest(HTTP_POST,
1495 ("/%s/nodes/%s/modify" %
1496 (GANETI_RAPI_VERSION, node)), None, kwargs)
1497
1499 """Gets the storage units for a node.
1500
1501 @type node: str
1502 @param node: the node whose storage units to return
1503 @type storage_type: str
1504 @param storage_type: storage type whose units to return
1505 @type output_fields: str
1506 @param output_fields: storage type fields to return
1507
1508 @rtype: string
1509 @return: job id where results can be retrieved
1510
1511 """
1512 query = [
1513 ("storage_type", storage_type),
1514 ("output_fields", output_fields),
1515 ]
1516
1517 return self._SendRequest(HTTP_GET,
1518 ("/%s/nodes/%s/storage" %
1519 (GANETI_RAPI_VERSION, node)), query, None)
1520
1522 """Modifies parameters of storage units on the node.
1523
1524 @type node: str
1525 @param node: node whose storage units to modify
1526 @type storage_type: str
1527 @param storage_type: storage type whose units to modify
1528 @type name: str
1529 @param name: name of the storage unit
1530 @type allocatable: bool or None
1531 @param allocatable: Whether to set the "allocatable" flag on the storage
1532 unit (None=no modification, True=set, False=unset)
1533
1534 @rtype: string
1535 @return: job id
1536
1537 """
1538 query = [
1539 ("storage_type", storage_type),
1540 ("name", name),
1541 ]
1542
1543 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1544
1545 return self._SendRequest(HTTP_PUT,
1546 ("/%s/nodes/%s/storage/modify" %
1547 (GANETI_RAPI_VERSION, node)), query, None)
1548
1550 """Repairs a storage unit on the node.
1551
1552 @type node: str
1553 @param node: node whose storage units to repair
1554 @type storage_type: str
1555 @param storage_type: storage type to repair
1556 @type name: str
1557 @param name: name of the storage unit to repair
1558
1559 @rtype: string
1560 @return: job id
1561
1562 """
1563 query = [
1564 ("storage_type", storage_type),
1565 ("name", name),
1566 ]
1567
1568 return self._SendRequest(HTTP_PUT,
1569 ("/%s/nodes/%s/storage/repair" %
1570 (GANETI_RAPI_VERSION, node)), query, None)
1571
1585
1606
1627
1629 """Gets all node groups in the cluster.
1630
1631 @type bulk: bool
1632 @param bulk: whether to return all information about the groups
1633
1634 @rtype: list of dict or str
1635 @return: if bulk is true, a list of dictionaries with info about all node
1636 groups in the cluster, else a list of names of those node groups
1637
1638 """
1639 query = []
1640 _AppendIf(query, bulk, ("bulk", 1))
1641
1642 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1643 query, None)
1644 if bulk:
1645 return groups
1646 else:
1647 return [g["name"] for g in groups]
1648
1650 """Gets information about a node group.
1651
1652 @type group: str
1653 @param group: name of the node group whose info to return
1654
1655 @rtype: dict
1656 @return: info about the node group
1657
1658 """
1659 return self._SendRequest(HTTP_GET,
1660 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1661 None, None)
1662
1663 - def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1664 """Creates a new node group.
1665
1666 @type name: str
1667 @param name: the name of node group to create
1668 @type alloc_policy: str
1669 @param alloc_policy: the desired allocation policy for the group, if any
1670 @type dry_run: bool
1671 @param dry_run: whether to peform a dry run
1672
1673 @rtype: string
1674 @return: job id
1675
1676 """
1677 query = []
1678 _AppendDryRunIf(query, dry_run)
1679
1680 body = {
1681 "name": name,
1682 "alloc_policy": alloc_policy
1683 }
1684
1685 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1686 query, body)
1687
1689 """Modifies a node group.
1690
1691 More details for parameters can be found in the RAPI documentation.
1692
1693 @type group: string
1694 @param group: Node group name
1695 @rtype: string
1696 @return: job id
1697
1698 """
1699 return self._SendRequest(HTTP_PUT,
1700 ("/%s/groups/%s/modify" %
1701 (GANETI_RAPI_VERSION, group)), None, kwargs)
1702
1704 """Deletes a node group.
1705
1706 @type group: str
1707 @param group: the node group to delete
1708 @type dry_run: bool
1709 @param dry_run: whether to peform a dry run
1710
1711 @rtype: string
1712 @return: job id
1713
1714 """
1715 query = []
1716 _AppendDryRunIf(query, dry_run)
1717
1718 return self._SendRequest(HTTP_DELETE,
1719 ("/%s/groups/%s" %
1720 (GANETI_RAPI_VERSION, group)), query, None)
1721
1723 """Changes the name of a node group.
1724
1725 @type group: string
1726 @param group: Node group name
1727 @type new_name: string
1728 @param new_name: New node group name
1729
1730 @rtype: string
1731 @return: job id
1732
1733 """
1734 body = {
1735 "new_name": new_name,
1736 }
1737
1738 return self._SendRequest(HTTP_PUT,
1739 ("/%s/groups/%s/rename" %
1740 (GANETI_RAPI_VERSION, group)), None, body)
1741
1743 """Assigns nodes to a group.
1744
1745 @type group: string
1746 @param group: Node gropu name
1747 @type nodes: list of strings
1748 @param nodes: List of nodes to assign to the group
1749
1750 @rtype: string
1751 @return: job id
1752
1753 """
1754 query = []
1755 _AppendForceIf(query, force)
1756 _AppendDryRunIf(query, dry_run)
1757
1758 body = {
1759 "nodes": nodes,
1760 }
1761
1762 return self._SendRequest(HTTP_PUT,
1763 ("/%s/groups/%s/assign-nodes" %
1764 (GANETI_RAPI_VERSION, group)), query, body)
1765
1779
1800
1820
1821 - def Query(self, what, fields, qfilter=None):
1822 """Retrieves information about resources.
1823
1824 @type what: string
1825 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1826 @type fields: list of string
1827 @param fields: Requested fields
1828 @type qfilter: None or list
1829 @param qfilter: Query filter
1830
1831 @rtype: string
1832 @return: job id
1833
1834 """
1835 body = {
1836 "fields": fields,
1837 }
1838
1839 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
1840
1841 _SetItemIf(body, qfilter is not None, "filter", qfilter)
1842
1843 return self._SendRequest(HTTP_PUT,
1844 ("/%s/query/%s" %
1845 (GANETI_RAPI_VERSION, what)), None, body)
1846
1848 """Retrieves available fields for a resource.
1849
1850 @type what: string
1851 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1852 @type fields: list of string
1853 @param fields: Requested fields
1854
1855 @rtype: string
1856 @return: job id
1857
1858 """
1859 query = []
1860
1861 if fields is not None:
1862 _AppendIf(query, True, ("fields", ",".join(fields)))
1863
1864 return self._SendRequest(HTTP_GET,
1865 ("/%s/query/%s/fields" %
1866 (GANETI_RAPI_VERSION, what)), query, None)
1867