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_REINSTALL_REQV1 = "instance-reinstall-reqv1"
75 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
76 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
77 _INST_CREATE_V0_PARAMS = frozenset([
78 "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
79 "hypervisor", "file_storage_dir", "file_driver", "dry_run",
80 ])
81 _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
82
83
84 try:
85 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
86 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
87 except AttributeError:
88 _CURLE_SSL_CACERT = 60
89 _CURLE_SSL_CACERT_BADFILE = 77
90
91 _CURL_SSL_CERT_ERRORS = frozenset([
92 _CURLE_SSL_CACERT,
93 _CURLE_SSL_CACERT_BADFILE,
94 ])
95
96
97 -class Error(Exception):
98 """Base error class for this module.
99
100 """
101 pass
102
105 """Raised when a problem is found with the SSL certificate.
106
107 """
108 pass
109
112 """Generic error raised from Ganeti API.
113
114 """
118
121 """Decorator for code using RAPI client to initialize pycURL.
122
123 """
124 def wrapper(*args, **kwargs):
125
126
127
128 assert threading.activeCount() == 1, \
129 "Found active threads when initializing pycURL"
130
131 pycurl.global_init(pycurl.GLOBAL_ALL)
132 try:
133 return fn(*args, **kwargs)
134 finally:
135 pycurl.global_cleanup()
136
137 return wrapper
138
139
140 -def GenericCurlConfig(verbose=False, use_signal=False,
141 use_curl_cabundle=False, cafile=None, capath=None,
142 proxy=None, verify_hostname=False,
143 connect_timeout=None, timeout=None,
144 _pycurl_version_fn=pycurl.version_info):
145 """Curl configuration function generator.
146
147 @type verbose: bool
148 @param verbose: Whether to set cURL to verbose mode
149 @type use_signal: bool
150 @param use_signal: Whether to allow cURL to use signals
151 @type use_curl_cabundle: bool
152 @param use_curl_cabundle: Whether to use cURL's default CA bundle
153 @type cafile: string
154 @param cafile: In which file we can find the certificates
155 @type capath: string
156 @param capath: In which directory we can find the certificates
157 @type proxy: string
158 @param proxy: Proxy to use, None for default behaviour and empty string for
159 disabling proxies (see curl_easy_setopt(3))
160 @type verify_hostname: bool
161 @param verify_hostname: Whether to verify the remote peer certificate's
162 commonName
163 @type connect_timeout: number
164 @param connect_timeout: Timeout for establishing connection in seconds
165 @type timeout: number
166 @param timeout: Timeout for complete transfer in seconds (see
167 curl_easy_setopt(3)).
168
169 """
170 if use_curl_cabundle and (cafile or capath):
171 raise Error("Can not use default CA bundle when CA file or path is set")
172
173 def _ConfigCurl(curl, logger):
174 """Configures a cURL object
175
176 @type curl: pycurl.Curl
177 @param curl: cURL object
178
179 """
180 logger.debug("Using cURL version %s", pycurl.version)
181
182
183
184
185
186 sslver = _pycurl_version_fn()[5]
187 if not sslver:
188 raise Error("No SSL support in cURL")
189
190 lcsslver = sslver.lower()
191 if lcsslver.startswith("openssl/"):
192 pass
193 elif lcsslver.startswith("gnutls/"):
194 if capath:
195 raise Error("cURL linked against GnuTLS has no support for a"
196 " CA path (%s)" % (pycurl.version, ))
197 else:
198 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
199 sslver)
200
201 curl.setopt(pycurl.VERBOSE, verbose)
202 curl.setopt(pycurl.NOSIGNAL, not use_signal)
203
204
205 if verify_hostname:
206
207
208
209
210
211 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
212 else:
213 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
214
215 if cafile or capath or use_curl_cabundle:
216
217 curl.setopt(pycurl.SSL_VERIFYPEER, True)
218 if cafile:
219 curl.setopt(pycurl.CAINFO, str(cafile))
220 if capath:
221 curl.setopt(pycurl.CAPATH, str(capath))
222
223 else:
224
225 curl.setopt(pycurl.SSL_VERIFYPEER, False)
226
227 if proxy is not None:
228 curl.setopt(pycurl.PROXY, str(proxy))
229
230
231 if connect_timeout is not None:
232 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
233 if timeout is not None:
234 curl.setopt(pycurl.TIMEOUT, timeout)
235
236 return _ConfigCurl
237
240 """Ganeti RAPI client.
241
242 """
243 USER_AGENT = "Ganeti RAPI Client"
244 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
245
246 - def __init__(self, host, port=GANETI_RAPI_PORT,
247 username=None, password=None, logger=logging,
248 curl_config_fn=None, curl_factory=None):
249 """Initializes this class.
250
251 @type host: string
252 @param host: the ganeti cluster master to interact with
253 @type port: int
254 @param port: the port on which the RAPI is running (default is 5080)
255 @type username: string
256 @param username: the username to connect with
257 @type password: string
258 @param password: the password to connect with
259 @type curl_config_fn: callable
260 @param curl_config_fn: Function to configure C{pycurl.Curl} object
261 @param logger: Logging object
262
263 """
264 self._username = username
265 self._password = password
266 self._logger = logger
267 self._curl_config_fn = curl_config_fn
268 self._curl_factory = curl_factory
269
270 try:
271 socket.inet_pton(socket.AF_INET6, host)
272 address = "[%s]:%s" % (host, port)
273 except socket.error:
274 address = "%s:%s" % (host, port)
275
276 self._base_url = "https://%s" % address
277
278 if username is not None:
279 if password is None:
280 raise Error("Password not specified")
281 elif password:
282 raise Error("Specified password without username")
283
285 """Creates a cURL object.
286
287 """
288
289 if self._curl_factory:
290 curl = self._curl_factory()
291 else:
292 curl = pycurl.Curl()
293
294
295 curl.setopt(pycurl.VERBOSE, False)
296 curl.setopt(pycurl.FOLLOWLOCATION, False)
297 curl.setopt(pycurl.MAXREDIRS, 5)
298 curl.setopt(pycurl.NOSIGNAL, True)
299 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
300 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
301 curl.setopt(pycurl.SSL_VERIFYPEER, False)
302 curl.setopt(pycurl.HTTPHEADER, [
303 "Accept: %s" % HTTP_APP_JSON,
304 "Content-type: %s" % HTTP_APP_JSON,
305 ])
306
307 assert ((self._username is None and self._password is None) ^
308 (self._username is not None and self._password is not None))
309
310 if self._username:
311
312 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
313 curl.setopt(pycurl.USERPWD,
314 str("%s:%s" % (self._username, self._password)))
315
316
317 if self._curl_config_fn:
318 self._curl_config_fn(curl, self._logger)
319
320 return curl
321
322 @staticmethod
324 """Encode query values for RAPI URL.
325
326 @type query: list of two-tuples
327 @param query: Query arguments
328 @rtype: list
329 @return: Query list with encoded values
330
331 """
332 result = []
333
334 for name, value in query:
335 if value is None:
336 result.append((name, ""))
337
338 elif isinstance(value, bool):
339
340 result.append((name, int(value)))
341
342 elif isinstance(value, (list, tuple, dict)):
343 raise ValueError("Invalid query data type %r" % type(value).__name__)
344
345 else:
346 result.append((name, value))
347
348 return result
349
351 """Sends an HTTP request.
352
353 This constructs a full URL, encodes and decodes HTTP bodies, and
354 handles invalid responses in a pythonic way.
355
356 @type method: string
357 @param method: HTTP method to use
358 @type path: string
359 @param path: HTTP URL path
360 @type query: list of two-tuples
361 @param query: query arguments to pass to urllib.urlencode
362 @type content: str or None
363 @param content: HTTP body content
364
365 @rtype: str
366 @return: JSON-Decoded response
367
368 @raises CertificateError: If an invalid SSL certificate is found
369 @raises GanetiApiError: If an invalid response is returned
370
371 """
372 assert path.startswith("/")
373
374 curl = self._CreateCurl()
375
376 if content is not None:
377 encoded_content = self._json_encoder.encode(content)
378 else:
379 encoded_content = ""
380
381
382 urlparts = [self._base_url, path]
383 if query:
384 urlparts.append("?")
385 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
386
387 url = "".join(urlparts)
388
389 self._logger.debug("Sending request %s %s (content=%r)",
390 method, url, encoded_content)
391
392
393 encoded_resp_body = StringIO()
394
395
396 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
397 curl.setopt(pycurl.URL, str(url))
398 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
399 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
400
401 try:
402
403 try:
404 curl.perform()
405 except pycurl.error, err:
406 if err.args[0] in _CURL_SSL_CERT_ERRORS:
407 raise CertificateError("SSL certificate error %s" % err)
408
409 raise GanetiApiError(str(err))
410 finally:
411
412
413 curl.setopt(pycurl.POSTFIELDS, "")
414 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
415
416
417 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
418
419
420 if encoded_resp_body.tell():
421 response_content = simplejson.loads(encoded_resp_body.getvalue())
422 else:
423 response_content = None
424
425 if http_code != HTTP_OK:
426 if isinstance(response_content, dict):
427 msg = ("%s %s: %s" %
428 (response_content["code"],
429 response_content["message"],
430 response_content["explain"]))
431 else:
432 msg = str(response_content)
433
434 raise GanetiApiError(msg, code=http_code)
435
436 return response_content
437
439 """Gets the Remote API version running on the cluster.
440
441 @rtype: int
442 @return: Ganeti Remote API version
443
444 """
445 return self._SendRequest(HTTP_GET, "/version", None, None)
446
463
465 """Gets the Operating Systems running in the Ganeti cluster.
466
467 @rtype: list of str
468 @return: operating systems
469
470 """
471 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
472 None, None)
473
475 """Gets info about the cluster.
476
477 @rtype: dict
478 @return: information about the cluster
479
480 """
481 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
482 None, None)
483
485 """Tells the cluster to redistribute its configuration files.
486
487 @return: job id
488
489 """
490 return self._SendRequest(HTTP_PUT,
491 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
492 None, None)
493
495 """Modifies cluster parameters.
496
497 More details for parameters can be found in the RAPI documentation.
498
499 @rtype: int
500 @return: job id
501
502 """
503 body = kwargs
504
505 return self._SendRequest(HTTP_PUT,
506 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
507
517
536
552
554 """Gets information about instances on the cluster.
555
556 @type bulk: bool
557 @param bulk: whether to return all information about all instances
558
559 @rtype: list of dict or list of str
560 @return: if bulk is True, info about the instances, else a list of instances
561
562 """
563 query = []
564 if bulk:
565 query.append(("bulk", 1))
566
567 instances = self._SendRequest(HTTP_GET,
568 "/%s/instances" % GANETI_RAPI_VERSION,
569 query, None)
570 if bulk:
571 return instances
572 else:
573 return [i["id"] for i in instances]
574
576 """Gets information about an instance.
577
578 @type instance: str
579 @param instance: instance whose info to return
580
581 @rtype: dict
582 @return: info about the instance
583
584 """
585 return self._SendRequest(HTTP_GET,
586 ("/%s/instances/%s" %
587 (GANETI_RAPI_VERSION, instance)), None, None)
588
590 """Gets information about an instance.
591
592 @type instance: string
593 @param instance: Instance name
594 @rtype: string
595 @return: Job ID
596
597 """
598 if static is not None:
599 query = [("static", static)]
600 else:
601 query = None
602
603 return self._SendRequest(HTTP_GET,
604 ("/%s/instances/%s/info" %
605 (GANETI_RAPI_VERSION, instance)), query, None)
606
607 - def CreateInstance(self, mode, name, disk_template, disks, nics,
608 **kwargs):
609 """Creates a new instance.
610
611 More details for parameters can be found in the RAPI documentation.
612
613 @type mode: string
614 @param mode: Instance creation mode
615 @type name: string
616 @param name: Hostname of the instance to create
617 @type disk_template: string
618 @param disk_template: Disk template for instance (e.g. plain, diskless,
619 file, or drbd)
620 @type disks: list of dicts
621 @param disks: List of disk definitions
622 @type nics: list of dicts
623 @param nics: List of NIC definitions
624 @type dry_run: bool
625 @keyword dry_run: whether to perform a dry run
626
627 @rtype: int
628 @return: job id
629
630 """
631 query = []
632
633 if kwargs.get("dry_run"):
634 query.append(("dry-run", 1))
635
636 if _INST_CREATE_REQV1 in self.GetFeatures():
637
638 body = {
639 _REQ_DATA_VERSION_FIELD: 1,
640 "mode": mode,
641 "name": name,
642 "disk_template": disk_template,
643 "disks": disks,
644 "nics": nics,
645 }
646
647 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
648 if conflicts:
649 raise GanetiApiError("Required fields can not be specified as"
650 " keywords: %s" % ", ".join(conflicts))
651
652 body.update((key, value) for key, value in kwargs.iteritems()
653 if key != "dry_run")
654 else:
655
656
657
658
659
660
661
662
663
664 for idx, disk in enumerate(disks):
665 unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS
666 if unsupported:
667 raise GanetiApiError("Server supports request version 0 only, but"
668 " disk %s specifies the unsupported parameters"
669 " %s, allowed are %s" %
670 (idx, unsupported,
671 list(_INST_CREATE_V0_DISK_PARAMS)))
672
673 assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and
674 "size" in _INST_CREATE_V0_DISK_PARAMS)
675 disk_sizes = [disk["size"] for disk in disks]
676
677
678 if not nics:
679 raise GanetiApiError("Server supports request version 0 only, but"
680 " no NIC specified")
681 elif len(nics) > 1:
682 raise GanetiApiError("Server supports request version 0 only, but"
683 " more than one NIC specified")
684
685 assert len(nics) == 1
686
687 unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS
688 if unsupported:
689 raise GanetiApiError("Server supports request version 0 only, but"
690 " NIC 0 specifies the unsupported parameters %s,"
691 " allowed are %s" %
692 (unsupported, list(_INST_NIC_PARAMS)))
693
694
695 unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS -
696 _INST_CREATE_V0_DPARAMS)
697 if unsupported:
698 allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS)
699 raise GanetiApiError("Server supports request version 0 only, but"
700 " the following unsupported parameters are"
701 " specified: %s, allowed are %s" %
702 (unsupported, list(allowed)))
703
704
705 body = {
706 _REQ_DATA_VERSION_FIELD: 0,
707 "name": name,
708 "disk_template": disk_template,
709 "disks": disk_sizes,
710 }
711
712
713 assert len(nics) == 1
714 assert not (set(body.keys()) & set(nics[0].keys()))
715 body.update(nics[0])
716
717
718 assert not (set(body.keys()) & set(kwargs.keys()))
719 body.update(dict((key, value) for key, value in kwargs.items()
720 if key in _INST_CREATE_V0_PARAMS))
721
722
723 for i in (value for key, value in kwargs.items()
724 if key in _INST_CREATE_V0_DPARAMS):
725 assert not (set(body.keys()) & set(i.keys()))
726 body.update(i)
727
728 assert not (set(kwargs.keys()) -
729 (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS))
730 assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS)
731
732 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
733 query, body)
734
736 """Deletes an instance.
737
738 @type instance: str
739 @param instance: the instance to delete
740
741 @rtype: int
742 @return: job id
743
744 """
745 query = []
746 if dry_run:
747 query.append(("dry-run", 1))
748
749 return self._SendRequest(HTTP_DELETE,
750 ("/%s/instances/%s" %
751 (GANETI_RAPI_VERSION, instance)), query, None)
752
754 """Modifies an instance.
755
756 More details for parameters can be found in the RAPI documentation.
757
758 @type instance: string
759 @param instance: Instance name
760 @rtype: int
761 @return: job id
762
763 """
764 body = kwargs
765
766 return self._SendRequest(HTTP_PUT,
767 ("/%s/instances/%s/modify" %
768 (GANETI_RAPI_VERSION, instance)), None, body)
769
771 """Activates an instance's disks.
772
773 @type instance: string
774 @param instance: Instance name
775 @type ignore_size: bool
776 @param ignore_size: Whether to ignore recorded size
777 @return: job id
778
779 """
780 query = []
781 if ignore_size:
782 query.append(("ignore_size", 1))
783
784 return self._SendRequest(HTTP_PUT,
785 ("/%s/instances/%s/activate-disks" %
786 (GANETI_RAPI_VERSION, instance)), query, None)
787
789 """Deactivates an instance's disks.
790
791 @type instance: string
792 @param instance: Instance name
793 @return: job id
794
795 """
796 return self._SendRequest(HTTP_PUT,
797 ("/%s/instances/%s/deactivate-disks" %
798 (GANETI_RAPI_VERSION, instance)), None, None)
799
801 """Grows a disk of an instance.
802
803 More details for parameters can be found in the RAPI documentation.
804
805 @type instance: string
806 @param instance: Instance name
807 @type disk: integer
808 @param disk: Disk index
809 @type amount: integer
810 @param amount: Grow disk by this amount (MiB)
811 @type wait_for_sync: bool
812 @param wait_for_sync: Wait for disk to synchronize
813 @rtype: int
814 @return: job id
815
816 """
817 body = {
818 "amount": amount,
819 }
820
821 if wait_for_sync is not None:
822 body["wait_for_sync"] = wait_for_sync
823
824 return self._SendRequest(HTTP_POST,
825 ("/%s/instances/%s/disk/%s/grow" %
826 (GANETI_RAPI_VERSION, instance, disk)),
827 None, body)
828
842
864
883
884 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
885 dry_run=False):
886 """Reboots an instance.
887
888 @type instance: str
889 @param instance: instance to rebot
890 @type reboot_type: str
891 @param reboot_type: one of: hard, soft, full
892 @type ignore_secondaries: bool
893 @param ignore_secondaries: if True, ignores errors for the secondary node
894 while re-assembling disks (in hard-reboot mode only)
895 @type dry_run: bool
896 @param dry_run: whether to perform a dry run
897
898 """
899 query = []
900 if reboot_type:
901 query.append(("type", reboot_type))
902 if ignore_secondaries is not None:
903 query.append(("ignore_secondaries", ignore_secondaries))
904 if dry_run:
905 query.append(("dry-run", 1))
906
907 return self._SendRequest(HTTP_POST,
908 ("/%s/instances/%s/reboot" %
909 (GANETI_RAPI_VERSION, instance)), query, None)
910
912 """Shuts down an instance.
913
914 @type instance: str
915 @param instance: the instance to shut down
916 @type dry_run: bool
917 @param dry_run: whether to perform a dry run
918 @type no_remember: bool
919 @param no_remember: if true, will not record the state change
920
921 """
922 query = []
923 if dry_run:
924 query.append(("dry-run", 1))
925 if no_remember:
926 query.append(("no-remember", 1))
927
928 return self._SendRequest(HTTP_PUT,
929 ("/%s/instances/%s/shutdown" %
930 (GANETI_RAPI_VERSION, instance)), query, None)
931
933 """Starts up an instance.
934
935 @type instance: str
936 @param instance: the instance to start up
937 @type dry_run: bool
938 @param dry_run: whether to perform a dry run
939 @type no_remember: bool
940 @param no_remember: if true, will not record the state change
941
942 """
943 query = []
944 if dry_run:
945 query.append(("dry-run", 1))
946 if no_remember:
947 query.append(("no-remember", 1))
948
949 return self._SendRequest(HTTP_PUT,
950 ("/%s/instances/%s/startup" %
951 (GANETI_RAPI_VERSION, instance)), query, None)
952
953 - def ReinstallInstance(self, instance, os=None, no_startup=False,
954 osparams=None):
955 """Reinstalls an instance.
956
957 @type instance: str
958 @param instance: The instance to reinstall
959 @type os: str or None
960 @param os: The operating system to reinstall. If None, the instance's
961 current operating system will be installed again
962 @type no_startup: bool
963 @param no_startup: Whether to start the instance automatically
964
965 """
966 if _INST_REINSTALL_REQV1 in self.GetFeatures():
967 body = {
968 "start": not no_startup,
969 }
970 if os is not None:
971 body["os"] = os
972 if osparams is not None:
973 body["osparams"] = osparams
974 return self._SendRequest(HTTP_POST,
975 ("/%s/instances/%s/reinstall" %
976 (GANETI_RAPI_VERSION, instance)), None, body)
977
978
979 if osparams:
980 raise GanetiApiError("Server does not support specifying OS parameters"
981 " for instance reinstallation")
982
983 query = []
984 if os:
985 query.append(("os", os))
986 if no_startup:
987 query.append(("nostartup", 1))
988 return self._SendRequest(HTTP_POST,
989 ("/%s/instances/%s/reinstall" %
990 (GANETI_RAPI_VERSION, instance)), query, None)
991
994 """Replaces disks on an instance.
995
996 @type instance: str
997 @param instance: instance whose disks to replace
998 @type disks: list of ints
999 @param disks: Indexes of disks to replace
1000 @type mode: str
1001 @param mode: replacement mode to use (defaults to replace_auto)
1002 @type remote_node: str or None
1003 @param remote_node: new secondary node to use (for use with
1004 replace_new_secondary mode)
1005 @type iallocator: str or None
1006 @param iallocator: instance allocator plugin to use (for use with
1007 replace_auto mode)
1008 @type dry_run: bool
1009 @param dry_run: whether to perform a dry run
1010
1011 @rtype: int
1012 @return: job id
1013
1014 """
1015 query = [
1016 ("mode", mode),
1017 ]
1018
1019 if disks:
1020 query.append(("disks", ",".join(str(idx) for idx in disks)))
1021
1022 if remote_node:
1023 query.append(("remote_node", remote_node))
1024
1025 if iallocator:
1026 query.append(("iallocator", iallocator))
1027
1028 if dry_run:
1029 query.append(("dry-run", 1))
1030
1031 return self._SendRequest(HTTP_POST,
1032 ("/%s/instances/%s/replace-disks" %
1033 (GANETI_RAPI_VERSION, instance)), query, None)
1034
1036 """Prepares an instance for an export.
1037
1038 @type instance: string
1039 @param instance: Instance name
1040 @type mode: string
1041 @param mode: Export mode
1042 @rtype: string
1043 @return: Job ID
1044
1045 """
1046 query = [("mode", mode)]
1047 return self._SendRequest(HTTP_PUT,
1048 ("/%s/instances/%s/prepare-export" %
1049 (GANETI_RAPI_VERSION, instance)), query, None)
1050
1051 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1052 remove_instance=None,
1053 x509_key_name=None, destination_x509_ca=None):
1054 """Exports an instance.
1055
1056 @type instance: string
1057 @param instance: Instance name
1058 @type mode: string
1059 @param mode: Export mode
1060 @rtype: string
1061 @return: Job ID
1062
1063 """
1064 body = {
1065 "destination": destination,
1066 "mode": mode,
1067 }
1068
1069 if shutdown is not None:
1070 body["shutdown"] = shutdown
1071
1072 if remove_instance is not None:
1073 body["remove_instance"] = remove_instance
1074
1075 if x509_key_name is not None:
1076 body["x509_key_name"] = x509_key_name
1077
1078 if destination_x509_ca is not None:
1079 body["destination_x509_ca"] = destination_x509_ca
1080
1081 return self._SendRequest(HTTP_PUT,
1082 ("/%s/instances/%s/export" %
1083 (GANETI_RAPI_VERSION, instance)), None, body)
1084
1086 """Migrates an instance.
1087
1088 @type instance: string
1089 @param instance: Instance name
1090 @type mode: string
1091 @param mode: Migration mode
1092 @type cleanup: bool
1093 @param cleanup: Whether to clean up a previously failed migration
1094
1095 """
1096 body = {}
1097
1098 if mode is not None:
1099 body["mode"] = mode
1100
1101 if cleanup is not None:
1102 body["cleanup"] = cleanup
1103
1104 return self._SendRequest(HTTP_PUT,
1105 ("/%s/instances/%s/migrate" %
1106 (GANETI_RAPI_VERSION, instance)), None, body)
1107
1108 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1109 """Changes the name of an instance.
1110
1111 @type instance: string
1112 @param instance: Instance name
1113 @type new_name: string
1114 @param new_name: New instance name
1115 @type ip_check: bool
1116 @param ip_check: Whether to ensure instance's IP address is inactive
1117 @type name_check: bool
1118 @param name_check: Whether to ensure instance's name is resolvable
1119
1120 """
1121 body = {
1122 "new_name": new_name,
1123 }
1124
1125 if ip_check is not None:
1126 body["ip_check"] = ip_check
1127
1128 if name_check is not None:
1129 body["name_check"] = name_check
1130
1131 return self._SendRequest(HTTP_PUT,
1132 ("/%s/instances/%s/rename" %
1133 (GANETI_RAPI_VERSION, instance)), None, body)
1134
1136 """Request information for connecting to instance's console.
1137
1138 @type instance: string
1139 @param instance: Instance name
1140
1141 """
1142 return self._SendRequest(HTTP_GET,
1143 ("/%s/instances/%s/console" %
1144 (GANETI_RAPI_VERSION, instance)), None, None)
1145
1147 """Gets all jobs for the cluster.
1148
1149 @rtype: list of int
1150 @return: job ids for the cluster
1151
1152 """
1153 return [int(j["id"])
1154 for j in self._SendRequest(HTTP_GET,
1155 "/%s/jobs" % GANETI_RAPI_VERSION,
1156 None, None)]
1157
1159 """Gets the status of a job.
1160
1161 @type job_id: int
1162 @param job_id: job id whose status to query
1163
1164 @rtype: dict
1165 @return: job status
1166
1167 """
1168 return self._SendRequest(HTTP_GET,
1169 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1170 None, None)
1171
1173 """Waits for job changes.
1174
1175 @type job_id: int
1176 @param job_id: Job ID for which to wait
1177
1178 """
1179 body = {
1180 "fields": fields,
1181 "previous_job_info": prev_job_info,
1182 "previous_log_serial": prev_log_serial,
1183 }
1184
1185 return self._SendRequest(HTTP_GET,
1186 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1187 None, body)
1188
1189 - def CancelJob(self, job_id, dry_run=False):
1190 """Cancels a job.
1191
1192 @type job_id: int
1193 @param job_id: id of the job to delete
1194 @type dry_run: bool
1195 @param dry_run: whether to perform a dry run
1196
1197 """
1198 query = []
1199 if dry_run:
1200 query.append(("dry-run", 1))
1201
1202 return self._SendRequest(HTTP_DELETE,
1203 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1204 query, None)
1205
1207 """Gets all nodes in the cluster.
1208
1209 @type bulk: bool
1210 @param bulk: whether to return all information about all instances
1211
1212 @rtype: list of dict or str
1213 @return: if bulk is true, info about nodes in the cluster,
1214 else list of nodes in the cluster
1215
1216 """
1217 query = []
1218 if bulk:
1219 query.append(("bulk", 1))
1220
1221 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1222 query, None)
1223 if bulk:
1224 return nodes
1225 else:
1226 return [n["id"] for n in nodes]
1227
1229 """Gets information about a node.
1230
1231 @type node: str
1232 @param node: node whose info to return
1233
1234 @rtype: dict
1235 @return: info about the node
1236
1237 """
1238 return self._SendRequest(HTTP_GET,
1239 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1240 None, None)
1241
1242 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1243 dry_run=False, early_release=False):
1244 """Evacuates instances from a Ganeti node.
1245
1246 @type node: str
1247 @param node: node to evacuate
1248 @type iallocator: str or None
1249 @param iallocator: instance allocator to use
1250 @type remote_node: str
1251 @param remote_node: node to evaucate to
1252 @type dry_run: bool
1253 @param dry_run: whether to perform a dry run
1254 @type early_release: bool
1255 @param early_release: whether to enable parallelization
1256
1257 @rtype: list
1258 @return: list of (job ID, instance name, new secondary node); if
1259 dry_run was specified, then the actual move jobs were not
1260 submitted and the job IDs will be C{None}
1261
1262 @raises GanetiApiError: if an iallocator and remote_node are both
1263 specified
1264
1265 """
1266 if iallocator and remote_node:
1267 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1268
1269 query = []
1270 if iallocator:
1271 query.append(("iallocator", iallocator))
1272 if remote_node:
1273 query.append(("remote_node", remote_node))
1274 if dry_run:
1275 query.append(("dry-run", 1))
1276 if early_release:
1277 query.append(("early_release", 1))
1278
1279 return self._SendRequest(HTTP_POST,
1280 ("/%s/nodes/%s/evacuate" %
1281 (GANETI_RAPI_VERSION, node)), query, None)
1282
1283 - def MigrateNode(self, node, mode=None, dry_run=False):
1284 """Migrates all primary instances from a node.
1285
1286 @type node: str
1287 @param node: node to migrate
1288 @type mode: string
1289 @param mode: if passed, it will overwrite the live migration type,
1290 otherwise the hypervisor default will be used
1291 @type dry_run: bool
1292 @param dry_run: whether to perform a dry run
1293
1294 @rtype: int
1295 @return: job id
1296
1297 """
1298 query = []
1299 if mode is not None:
1300 query.append(("mode", mode))
1301 if dry_run:
1302 query.append(("dry-run", 1))
1303
1304 return self._SendRequest(HTTP_POST,
1305 ("/%s/nodes/%s/migrate" %
1306 (GANETI_RAPI_VERSION, node)), query, None)
1307
1309 """Gets the current role for a node.
1310
1311 @type node: str
1312 @param node: node whose role to return
1313
1314 @rtype: str
1315 @return: the current role for a node
1316
1317 """
1318 return self._SendRequest(HTTP_GET,
1319 ("/%s/nodes/%s/role" %
1320 (GANETI_RAPI_VERSION, node)), None, None)
1321
1323 """Sets the role for a node.
1324
1325 @type node: str
1326 @param node: the node whose role to set
1327 @type role: str
1328 @param role: the role to set for the node
1329 @type force: bool
1330 @param force: whether to force the role change
1331
1332 @rtype: int
1333 @return: job id
1334
1335 """
1336 query = [
1337 ("force", force),
1338 ]
1339
1340 return self._SendRequest(HTTP_PUT,
1341 ("/%s/nodes/%s/role" %
1342 (GANETI_RAPI_VERSION, node)), query, role)
1343
1345 """Gets the storage units for a node.
1346
1347 @type node: str
1348 @param node: the node whose storage units to return
1349 @type storage_type: str
1350 @param storage_type: storage type whose units to return
1351 @type output_fields: str
1352 @param output_fields: storage type fields to return
1353
1354 @rtype: int
1355 @return: job id where results can be retrieved
1356
1357 """
1358 query = [
1359 ("storage_type", storage_type),
1360 ("output_fields", output_fields),
1361 ]
1362
1363 return self._SendRequest(HTTP_GET,
1364 ("/%s/nodes/%s/storage" %
1365 (GANETI_RAPI_VERSION, node)), query, None)
1366
1368 """Modifies parameters of storage units on the node.
1369
1370 @type node: str
1371 @param node: node whose storage units to modify
1372 @type storage_type: str
1373 @param storage_type: storage type whose units to modify
1374 @type name: str
1375 @param name: name of the storage unit
1376 @type allocatable: bool or None
1377 @param allocatable: Whether to set the "allocatable" flag on the storage
1378 unit (None=no modification, True=set, False=unset)
1379
1380 @rtype: int
1381 @return: job id
1382
1383 """
1384 query = [
1385 ("storage_type", storage_type),
1386 ("name", name),
1387 ]
1388
1389 if allocatable is not None:
1390 query.append(("allocatable", allocatable))
1391
1392 return self._SendRequest(HTTP_PUT,
1393 ("/%s/nodes/%s/storage/modify" %
1394 (GANETI_RAPI_VERSION, node)), query, None)
1395
1397 """Repairs a storage unit on the node.
1398
1399 @type node: str
1400 @param node: node whose storage units to repair
1401 @type storage_type: str
1402 @param storage_type: storage type to repair
1403 @type name: str
1404 @param name: name of the storage unit to repair
1405
1406 @rtype: int
1407 @return: job id
1408
1409 """
1410 query = [
1411 ("storage_type", storage_type),
1412 ("name", name),
1413 ]
1414
1415 return self._SendRequest(HTTP_PUT,
1416 ("/%s/nodes/%s/storage/repair" %
1417 (GANETI_RAPI_VERSION, node)), query, None)
1418
1432
1454
1476
1478 """Gets all node groups in the cluster.
1479
1480 @type bulk: bool
1481 @param bulk: whether to return all information about the groups
1482
1483 @rtype: list of dict or str
1484 @return: if bulk is true, a list of dictionaries with info about all node
1485 groups in the cluster, else a list of names of those node groups
1486
1487 """
1488 query = []
1489 if bulk:
1490 query.append(("bulk", 1))
1491
1492 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1493 query, None)
1494 if bulk:
1495 return groups
1496 else:
1497 return [g["name"] for g in groups]
1498
1500 """Gets information about a node group.
1501
1502 @type group: str
1503 @param group: name of the node group whose info to return
1504
1505 @rtype: dict
1506 @return: info about the node group
1507
1508 """
1509 return self._SendRequest(HTTP_GET,
1510 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1511 None, None)
1512
1513 - def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1514 """Creates a new node group.
1515
1516 @type name: str
1517 @param name: the name of node group to create
1518 @type alloc_policy: str
1519 @param alloc_policy: the desired allocation policy for the group, if any
1520 @type dry_run: bool
1521 @param dry_run: whether to peform a dry run
1522
1523 @rtype: int
1524 @return: job id
1525
1526 """
1527 query = []
1528 if dry_run:
1529 query.append(("dry-run", 1))
1530
1531 body = {
1532 "name": name,
1533 "alloc_policy": alloc_policy
1534 }
1535
1536 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1537 query, body)
1538
1540 """Modifies a node group.
1541
1542 More details for parameters can be found in the RAPI documentation.
1543
1544 @type group: string
1545 @param group: Node group name
1546 @rtype: int
1547 @return: job id
1548
1549 """
1550 return self._SendRequest(HTTP_PUT,
1551 ("/%s/groups/%s/modify" %
1552 (GANETI_RAPI_VERSION, group)), None, kwargs)
1553
1555 """Deletes a node group.
1556
1557 @type group: str
1558 @param group: the node group to delete
1559 @type dry_run: bool
1560 @param dry_run: whether to peform a dry run
1561
1562 @rtype: int
1563 @return: job id
1564
1565 """
1566 query = []
1567 if dry_run:
1568 query.append(("dry-run", 1))
1569
1570 return self._SendRequest(HTTP_DELETE,
1571 ("/%s/groups/%s" %
1572 (GANETI_RAPI_VERSION, group)), query, None)
1573
1575 """Changes the name of a node group.
1576
1577 @type group: string
1578 @param group: Node group name
1579 @type new_name: string
1580 @param new_name: New node group name
1581
1582 @rtype: int
1583 @return: job id
1584
1585 """
1586 body = {
1587 "new_name": new_name,
1588 }
1589
1590 return self._SendRequest(HTTP_PUT,
1591 ("/%s/groups/%s/rename" %
1592 (GANETI_RAPI_VERSION, group)), None, body)
1593
1594
1596 """Assigns nodes to a group.
1597
1598 @type group: string
1599 @param group: Node gropu name
1600 @type nodes: list of strings
1601 @param nodes: List of nodes to assign to the group
1602
1603 @rtype: int
1604 @return: job id
1605
1606 """
1607 query = []
1608
1609 if force:
1610 query.append(("force", 1))
1611
1612 if dry_run:
1613 query.append(("dry-run", 1))
1614
1615 body = {
1616 "nodes": nodes,
1617 }
1618
1619 return self._SendRequest(HTTP_PUT,
1620 ("/%s/groups/%s/assign-nodes" %
1621 (GANETI_RAPI_VERSION, group)), query, body)
1622