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 _INST_CREATE_REQV1 = "instance-create-reqv1"
101 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
102 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
103 _NODE_EVAC_RES1 = "node-evac-res1"
104 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"])
105 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
106 _INST_CREATE_V0_PARAMS = frozenset([
107 "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
108 "hypervisor", "file_storage_dir", "file_driver", "dry_run",
109 ])
110 _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
111
112
113 try:
114 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
115 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
116 except AttributeError:
117 _CURLE_SSL_CACERT = 60
118 _CURLE_SSL_CACERT_BADFILE = 77
119
120 _CURL_SSL_CERT_ERRORS = frozenset([
121 _CURLE_SSL_CACERT,
122 _CURLE_SSL_CACERT_BADFILE,
123 ])
124
125
126 -class Error(Exception):
127 """Base error class for this module.
128
129 """
130 pass
131
134 """Raised when a problem is found with the SSL certificate.
135
136 """
137 pass
138
141 """Generic error raised from Ganeti API.
142
143 """
147
150 """Decorator for code using RAPI client to initialize pycURL.
151
152 """
153 def wrapper(*args, **kwargs):
154
155
156
157 assert threading.activeCount() == 1, \
158 "Found active threads when initializing pycURL"
159
160 pycurl.global_init(pycurl.GLOBAL_ALL)
161 try:
162 return fn(*args, **kwargs)
163 finally:
164 pycurl.global_cleanup()
165
166 return wrapper
167
168
169 -def GenericCurlConfig(verbose=False, use_signal=False,
170 use_curl_cabundle=False, cafile=None, capath=None,
171 proxy=None, verify_hostname=False,
172 connect_timeout=None, timeout=None,
173 _pycurl_version_fn=pycurl.version_info):
174 """Curl configuration function generator.
175
176 @type verbose: bool
177 @param verbose: Whether to set cURL to verbose mode
178 @type use_signal: bool
179 @param use_signal: Whether to allow cURL to use signals
180 @type use_curl_cabundle: bool
181 @param use_curl_cabundle: Whether to use cURL's default CA bundle
182 @type cafile: string
183 @param cafile: In which file we can find the certificates
184 @type capath: string
185 @param capath: In which directory we can find the certificates
186 @type proxy: string
187 @param proxy: Proxy to use, None for default behaviour and empty string for
188 disabling proxies (see curl_easy_setopt(3))
189 @type verify_hostname: bool
190 @param verify_hostname: Whether to verify the remote peer certificate's
191 commonName
192 @type connect_timeout: number
193 @param connect_timeout: Timeout for establishing connection in seconds
194 @type timeout: number
195 @param timeout: Timeout for complete transfer in seconds (see
196 curl_easy_setopt(3)).
197
198 """
199 if use_curl_cabundle and (cafile or capath):
200 raise Error("Can not use default CA bundle when CA file or path is set")
201
202 def _ConfigCurl(curl, logger):
203 """Configures a cURL object
204
205 @type curl: pycurl.Curl
206 @param curl: cURL object
207
208 """
209 logger.debug("Using cURL version %s", pycurl.version)
210
211
212
213
214
215 sslver = _pycurl_version_fn()[5]
216 if not sslver:
217 raise Error("No SSL support in cURL")
218
219 lcsslver = sslver.lower()
220 if lcsslver.startswith("openssl/"):
221 pass
222 elif lcsslver.startswith("gnutls/"):
223 if capath:
224 raise Error("cURL linked against GnuTLS has no support for a"
225 " CA path (%s)" % (pycurl.version, ))
226 else:
227 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
228 sslver)
229
230 curl.setopt(pycurl.VERBOSE, verbose)
231 curl.setopt(pycurl.NOSIGNAL, not use_signal)
232
233
234 if verify_hostname:
235
236
237
238
239
240 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
241 else:
242 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
243
244 if cafile or capath or use_curl_cabundle:
245
246 curl.setopt(pycurl.SSL_VERIFYPEER, True)
247 if cafile:
248 curl.setopt(pycurl.CAINFO, str(cafile))
249 if capath:
250 curl.setopt(pycurl.CAPATH, str(capath))
251
252 else:
253
254 curl.setopt(pycurl.SSL_VERIFYPEER, False)
255
256 if proxy is not None:
257 curl.setopt(pycurl.PROXY, str(proxy))
258
259
260 if connect_timeout is not None:
261 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
262 if timeout is not None:
263 curl.setopt(pycurl.TIMEOUT, timeout)
264
265 return _ConfigCurl
266
269 """Ganeti RAPI client.
270
271 """
272 USER_AGENT = "Ganeti RAPI Client"
273 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
274
275 - def __init__(self, host, port=GANETI_RAPI_PORT,
276 username=None, password=None, logger=logging,
277 curl_config_fn=None, curl_factory=None):
278 """Initializes this class.
279
280 @type host: string
281 @param host: the ganeti cluster master to interact with
282 @type port: int
283 @param port: the port on which the RAPI is running (default is 5080)
284 @type username: string
285 @param username: the username to connect with
286 @type password: string
287 @param password: the password to connect with
288 @type curl_config_fn: callable
289 @param curl_config_fn: Function to configure C{pycurl.Curl} object
290 @param logger: Logging object
291
292 """
293 self._username = username
294 self._password = password
295 self._logger = logger
296 self._curl_config_fn = curl_config_fn
297 self._curl_factory = curl_factory
298
299 try:
300 socket.inet_pton(socket.AF_INET6, host)
301 address = "[%s]:%s" % (host, port)
302 except socket.error:
303 address = "%s:%s" % (host, port)
304
305 self._base_url = "https://%s" % address
306
307 if username is not None:
308 if password is None:
309 raise Error("Password not specified")
310 elif password:
311 raise Error("Specified password without username")
312
314 """Creates a cURL object.
315
316 """
317
318 if self._curl_factory:
319 curl = self._curl_factory()
320 else:
321 curl = pycurl.Curl()
322
323
324 curl.setopt(pycurl.VERBOSE, False)
325 curl.setopt(pycurl.FOLLOWLOCATION, False)
326 curl.setopt(pycurl.MAXREDIRS, 5)
327 curl.setopt(pycurl.NOSIGNAL, True)
328 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
329 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
330 curl.setopt(pycurl.SSL_VERIFYPEER, False)
331 curl.setopt(pycurl.HTTPHEADER, [
332 "Accept: %s" % HTTP_APP_JSON,
333 "Content-type: %s" % HTTP_APP_JSON,
334 ])
335
336 assert ((self._username is None and self._password is None) ^
337 (self._username is not None and self._password is not None))
338
339 if self._username:
340
341 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
342 curl.setopt(pycurl.USERPWD,
343 str("%s:%s" % (self._username, self._password)))
344
345
346 if self._curl_config_fn:
347 self._curl_config_fn(curl, self._logger)
348
349 return curl
350
351 @staticmethod
353 """Encode query values for RAPI URL.
354
355 @type query: list of two-tuples
356 @param query: Query arguments
357 @rtype: list
358 @return: Query list with encoded values
359
360 """
361 result = []
362
363 for name, value in query:
364 if value is None:
365 result.append((name, ""))
366
367 elif isinstance(value, bool):
368
369 result.append((name, int(value)))
370
371 elif isinstance(value, (list, tuple, dict)):
372 raise ValueError("Invalid query data type %r" % type(value).__name__)
373
374 else:
375 result.append((name, value))
376
377 return result
378
380 """Sends an HTTP request.
381
382 This constructs a full URL, encodes and decodes HTTP bodies, and
383 handles invalid responses in a pythonic way.
384
385 @type method: string
386 @param method: HTTP method to use
387 @type path: string
388 @param path: HTTP URL path
389 @type query: list of two-tuples
390 @param query: query arguments to pass to urllib.urlencode
391 @type content: str or None
392 @param content: HTTP body content
393
394 @rtype: str
395 @return: JSON-Decoded response
396
397 @raises CertificateError: If an invalid SSL certificate is found
398 @raises GanetiApiError: If an invalid response is returned
399
400 """
401 assert path.startswith("/")
402
403 curl = self._CreateCurl()
404
405 if content is not None:
406 encoded_content = self._json_encoder.encode(content)
407 else:
408 encoded_content = ""
409
410
411 urlparts = [self._base_url, path]
412 if query:
413 urlparts.append("?")
414 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
415
416 url = "".join(urlparts)
417
418 self._logger.debug("Sending request %s %s (content=%r)",
419 method, url, encoded_content)
420
421
422 encoded_resp_body = StringIO()
423
424
425 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
426 curl.setopt(pycurl.URL, str(url))
427 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
428 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
429
430 try:
431
432 try:
433 curl.perform()
434 except pycurl.error, err:
435 if err.args[0] in _CURL_SSL_CERT_ERRORS:
436 raise CertificateError("SSL certificate error %s" % err)
437
438 raise GanetiApiError(str(err))
439 finally:
440
441
442 curl.setopt(pycurl.POSTFIELDS, "")
443 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
444
445
446 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
447
448
449 if encoded_resp_body.tell():
450 response_content = simplejson.loads(encoded_resp_body.getvalue())
451 else:
452 response_content = None
453
454 if http_code != HTTP_OK:
455 if isinstance(response_content, dict):
456 msg = ("%s %s: %s" %
457 (response_content["code"],
458 response_content["message"],
459 response_content["explain"]))
460 else:
461 msg = str(response_content)
462
463 raise GanetiApiError(msg, code=http_code)
464
465 return response_content
466
468 """Gets the Remote API version running on the cluster.
469
470 @rtype: int
471 @return: Ganeti Remote API version
472
473 """
474 return self._SendRequest(HTTP_GET, "/version", None, None)
475
492
494 """Gets the Operating Systems running in the Ganeti cluster.
495
496 @rtype: list of str
497 @return: operating systems
498
499 """
500 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
501 None, None)
502
504 """Gets info about the cluster.
505
506 @rtype: dict
507 @return: information about the cluster
508
509 """
510 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
511 None, None)
512
514 """Tells the cluster to redistribute its configuration files.
515
516 @rtype: string
517 @return: job id
518
519 """
520 return self._SendRequest(HTTP_PUT,
521 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
522 None, None)
523
525 """Modifies cluster parameters.
526
527 More details for parameters can be found in the RAPI documentation.
528
529 @rtype: string
530 @return: job id
531
532 """
533 body = kwargs
534
535 return self._SendRequest(HTTP_PUT,
536 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
537
547
566
584
586 """Gets information about instances on the cluster.
587
588 @type bulk: bool
589 @param bulk: whether to return all information about all instances
590
591 @rtype: list of dict or list of str
592 @return: if bulk is True, info about the instances, else a list of instances
593
594 """
595 query = []
596 if bulk:
597 query.append(("bulk", 1))
598
599 instances = self._SendRequest(HTTP_GET,
600 "/%s/instances" % GANETI_RAPI_VERSION,
601 query, None)
602 if bulk:
603 return instances
604 else:
605 return [i["id"] for i in instances]
606
608 """Gets information about an instance.
609
610 @type instance: str
611 @param instance: instance whose info to return
612
613 @rtype: dict
614 @return: info about the instance
615
616 """
617 return self._SendRequest(HTTP_GET,
618 ("/%s/instances/%s" %
619 (GANETI_RAPI_VERSION, instance)), None, None)
620
622 """Gets information about an instance.
623
624 @type instance: string
625 @param instance: Instance name
626 @rtype: string
627 @return: Job ID
628
629 """
630 if static is not None:
631 query = [("static", static)]
632 else:
633 query = None
634
635 return self._SendRequest(HTTP_GET,
636 ("/%s/instances/%s/info" %
637 (GANETI_RAPI_VERSION, instance)), query, None)
638
639 - def CreateInstance(self, mode, name, disk_template, disks, nics,
640 **kwargs):
641 """Creates a new instance.
642
643 More details for parameters can be found in the RAPI documentation.
644
645 @type mode: string
646 @param mode: Instance creation mode
647 @type name: string
648 @param name: Hostname of the instance to create
649 @type disk_template: string
650 @param disk_template: Disk template for instance (e.g. plain, diskless,
651 file, or drbd)
652 @type disks: list of dicts
653 @param disks: List of disk definitions
654 @type nics: list of dicts
655 @param nics: List of NIC definitions
656 @type dry_run: bool
657 @keyword dry_run: whether to perform a dry run
658
659 @rtype: string
660 @return: job id
661
662 """
663 query = []
664
665 if kwargs.get("dry_run"):
666 query.append(("dry-run", 1))
667
668 if _INST_CREATE_REQV1 in self.GetFeatures():
669
670 body = {
671 _REQ_DATA_VERSION_FIELD: 1,
672 "mode": mode,
673 "name": name,
674 "disk_template": disk_template,
675 "disks": disks,
676 "nics": nics,
677 }
678
679 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
680 if conflicts:
681 raise GanetiApiError("Required fields can not be specified as"
682 " keywords: %s" % ", ".join(conflicts))
683
684 body.update((key, value) for key, value in kwargs.iteritems()
685 if key != "dry_run")
686 else:
687 raise GanetiApiError("Server does not support new-style (version 1)"
688 " instance creation requests")
689
690 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
691 query, body)
692
694 """Deletes an instance.
695
696 @type instance: str
697 @param instance: the instance to delete
698
699 @rtype: string
700 @return: job id
701
702 """
703 query = []
704 if dry_run:
705 query.append(("dry-run", 1))
706
707 return self._SendRequest(HTTP_DELETE,
708 ("/%s/instances/%s" %
709 (GANETI_RAPI_VERSION, instance)), query, None)
710
712 """Modifies an instance.
713
714 More details for parameters can be found in the RAPI documentation.
715
716 @type instance: string
717 @param instance: Instance name
718 @rtype: string
719 @return: job id
720
721 """
722 body = kwargs
723
724 return self._SendRequest(HTTP_PUT,
725 ("/%s/instances/%s/modify" %
726 (GANETI_RAPI_VERSION, instance)), None, body)
727
729 """Activates an instance's disks.
730
731 @type instance: string
732 @param instance: Instance name
733 @type ignore_size: bool
734 @param ignore_size: Whether to ignore recorded size
735 @rtype: string
736 @return: job id
737
738 """
739 query = []
740 if ignore_size:
741 query.append(("ignore_size", 1))
742
743 return self._SendRequest(HTTP_PUT,
744 ("/%s/instances/%s/activate-disks" %
745 (GANETI_RAPI_VERSION, instance)), query, None)
746
748 """Deactivates an instance's disks.
749
750 @type instance: string
751 @param instance: Instance name
752 @rtype: string
753 @return: job id
754
755 """
756 return self._SendRequest(HTTP_PUT,
757 ("/%s/instances/%s/deactivate-disks" %
758 (GANETI_RAPI_VERSION, instance)), None, None)
759
761 """Grows a disk of an instance.
762
763 More details for parameters can be found in the RAPI documentation.
764
765 @type instance: string
766 @param instance: Instance name
767 @type disk: integer
768 @param disk: Disk index
769 @type amount: integer
770 @param amount: Grow disk by this amount (MiB)
771 @type wait_for_sync: bool
772 @param wait_for_sync: Wait for disk to synchronize
773 @rtype: string
774 @return: job id
775
776 """
777 body = {
778 "amount": amount,
779 }
780
781 if wait_for_sync is not None:
782 body["wait_for_sync"] = wait_for_sync
783
784 return self._SendRequest(HTTP_POST,
785 ("/%s/instances/%s/disk/%s/grow" %
786 (GANETI_RAPI_VERSION, instance, disk)),
787 None, body)
788
802
824
845
846 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
847 dry_run=False):
848 """Reboots an instance.
849
850 @type instance: str
851 @param instance: instance to rebot
852 @type reboot_type: str
853 @param reboot_type: one of: hard, soft, full
854 @type ignore_secondaries: bool
855 @param ignore_secondaries: if True, ignores errors for the secondary node
856 while re-assembling disks (in hard-reboot mode only)
857 @type dry_run: bool
858 @param dry_run: whether to perform a dry run
859 @rtype: string
860 @return: job id
861
862 """
863 query = []
864 if reboot_type:
865 query.append(("type", reboot_type))
866 if ignore_secondaries is not None:
867 query.append(("ignore_secondaries", ignore_secondaries))
868 if dry_run:
869 query.append(("dry-run", 1))
870
871 return self._SendRequest(HTTP_POST,
872 ("/%s/instances/%s/reboot" %
873 (GANETI_RAPI_VERSION, instance)), query, None)
874
876 """Shuts down an instance.
877
878 @type instance: str
879 @param instance: the instance to shut down
880 @type dry_run: bool
881 @param dry_run: whether to perform a dry run
882 @type no_remember: bool
883 @param no_remember: if true, will not record the state change
884 @rtype: string
885 @return: job id
886
887 """
888 query = []
889 if dry_run:
890 query.append(("dry-run", 1))
891 if no_remember:
892 query.append(("no-remember", 1))
893
894 return self._SendRequest(HTTP_PUT,
895 ("/%s/instances/%s/shutdown" %
896 (GANETI_RAPI_VERSION, instance)), query, None)
897
899 """Starts up an instance.
900
901 @type instance: str
902 @param instance: the instance to start up
903 @type dry_run: bool
904 @param dry_run: whether to perform a dry run
905 @type no_remember: bool
906 @param no_remember: if true, will not record the state change
907 @rtype: string
908 @return: job id
909
910 """
911 query = []
912 if dry_run:
913 query.append(("dry-run", 1))
914 if no_remember:
915 query.append(("no-remember", 1))
916
917 return self._SendRequest(HTTP_PUT,
918 ("/%s/instances/%s/startup" %
919 (GANETI_RAPI_VERSION, instance)), query, None)
920
921 - def ReinstallInstance(self, instance, os=None, no_startup=False,
922 osparams=None):
923 """Reinstalls an instance.
924
925 @type instance: str
926 @param instance: The instance to reinstall
927 @type os: str or None
928 @param os: The operating system to reinstall. If None, the instance's
929 current operating system will be installed again
930 @type no_startup: bool
931 @param no_startup: Whether to start the instance automatically
932 @rtype: string
933 @return: job id
934
935 """
936 if _INST_REINSTALL_REQV1 in self.GetFeatures():
937 body = {
938 "start": not no_startup,
939 }
940 if os is not None:
941 body["os"] = os
942 if osparams is not None:
943 body["osparams"] = osparams
944 return self._SendRequest(HTTP_POST,
945 ("/%s/instances/%s/reinstall" %
946 (GANETI_RAPI_VERSION, instance)), None, body)
947
948
949 if osparams:
950 raise GanetiApiError("Server does not support specifying OS parameters"
951 " for instance reinstallation")
952
953 query = []
954 if os:
955 query.append(("os", os))
956 if no_startup:
957 query.append(("nostartup", 1))
958 return self._SendRequest(HTTP_POST,
959 ("/%s/instances/%s/reinstall" %
960 (GANETI_RAPI_VERSION, instance)), query, None)
961
964 """Replaces disks on an instance.
965
966 @type instance: str
967 @param instance: instance whose disks to replace
968 @type disks: list of ints
969 @param disks: Indexes of disks to replace
970 @type mode: str
971 @param mode: replacement mode to use (defaults to replace_auto)
972 @type remote_node: str or None
973 @param remote_node: new secondary node to use (for use with
974 replace_new_secondary mode)
975 @type iallocator: str or None
976 @param iallocator: instance allocator plugin to use (for use with
977 replace_auto mode)
978
979 @rtype: string
980 @return: job id
981
982 """
983 query = [
984 ("mode", mode),
985 ]
986
987
988
989 if disks is not None:
990 query.append(("disks", ",".join(str(idx) for idx in disks)))
991
992 if remote_node is not None:
993 query.append(("remote_node", remote_node))
994
995 if iallocator is not None:
996 query.append(("iallocator", iallocator))
997
998 return self._SendRequest(HTTP_POST,
999 ("/%s/instances/%s/replace-disks" %
1000 (GANETI_RAPI_VERSION, instance)), query, None)
1001
1003 """Prepares an instance for an export.
1004
1005 @type instance: string
1006 @param instance: Instance name
1007 @type mode: string
1008 @param mode: Export mode
1009 @rtype: string
1010 @return: Job ID
1011
1012 """
1013 query = [("mode", mode)]
1014 return self._SendRequest(HTTP_PUT,
1015 ("/%s/instances/%s/prepare-export" %
1016 (GANETI_RAPI_VERSION, instance)), query, None)
1017
1018 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1019 remove_instance=None,
1020 x509_key_name=None, destination_x509_ca=None):
1021 """Exports an instance.
1022
1023 @type instance: string
1024 @param instance: Instance name
1025 @type mode: string
1026 @param mode: Export mode
1027 @rtype: string
1028 @return: Job ID
1029
1030 """
1031 body = {
1032 "destination": destination,
1033 "mode": mode,
1034 }
1035
1036 if shutdown is not None:
1037 body["shutdown"] = shutdown
1038
1039 if remove_instance is not None:
1040 body["remove_instance"] = remove_instance
1041
1042 if x509_key_name is not None:
1043 body["x509_key_name"] = x509_key_name
1044
1045 if destination_x509_ca is not None:
1046 body["destination_x509_ca"] = destination_x509_ca
1047
1048 return self._SendRequest(HTTP_PUT,
1049 ("/%s/instances/%s/export" %
1050 (GANETI_RAPI_VERSION, instance)), None, body)
1051
1053 """Migrates an instance.
1054
1055 @type instance: string
1056 @param instance: Instance name
1057 @type mode: string
1058 @param mode: Migration mode
1059 @type cleanup: bool
1060 @param cleanup: Whether to clean up a previously failed migration
1061 @rtype: string
1062 @return: job id
1063
1064 """
1065 body = {}
1066
1067 if mode is not None:
1068 body["mode"] = mode
1069
1070 if cleanup is not None:
1071 body["cleanup"] = cleanup
1072
1073 return self._SendRequest(HTTP_PUT,
1074 ("/%s/instances/%s/migrate" %
1075 (GANETI_RAPI_VERSION, instance)), None, body)
1076
1077 - def FailoverInstance(self, instance, iallocator=None,
1078 ignore_consistency=None, target_node=None):
1079 """Does a failover of an instance.
1080
1081 @type instance: string
1082 @param instance: Instance name
1083 @type iallocator: string
1084 @param iallocator: Iallocator for deciding the target node for
1085 shared-storage instances
1086 @type ignore_consistency: bool
1087 @param ignore_consistency: Whether to ignore disk consistency
1088 @type target_node: string
1089 @param target_node: Target node for shared-storage instances
1090 @rtype: string
1091 @return: job id
1092
1093 """
1094 body = {}
1095
1096 if iallocator is not None:
1097 body["iallocator"] = iallocator
1098
1099 if ignore_consistency is not None:
1100 body["ignore_consistency"] = ignore_consistency
1101
1102 if target_node is not None:
1103 body["target_node"] = target_node
1104
1105 return self._SendRequest(HTTP_PUT,
1106 ("/%s/instances/%s/failover" %
1107 (GANETI_RAPI_VERSION, instance)), None, body)
1108
1109 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1110 """Changes the name of an instance.
1111
1112 @type instance: string
1113 @param instance: Instance name
1114 @type new_name: string
1115 @param new_name: New instance name
1116 @type ip_check: bool
1117 @param ip_check: Whether to ensure instance's IP address is inactive
1118 @type name_check: bool
1119 @param name_check: Whether to ensure instance's name is resolvable
1120 @rtype: string
1121 @return: job id
1122
1123 """
1124 body = {
1125 "new_name": new_name,
1126 }
1127
1128 if ip_check is not None:
1129 body["ip_check"] = ip_check
1130
1131 if name_check is not None:
1132 body["name_check"] = name_check
1133
1134 return self._SendRequest(HTTP_PUT,
1135 ("/%s/instances/%s/rename" %
1136 (GANETI_RAPI_VERSION, instance)), None, body)
1137
1139 """Request information for connecting to instance's console.
1140
1141 @type instance: string
1142 @param instance: Instance name
1143 @rtype: dict
1144 @return: dictionary containing information about instance's console
1145
1146 """
1147 return self._SendRequest(HTTP_GET,
1148 ("/%s/instances/%s/console" %
1149 (GANETI_RAPI_VERSION, instance)), None, None)
1150
1152 """Gets all jobs for the cluster.
1153
1154 @rtype: list of int
1155 @return: job ids for the cluster
1156
1157 """
1158 return [int(j["id"])
1159 for j in self._SendRequest(HTTP_GET,
1160 "/%s/jobs" % GANETI_RAPI_VERSION,
1161 None, None)]
1162
1164 """Gets the status of a job.
1165
1166 @type job_id: string
1167 @param job_id: job id whose status to query
1168
1169 @rtype: dict
1170 @return: job status
1171
1172 """
1173 return self._SendRequest(HTTP_GET,
1174 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1175 None, None)
1176
1178 """Polls cluster for job status until completion.
1179
1180 Completion is defined as any of the following states listed in
1181 L{JOB_STATUS_FINALIZED}.
1182
1183 @type job_id: string
1184 @param job_id: job id to watch
1185 @type period: int
1186 @param period: how often to poll for status (optional, default 5s)
1187 @type retries: int
1188 @param retries: how many time to poll before giving up
1189 (optional, default -1 means unlimited)
1190
1191 @rtype: bool
1192 @return: C{True} if job succeeded or C{False} if failed/status timeout
1193 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1194 possible; L{WaitForJobChange} returns immediately after a job changed and
1195 does not use polling
1196
1197 """
1198 while retries != 0:
1199 job_result = self.GetJobStatus(job_id)
1200
1201 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1202 return True
1203 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1204 return False
1205
1206 if period:
1207 time.sleep(period)
1208
1209 if retries > 0:
1210 retries -= 1
1211
1212 return False
1213
1215 """Waits for job changes.
1216
1217 @type job_id: string
1218 @param job_id: Job ID for which to wait
1219 @return: C{None} if no changes have been detected and a dict with two keys,
1220 C{job_info} and C{log_entries} otherwise.
1221 @rtype: dict
1222
1223 """
1224 body = {
1225 "fields": fields,
1226 "previous_job_info": prev_job_info,
1227 "previous_log_serial": prev_log_serial,
1228 }
1229
1230 return self._SendRequest(HTTP_GET,
1231 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1232 None, body)
1233
1234 - def CancelJob(self, job_id, dry_run=False):
1235 """Cancels a job.
1236
1237 @type job_id: string
1238 @param job_id: id of the job to delete
1239 @type dry_run: bool
1240 @param dry_run: whether to perform a dry run
1241 @rtype: tuple
1242 @return: tuple containing the result, and a message (bool, string)
1243
1244 """
1245 query = []
1246 if dry_run:
1247 query.append(("dry-run", 1))
1248
1249 return self._SendRequest(HTTP_DELETE,
1250 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1251 query, None)
1252
1254 """Gets all nodes in the cluster.
1255
1256 @type bulk: bool
1257 @param bulk: whether to return all information about all instances
1258
1259 @rtype: list of dict or str
1260 @return: if bulk is true, info about nodes in the cluster,
1261 else list of nodes in the cluster
1262
1263 """
1264 query = []
1265 if bulk:
1266 query.append(("bulk", 1))
1267
1268 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1269 query, None)
1270 if bulk:
1271 return nodes
1272 else:
1273 return [n["id"] for n in nodes]
1274
1276 """Gets information about a node.
1277
1278 @type node: str
1279 @param node: node whose info to return
1280
1281 @rtype: dict
1282 @return: info about the node
1283
1284 """
1285 return self._SendRequest(HTTP_GET,
1286 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1287 None, None)
1288
1289 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1290 dry_run=False, early_release=None,
1291 mode=None, accept_old=False):
1292 """Evacuates instances from a Ganeti node.
1293
1294 @type node: str
1295 @param node: node to evacuate
1296 @type iallocator: str or None
1297 @param iallocator: instance allocator to use
1298 @type remote_node: str
1299 @param remote_node: node to evaucate to
1300 @type dry_run: bool
1301 @param dry_run: whether to perform a dry run
1302 @type early_release: bool
1303 @param early_release: whether to enable parallelization
1304 @type mode: string
1305 @param mode: Node evacuation mode
1306 @type accept_old: bool
1307 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1308 results
1309
1310 @rtype: string, or a list for pre-2.5 results
1311 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1312 list of (job ID, instance name, new secondary node); if dry_run was
1313 specified, then the actual move jobs were not submitted and the job IDs
1314 will be C{None}
1315
1316 @raises GanetiApiError: if an iallocator and remote_node are both
1317 specified
1318
1319 """
1320 if iallocator and remote_node:
1321 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1322
1323 query = []
1324 if dry_run:
1325 query.append(("dry-run", 1))
1326
1327 if _NODE_EVAC_RES1 in self.GetFeatures():
1328
1329 body = {}
1330
1331 if iallocator is not None:
1332 body["iallocator"] = iallocator
1333 if remote_node is not None:
1334 body["remote_node"] = remote_node
1335 if early_release is not None:
1336 body["early_release"] = early_release
1337 if mode is not None:
1338 body["mode"] = mode
1339 else:
1340
1341 body = None
1342
1343 if not accept_old:
1344 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1345 " not accept old-style results (parameter"
1346 " accept_old)")
1347
1348
1349 if mode is not None and mode != NODE_EVAC_SEC:
1350 raise GanetiApiError("Server can only evacuate secondary instances")
1351
1352 if iallocator:
1353 query.append(("iallocator", iallocator))
1354 if remote_node:
1355 query.append(("remote_node", remote_node))
1356 if early_release:
1357 query.append(("early_release", 1))
1358
1359 return self._SendRequest(HTTP_POST,
1360 ("/%s/nodes/%s/evacuate" %
1361 (GANETI_RAPI_VERSION, node)), query, body)
1362
1363 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1364 target_node=None):
1365 """Migrates all primary instances from a node.
1366
1367 @type node: str
1368 @param node: node to migrate
1369 @type mode: string
1370 @param mode: if passed, it will overwrite the live migration type,
1371 otherwise the hypervisor default will be used
1372 @type dry_run: bool
1373 @param dry_run: whether to perform a dry run
1374 @type iallocator: string
1375 @param iallocator: instance allocator to use
1376 @type target_node: string
1377 @param target_node: Target node for shared-storage instances
1378
1379 @rtype: string
1380 @return: job id
1381
1382 """
1383 query = []
1384 if dry_run:
1385 query.append(("dry-run", 1))
1386
1387 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1388 body = {}
1389
1390 if mode is not None:
1391 body["mode"] = mode
1392 if iallocator is not None:
1393 body["iallocator"] = iallocator
1394 if target_node is not None:
1395 body["target_node"] = target_node
1396
1397 assert len(query) <= 1
1398
1399 return self._SendRequest(HTTP_POST,
1400 ("/%s/nodes/%s/migrate" %
1401 (GANETI_RAPI_VERSION, node)), query, body)
1402 else:
1403
1404 if target_node is not None:
1405 raise GanetiApiError("Server does not support specifying target node"
1406 " for node migration")
1407
1408 if mode is not None:
1409 query.append(("mode", mode))
1410
1411 return self._SendRequest(HTTP_POST,
1412 ("/%s/nodes/%s/migrate" %
1413 (GANETI_RAPI_VERSION, node)), query, None)
1414
1416 """Gets the current role for a node.
1417
1418 @type node: str
1419 @param node: node whose role to return
1420
1421 @rtype: str
1422 @return: the current role for a node
1423
1424 """
1425 return self._SendRequest(HTTP_GET,
1426 ("/%s/nodes/%s/role" %
1427 (GANETI_RAPI_VERSION, node)), None, None)
1428
1430 """Sets the role for a node.
1431
1432 @type node: str
1433 @param node: the node whose role to set
1434 @type role: str
1435 @param role: the role to set for the node
1436 @type force: bool
1437 @param force: whether to force the role change
1438
1439 @rtype: string
1440 @return: job id
1441
1442 """
1443 query = [
1444 ("force", force),
1445 ]
1446
1447 return self._SendRequest(HTTP_PUT,
1448 ("/%s/nodes/%s/role" %
1449 (GANETI_RAPI_VERSION, node)), query, role)
1450
1452 """Gets the storage units for a node.
1453
1454 @type node: str
1455 @param node: the node whose storage units to return
1456 @type storage_type: str
1457 @param storage_type: storage type whose units to return
1458 @type output_fields: str
1459 @param output_fields: storage type fields to return
1460
1461 @rtype: string
1462 @return: job id where results can be retrieved
1463
1464 """
1465 query = [
1466 ("storage_type", storage_type),
1467 ("output_fields", output_fields),
1468 ]
1469
1470 return self._SendRequest(HTTP_GET,
1471 ("/%s/nodes/%s/storage" %
1472 (GANETI_RAPI_VERSION, node)), query, None)
1473
1475 """Modifies parameters of storage units on the node.
1476
1477 @type node: str
1478 @param node: node whose storage units to modify
1479 @type storage_type: str
1480 @param storage_type: storage type whose units to modify
1481 @type name: str
1482 @param name: name of the storage unit
1483 @type allocatable: bool or None
1484 @param allocatable: Whether to set the "allocatable" flag on the storage
1485 unit (None=no modification, True=set, False=unset)
1486
1487 @rtype: string
1488 @return: job id
1489
1490 """
1491 query = [
1492 ("storage_type", storage_type),
1493 ("name", name),
1494 ]
1495
1496 if allocatable is not None:
1497 query.append(("allocatable", allocatable))
1498
1499 return self._SendRequest(HTTP_PUT,
1500 ("/%s/nodes/%s/storage/modify" %
1501 (GANETI_RAPI_VERSION, node)), query, None)
1502
1504 """Repairs a storage unit on the node.
1505
1506 @type node: str
1507 @param node: node whose storage units to repair
1508 @type storage_type: str
1509 @param storage_type: storage type to repair
1510 @type name: str
1511 @param name: name of the storage unit to repair
1512
1513 @rtype: string
1514 @return: job id
1515
1516 """
1517 query = [
1518 ("storage_type", storage_type),
1519 ("name", name),
1520 ]
1521
1522 return self._SendRequest(HTTP_PUT,
1523 ("/%s/nodes/%s/storage/repair" %
1524 (GANETI_RAPI_VERSION, node)), query, None)
1525
1539
1561
1583
1585 """Gets all node groups in the cluster.
1586
1587 @type bulk: bool
1588 @param bulk: whether to return all information about the groups
1589
1590 @rtype: list of dict or str
1591 @return: if bulk is true, a list of dictionaries with info about all node
1592 groups in the cluster, else a list of names of those node groups
1593
1594 """
1595 query = []
1596 if bulk:
1597 query.append(("bulk", 1))
1598
1599 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1600 query, None)
1601 if bulk:
1602 return groups
1603 else:
1604 return [g["name"] for g in groups]
1605
1607 """Gets information about a node group.
1608
1609 @type group: str
1610 @param group: name of the node group whose info to return
1611
1612 @rtype: dict
1613 @return: info about the node group
1614
1615 """
1616 return self._SendRequest(HTTP_GET,
1617 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1618 None, None)
1619
1620 - def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1621 """Creates a new node group.
1622
1623 @type name: str
1624 @param name: the name of node group to create
1625 @type alloc_policy: str
1626 @param alloc_policy: the desired allocation policy for the group, if any
1627 @type dry_run: bool
1628 @param dry_run: whether to peform a dry run
1629
1630 @rtype: string
1631 @return: job id
1632
1633 """
1634 query = []
1635 if dry_run:
1636 query.append(("dry-run", 1))
1637
1638 body = {
1639 "name": name,
1640 "alloc_policy": alloc_policy
1641 }
1642
1643 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1644 query, body)
1645
1647 """Modifies a node group.
1648
1649 More details for parameters can be found in the RAPI documentation.
1650
1651 @type group: string
1652 @param group: Node group name
1653 @rtype: string
1654 @return: job id
1655
1656 """
1657 return self._SendRequest(HTTP_PUT,
1658 ("/%s/groups/%s/modify" %
1659 (GANETI_RAPI_VERSION, group)), None, kwargs)
1660
1662 """Deletes a node group.
1663
1664 @type group: str
1665 @param group: the node group to delete
1666 @type dry_run: bool
1667 @param dry_run: whether to peform a dry run
1668
1669 @rtype: string
1670 @return: job id
1671
1672 """
1673 query = []
1674 if dry_run:
1675 query.append(("dry-run", 1))
1676
1677 return self._SendRequest(HTTP_DELETE,
1678 ("/%s/groups/%s" %
1679 (GANETI_RAPI_VERSION, group)), query, None)
1680
1682 """Changes the name of a node group.
1683
1684 @type group: string
1685 @param group: Node group name
1686 @type new_name: string
1687 @param new_name: New node group name
1688
1689 @rtype: string
1690 @return: job id
1691
1692 """
1693 body = {
1694 "new_name": new_name,
1695 }
1696
1697 return self._SendRequest(HTTP_PUT,
1698 ("/%s/groups/%s/rename" %
1699 (GANETI_RAPI_VERSION, group)), None, body)
1700
1702 """Assigns nodes to a group.
1703
1704 @type group: string
1705 @param group: Node gropu name
1706 @type nodes: list of strings
1707 @param nodes: List of nodes to assign to the group
1708
1709 @rtype: string
1710 @return: job id
1711
1712 """
1713 query = []
1714
1715 if force:
1716 query.append(("force", 1))
1717
1718 if dry_run:
1719 query.append(("dry-run", 1))
1720
1721 body = {
1722 "nodes": nodes,
1723 }
1724
1725 return self._SendRequest(HTTP_PUT,
1726 ("/%s/groups/%s/assign-nodes" %
1727 (GANETI_RAPI_VERSION, group)), query, body)
1728
1742
1764
1785
1786 - def Query(self, what, fields, filter_=None):
1787 """Retrieves information about resources.
1788
1789 @type what: string
1790 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1791 @type fields: list of string
1792 @param fields: Requested fields
1793 @type filter_: None or list
1794 @param filter_: Query filter
1795
1796 @rtype: string
1797 @return: job id
1798
1799 """
1800 body = {
1801 "fields": fields,
1802 }
1803
1804 if filter_ is not None:
1805 body["filter"] = filter_
1806
1807 return self._SendRequest(HTTP_PUT,
1808 ("/%s/query/%s" %
1809 (GANETI_RAPI_VERSION, what)), None, body)
1810
1812 """Retrieves available fields for a resource.
1813
1814 @type what: string
1815 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1816 @type fields: list of string
1817 @param fields: Requested fields
1818
1819 @rtype: string
1820 @return: job id
1821
1822 """
1823 query = []
1824
1825 if fields is not None:
1826 query.append(("fields", ",".join(fields)))
1827
1828 return self._SendRequest(HTTP_GET,
1829 ("/%s/query/%s/fields" %
1830 (GANETI_RAPI_VERSION, what)), query, None)
1831