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_PENDING = frozenset([
84 JOB_STATUS_QUEUED,
85 JOB_STATUS_WAITING,
86 JOB_STATUS_CANCELING,
87 ])
88 JOB_STATUS_FINALIZED = frozenset([
89 JOB_STATUS_CANCELED,
90 JOB_STATUS_SUCCESS,
91 JOB_STATUS_ERROR,
92 ])
93 JOB_STATUS_ALL = frozenset([
94 JOB_STATUS_RUNNING,
95 ]) | JOB_STATUS_PENDING | JOB_STATUS_FINALIZED
96
97
98 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
99
100
101 _REQ_DATA_VERSION_FIELD = "__version__"
102 _QPARAM_DRY_RUN = "dry-run"
103 _QPARAM_FORCE = "force"
104
105
106 INST_CREATE_REQV1 = "instance-create-reqv1"
107 INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
108 NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
109 NODE_EVAC_RES1 = "node-evac-res1"
110
111
112 _INST_CREATE_REQV1 = INST_CREATE_REQV1
113 _INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
114 _NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
115 _NODE_EVAC_RES1 = NODE_EVAC_RES1
116
117
118 ECODE_RESOLVER = "resolver_error"
119
120
121 ECODE_NORES = "insufficient_resources"
122
123
124 ECODE_TEMP_NORES = "temp_insufficient_resources"
125
126
127 ECODE_INVAL = "wrong_input"
128
129
130 ECODE_STATE = "wrong_state"
131
132
133 ECODE_NOENT = "unknown_entity"
134
135
136 ECODE_EXISTS = "already_exists"
137
138
139 ECODE_NOTUNIQUE = "resource_not_unique"
140
141
142 ECODE_FAULT = "internal_error"
143
144
145 ECODE_ENVIRON = "environment_error"
146
147
148 ECODE_ALL = frozenset([
149 ECODE_RESOLVER,
150 ECODE_NORES,
151 ECODE_TEMP_NORES,
152 ECODE_INVAL,
153 ECODE_STATE,
154 ECODE_NOENT,
155 ECODE_EXISTS,
156 ECODE_NOTUNIQUE,
157 ECODE_FAULT,
158 ECODE_ENVIRON,
159 ])
160
161
162 try:
163 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
164 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
165 except AttributeError:
166 _CURLE_SSL_CACERT = 60
167 _CURLE_SSL_CACERT_BADFILE = 77
168
169 _CURL_SSL_CERT_ERRORS = frozenset([
170 _CURLE_SSL_CACERT,
171 _CURLE_SSL_CACERT_BADFILE,
172 ])
173
174
175 -class Error(Exception):
176 """Base error class for this module.
177
178 """
179 pass
180
183 """Generic error raised from Ganeti API.
184
185 """
189
192 """Raised when a problem is found with the SSL certificate.
193
194 """
195 pass
196
197
198 -def _AppendIf(container, condition, value):
199 """Appends to a list if a condition evaluates to truth.
200
201 """
202 if condition:
203 container.append(value)
204
205 return condition
206
209 """Appends a "dry-run" parameter if a condition evaluates to truth.
210
211 """
212 return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
213
216 """Appends a "force" parameter if a condition evaluates to truth.
217
218 """
219 return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
220
223 """Appends an element to the reason trail.
224
225 If the user provided a reason, it is added to the reason trail.
226
227 """
228 return _AppendIf(container, reason, ("reason", reason))
229
230
231 -def _SetItemIf(container, condition, item, value):
232 """Sets an item if a condition evaluates to truth.
233
234 """
235 if condition:
236 container[item] = value
237
238 return condition
239
242 """Decorator for code using RAPI client to initialize pycURL.
243
244 """
245 def wrapper(*args, **kwargs):
246
247
248
249 assert threading.activeCount() == 1, \
250 "Found active threads when initializing pycURL"
251
252 pycurl.global_init(pycurl.GLOBAL_ALL)
253 try:
254 return fn(*args, **kwargs)
255 finally:
256 pycurl.global_cleanup()
257
258 return wrapper
259
260
261 -def GenericCurlConfig(verbose=False, use_signal=False,
262 use_curl_cabundle=False, cafile=None, capath=None,
263 proxy=None, verify_hostname=False,
264 connect_timeout=None, timeout=None,
265 _pycurl_version_fn=pycurl.version_info):
266 """Curl configuration function generator.
267
268 @type verbose: bool
269 @param verbose: Whether to set cURL to verbose mode
270 @type use_signal: bool
271 @param use_signal: Whether to allow cURL to use signals
272 @type use_curl_cabundle: bool
273 @param use_curl_cabundle: Whether to use cURL's default CA bundle
274 @type cafile: string
275 @param cafile: In which file we can find the certificates
276 @type capath: string
277 @param capath: In which directory we can find the certificates
278 @type proxy: string
279 @param proxy: Proxy to use, None for default behaviour and empty string for
280 disabling proxies (see curl_easy_setopt(3))
281 @type verify_hostname: bool
282 @param verify_hostname: Whether to verify the remote peer certificate's
283 commonName
284 @type connect_timeout: number
285 @param connect_timeout: Timeout for establishing connection in seconds
286 @type timeout: number
287 @param timeout: Timeout for complete transfer in seconds (see
288 curl_easy_setopt(3)).
289
290 """
291 if use_curl_cabundle and (cafile or capath):
292 raise Error("Can not use default CA bundle when CA file or path is set")
293
294 def _ConfigCurl(curl, logger):
295 """Configures a cURL object
296
297 @type curl: pycurl.Curl
298 @param curl: cURL object
299
300 """
301 logger.debug("Using cURL version %s", pycurl.version)
302
303
304
305
306
307 sslver = _pycurl_version_fn()[5]
308 if not sslver:
309 raise Error("No SSL support in cURL")
310
311 lcsslver = sslver.lower()
312 if lcsslver.startswith("openssl/"):
313 pass
314 elif lcsslver.startswith("nss/"):
315
316 pass
317 elif lcsslver.startswith("gnutls/"):
318 if capath:
319 raise Error("cURL linked against GnuTLS has no support for a"
320 " CA path (%s)" % (pycurl.version, ))
321 else:
322 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
323 sslver)
324
325 curl.setopt(pycurl.VERBOSE, verbose)
326 curl.setopt(pycurl.NOSIGNAL, not use_signal)
327
328
329 if verify_hostname:
330
331
332
333
334
335 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
336 else:
337 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
338
339 if cafile or capath or use_curl_cabundle:
340
341 curl.setopt(pycurl.SSL_VERIFYPEER, True)
342 if cafile:
343 curl.setopt(pycurl.CAINFO, str(cafile))
344 if capath:
345 curl.setopt(pycurl.CAPATH, str(capath))
346
347 else:
348
349 curl.setopt(pycurl.SSL_VERIFYPEER, False)
350
351 if proxy is not None:
352 curl.setopt(pycurl.PROXY, str(proxy))
353
354
355 if connect_timeout is not None:
356 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
357 if timeout is not None:
358 curl.setopt(pycurl.TIMEOUT, timeout)
359
360 return _ConfigCurl
361
364 """Ganeti RAPI client.
365
366 """
367 USER_AGENT = "Ganeti RAPI Client"
368 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
369
370 - def __init__(self, host, port=GANETI_RAPI_PORT,
371 username=None, password=None, logger=logging,
372 curl_config_fn=None, curl_factory=None):
373 """Initializes this class.
374
375 @type host: string
376 @param host: the ganeti cluster master to interact with
377 @type port: int
378 @param port: the port on which the RAPI is running (default is 5080)
379 @type username: string
380 @param username: the username to connect with
381 @type password: string
382 @param password: the password to connect with
383 @type curl_config_fn: callable
384 @param curl_config_fn: Function to configure C{pycurl.Curl} object
385 @param logger: Logging object
386
387 """
388 self._username = username
389 self._password = password
390 self._logger = logger
391 self._curl_config_fn = curl_config_fn
392 self._curl_factory = curl_factory
393
394 try:
395 socket.inet_pton(socket.AF_INET6, host)
396 address = "[%s]:%s" % (host, port)
397 except socket.error:
398 address = "%s:%s" % (host, port)
399
400 self._base_url = "https://%s" % address
401
402 if username is not None:
403 if password is None:
404 raise Error("Password not specified")
405 elif password:
406 raise Error("Specified password without username")
407
409 """Creates a cURL object.
410
411 """
412
413 if self._curl_factory:
414 curl = self._curl_factory()
415 else:
416 curl = pycurl.Curl()
417
418
419 curl.setopt(pycurl.VERBOSE, False)
420 curl.setopt(pycurl.FOLLOWLOCATION, False)
421 curl.setopt(pycurl.MAXREDIRS, 5)
422 curl.setopt(pycurl.NOSIGNAL, True)
423 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
424 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
425 curl.setopt(pycurl.SSL_VERIFYPEER, False)
426 curl.setopt(pycurl.HTTPHEADER, [
427 "Accept: %s" % HTTP_APP_JSON,
428 "Content-type: %s" % HTTP_APP_JSON,
429 ])
430
431 assert ((self._username is None and self._password is None) ^
432 (self._username is not None and self._password is not None))
433
434 if self._username:
435
436 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
437 curl.setopt(pycurl.USERPWD,
438 str("%s:%s" % (self._username, self._password)))
439
440
441 if self._curl_config_fn:
442 self._curl_config_fn(curl, self._logger)
443
444 return curl
445
446 @staticmethod
448 """Encode query values for RAPI URL.
449
450 @type query: list of two-tuples
451 @param query: Query arguments
452 @rtype: list
453 @return: Query list with encoded values
454
455 """
456 result = []
457
458 for name, value in query:
459 if value is None:
460 result.append((name, ""))
461
462 elif isinstance(value, bool):
463
464 result.append((name, int(value)))
465
466 elif isinstance(value, (list, tuple, dict)):
467 raise ValueError("Invalid query data type %r" % type(value).__name__)
468
469 else:
470 result.append((name, value))
471
472 return result
473
475 """Sends an HTTP request.
476
477 This constructs a full URL, encodes and decodes HTTP bodies, and
478 handles invalid responses in a pythonic way.
479
480 @type method: string
481 @param method: HTTP method to use
482 @type path: string
483 @param path: HTTP URL path
484 @type query: list of two-tuples
485 @param query: query arguments to pass to urllib.urlencode
486 @type content: str or None
487 @param content: HTTP body content
488
489 @rtype: str
490 @return: JSON-Decoded response
491
492 @raises CertificateError: If an invalid SSL certificate is found
493 @raises GanetiApiError: If an invalid response is returned
494
495 """
496 assert path.startswith("/")
497
498 curl = self._CreateCurl()
499
500 if content is not None:
501 encoded_content = self._json_encoder.encode(content)
502 else:
503 encoded_content = ""
504
505
506 urlparts = [self._base_url, path]
507 if query:
508 urlparts.append("?")
509 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
510
511 url = "".join(urlparts)
512
513 self._logger.debug("Sending request %s %s (content=%r)",
514 method, url, encoded_content)
515
516
517 encoded_resp_body = StringIO()
518
519
520 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
521 curl.setopt(pycurl.URL, str(url))
522 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
523 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
524
525 try:
526
527 try:
528 curl.perform()
529 except pycurl.error, err:
530 if err.args[0] in _CURL_SSL_CERT_ERRORS:
531 raise CertificateError("SSL certificate error %s" % err,
532 code=err.args[0])
533
534 raise GanetiApiError(str(err), code=err.args[0])
535 finally:
536
537
538 curl.setopt(pycurl.POSTFIELDS, "")
539 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
540
541
542 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
543
544
545 if encoded_resp_body.tell():
546 response_content = simplejson.loads(encoded_resp_body.getvalue())
547 else:
548 response_content = None
549
550 if http_code != HTTP_OK:
551 if isinstance(response_content, dict):
552 msg = ("%s %s: %s" %
553 (response_content["code"],
554 response_content["message"],
555 response_content["explain"]))
556 else:
557 msg = str(response_content)
558
559 raise GanetiApiError(msg, code=http_code)
560
561 return response_content
562
564 """Gets the Remote API version running on the cluster.
565
566 @rtype: int
567 @return: Ganeti Remote API version
568
569 """
570 return self._SendRequest(HTTP_GET, "/version", None, None)
571
588
590 """Gets the Operating Systems running in the Ganeti cluster.
591
592 @rtype: list of str
593 @return: operating systems
594 @type reason: string
595 @param reason: the reason for executing this operation
596
597 """
598 query = []
599 _AppendReason(query, reason)
600 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
601 query, None)
602
616
618 """Tells the cluster to redistribute its configuration files.
619
620 @type reason: string
621 @param reason: the reason for executing this operation
622 @rtype: string
623 @return: job id
624
625 """
626 query = []
627 _AppendReason(query, reason)
628 return self._SendRequest(HTTP_PUT,
629 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
630 query, None)
631
633 """Modifies cluster parameters.
634
635 More details for parameters can be found in the RAPI documentation.
636
637 @type reason: string
638 @param reason: the reason for executing this operation
639 @rtype: string
640 @return: job id
641
642 """
643 query = []
644 _AppendReason(query, reason)
645
646 body = kwargs
647
648 return self._SendRequest(HTTP_PUT,
649 "/%s/modify" % GANETI_RAPI_VERSION, query, body)
650
664
685
705
707 """Gets information about instances on the cluster.
708
709 @type bulk: bool
710 @param bulk: whether to return all information about all instances
711 @type reason: string
712 @param reason: the reason for executing this operation
713
714 @rtype: list of dict or list of str
715 @return: if bulk is True, info about the instances, else a list of instances
716
717 """
718 query = []
719 _AppendIf(query, bulk, ("bulk", 1))
720 _AppendReason(query, reason)
721
722 instances = self._SendRequest(HTTP_GET,
723 "/%s/instances" % GANETI_RAPI_VERSION,
724 query, None)
725 if bulk:
726 return instances
727 else:
728 return [i["id"] for i in instances]
729
731 """Gets information about an instance.
732
733 @type instance: str
734 @param instance: instance whose info to return
735 @type reason: string
736 @param reason: the reason for executing this operation
737
738 @rtype: dict
739 @return: info about the instance
740
741 """
742 query = []
743 _AppendReason(query, reason)
744
745 return self._SendRequest(HTTP_GET,
746 ("/%s/instances/%s" %
747 (GANETI_RAPI_VERSION, instance)), query, None)
748
750 """Gets information about an instance.
751
752 @type instance: string
753 @param instance: Instance name
754 @type reason: string
755 @param reason: the reason for executing this operation
756 @rtype: string
757 @return: Job ID
758
759 """
760 query = []
761 if static is not None:
762 query.append(("static", static))
763 _AppendReason(query, reason)
764
765 return self._SendRequest(HTTP_GET,
766 ("/%s/instances/%s/info" %
767 (GANETI_RAPI_VERSION, instance)), query, None)
768
769 @staticmethod
771 """Updates the base with params from kwargs.
772
773 @param base: The base dict, filled with required fields
774
775 @note: This is an inplace update of base
776
777 """
778 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
779 if conflicts:
780 raise GanetiApiError("Required fields can not be specified as"
781 " keywords: %s" % ", ".join(conflicts))
782
783 base.update((key, value) for key, value in kwargs.iteritems()
784 if key != "dry_run")
785
788 """Generates an instance allocation as used by multiallocate.
789
790 More details for parameters can be found in the RAPI documentation.
791 It is the same as used by CreateInstance.
792
793 @type mode: string
794 @param mode: Instance creation mode
795 @type name: string
796 @param name: Hostname of the instance to create
797 @type disk_template: string
798 @param disk_template: Disk template for instance (e.g. plain, diskless,
799 file, or drbd)
800 @type disks: list of dicts
801 @param disks: List of disk definitions
802 @type nics: list of dicts
803 @param nics: List of NIC definitions
804
805 @return: A dict with the generated entry
806
807 """
808
809 alloc = {
810 "mode": mode,
811 "name": name,
812 "disk_template": disk_template,
813 "disks": disks,
814 "nics": nics,
815 }
816
817 self._UpdateWithKwargs(alloc, **kwargs)
818
819 return alloc
820
841
842 - def CreateInstance(self, mode, name, disk_template, disks, nics,
843 reason=None, **kwargs):
844 """Creates a new instance.
845
846 More details for parameters can be found in the RAPI documentation.
847
848 @type mode: string
849 @param mode: Instance creation mode
850 @type name: string
851 @param name: Hostname of the instance to create
852 @type disk_template: string
853 @param disk_template: Disk template for instance (e.g. plain, diskless,
854 file, or drbd)
855 @type disks: list of dicts
856 @param disks: List of disk definitions
857 @type nics: list of dicts
858 @param nics: List of NIC definitions
859 @type dry_run: bool
860 @keyword dry_run: whether to perform a dry run
861 @type reason: string
862 @param reason: the reason for executing this operation
863
864 @rtype: string
865 @return: job id
866
867 """
868 query = []
869
870 _AppendDryRunIf(query, kwargs.get("dry_run"))
871 _AppendReason(query, reason)
872
873 if _INST_CREATE_REQV1 in self.GetFeatures():
874 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
875 **kwargs)
876 body[_REQ_DATA_VERSION_FIELD] = 1
877 else:
878 raise GanetiApiError("Server does not support new-style (version 1)"
879 " instance creation requests")
880
881 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
882 query, body)
883
903
905 """Modifies an instance.
906
907 More details for parameters can be found in the RAPI documentation.
908
909 @type instance: string
910 @param instance: Instance name
911 @type reason: string
912 @param reason: the reason for executing this operation
913 @rtype: string
914 @return: job id
915
916 """
917 body = kwargs
918 query = []
919 _AppendReason(query, reason)
920
921 return self._SendRequest(HTTP_PUT,
922 ("/%s/instances/%s/modify" %
923 (GANETI_RAPI_VERSION, instance)), query, body)
924
926 """Activates an instance's disks.
927
928 @type instance: string
929 @param instance: Instance name
930 @type ignore_size: bool
931 @param ignore_size: Whether to ignore recorded size
932 @type reason: string
933 @param reason: the reason for executing this operation
934 @rtype: string
935 @return: job id
936
937 """
938 query = []
939 _AppendIf(query, ignore_size, ("ignore_size", 1))
940 _AppendReason(query, reason)
941
942 return self._SendRequest(HTTP_PUT,
943 ("/%s/instances/%s/activate-disks" %
944 (GANETI_RAPI_VERSION, instance)), query, None)
945
947 """Deactivates an instance's disks.
948
949 @type instance: string
950 @param instance: Instance name
951 @type reason: string
952 @param reason: the reason for executing this operation
953 @rtype: string
954 @return: job id
955
956 """
957 query = []
958 _AppendReason(query, reason)
959 return self._SendRequest(HTTP_PUT,
960 ("/%s/instances/%s/deactivate-disks" %
961 (GANETI_RAPI_VERSION, instance)), query, None)
962
965 """Recreate an instance's disks.
966
967 @type instance: string
968 @param instance: Instance name
969 @type disks: list of int
970 @param disks: List of disk indexes
971 @type nodes: list of string
972 @param nodes: New instance nodes, if relocation is desired
973 @type reason: string
974 @param reason: the reason for executing this operation
975 @rtype: string
976 @return: job id
977
978 """
979 body = {}
980 _SetItemIf(body, disks is not None, "disks", disks)
981 _SetItemIf(body, nodes is not None, "nodes", nodes)
982
983 query = []
984 _AppendReason(query, reason)
985
986 return self._SendRequest(HTTP_POST,
987 ("/%s/instances/%s/recreate-disks" %
988 (GANETI_RAPI_VERSION, instance)), query, body)
989
990 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
991 reason=None):
992 """Grows a disk of an instance.
993
994 More details for parameters can be found in the RAPI documentation.
995
996 @type instance: string
997 @param instance: Instance name
998 @type disk: integer
999 @param disk: Disk index
1000 @type amount: integer
1001 @param amount: Grow disk by this amount (MiB)
1002 @type wait_for_sync: bool
1003 @param wait_for_sync: Wait for disk to synchronize
1004 @type reason: string
1005 @param reason: the reason for executing this operation
1006 @rtype: string
1007 @return: job id
1008
1009 """
1010 body = {
1011 "amount": amount,
1012 }
1013
1014 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1015
1016 query = []
1017 _AppendReason(query, reason)
1018
1019 return self._SendRequest(HTTP_POST,
1020 ("/%s/instances/%s/disk/%s/grow" %
1021 (GANETI_RAPI_VERSION, instance, disk)),
1022 query, body)
1023
1041
1065
1088
1089 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1090 dry_run=False, reason=None):
1091 """Reboots an instance.
1092
1093 @type instance: str
1094 @param instance: instance to reboot
1095 @type reboot_type: str
1096 @param reboot_type: one of: hard, soft, full
1097 @type ignore_secondaries: bool
1098 @param ignore_secondaries: if True, ignores errors for the secondary node
1099 while re-assembling disks (in hard-reboot mode only)
1100 @type dry_run: bool
1101 @param dry_run: whether to perform a dry run
1102 @type reason: string
1103 @param reason: the reason for the reboot
1104 @rtype: string
1105 @return: job id
1106
1107 """
1108 query = []
1109 _AppendDryRunIf(query, dry_run)
1110 _AppendIf(query, reboot_type, ("type", reboot_type))
1111 _AppendIf(query, ignore_secondaries is not None,
1112 ("ignore_secondaries", ignore_secondaries))
1113 _AppendReason(query, reason)
1114
1115 return self._SendRequest(HTTP_POST,
1116 ("/%s/instances/%s/reboot" %
1117 (GANETI_RAPI_VERSION, instance)), query, None)
1118
1119 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1120 reason=None, **kwargs):
1121 """Shuts down an instance.
1122
1123 @type instance: str
1124 @param instance: the instance to shut down
1125 @type dry_run: bool
1126 @param dry_run: whether to perform a dry run
1127 @type no_remember: bool
1128 @param no_remember: if true, will not record the state change
1129 @type reason: string
1130 @param reason: the reason for the shutdown
1131 @rtype: string
1132 @return: job id
1133
1134 """
1135 query = []
1136 body = kwargs
1137
1138 _AppendDryRunIf(query, dry_run)
1139 _AppendIf(query, no_remember, ("no_remember", 1))
1140 _AppendReason(query, reason)
1141
1142 return self._SendRequest(HTTP_PUT,
1143 ("/%s/instances/%s/shutdown" %
1144 (GANETI_RAPI_VERSION, instance)), query, body)
1145
1146 - def StartupInstance(self, instance, dry_run=False, no_remember=False,
1147 reason=None):
1148 """Starts up an instance.
1149
1150 @type instance: str
1151 @param instance: the instance to start up
1152 @type dry_run: bool
1153 @param dry_run: whether to perform a dry run
1154 @type no_remember: bool
1155 @param no_remember: if true, will not record the state change
1156 @type reason: string
1157 @param reason: the reason for the startup
1158 @rtype: string
1159 @return: job id
1160
1161 """
1162 query = []
1163 _AppendDryRunIf(query, dry_run)
1164 _AppendIf(query, no_remember, ("no_remember", 1))
1165 _AppendReason(query, reason)
1166
1167 return self._SendRequest(HTTP_PUT,
1168 ("/%s/instances/%s/startup" %
1169 (GANETI_RAPI_VERSION, instance)), query, None)
1170
1171 - def ReinstallInstance(self, instance, os=None, no_startup=False,
1172 osparams=None, reason=None):
1173 """Reinstalls an instance.
1174
1175 @type instance: str
1176 @param instance: The instance to reinstall
1177 @type os: str or None
1178 @param os: The operating system to reinstall. If None, the instance's
1179 current operating system will be installed again
1180 @type no_startup: bool
1181 @param no_startup: Whether to start the instance automatically
1182 @type reason: string
1183 @param reason: the reason for executing this operation
1184 @rtype: string
1185 @return: job id
1186
1187 """
1188 query = []
1189 _AppendReason(query, reason)
1190
1191 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1192 body = {
1193 "start": not no_startup,
1194 }
1195 _SetItemIf(body, os is not None, "os", os)
1196 _SetItemIf(body, osparams is not None, "osparams", osparams)
1197 return self._SendRequest(HTTP_POST,
1198 ("/%s/instances/%s/reinstall" %
1199 (GANETI_RAPI_VERSION, instance)), query, body)
1200
1201
1202 if osparams:
1203 raise GanetiApiError("Server does not support specifying OS parameters"
1204 " for instance reinstallation")
1205
1206 query = []
1207 _AppendIf(query, os, ("os", os))
1208 _AppendIf(query, no_startup, ("nostartup", 1))
1209
1210 return self._SendRequest(HTTP_POST,
1211 ("/%s/instances/%s/reinstall" %
1212 (GANETI_RAPI_VERSION, instance)), query, None)
1213
1216 """Replaces disks on an instance.
1217
1218 @type instance: str
1219 @param instance: instance whose disks to replace
1220 @type disks: list of ints
1221 @param disks: Indexes of disks to replace
1222 @type mode: str
1223 @param mode: replacement mode to use (defaults to replace_auto)
1224 @type remote_node: str or None
1225 @param remote_node: new secondary node to use (for use with
1226 replace_new_secondary mode)
1227 @type iallocator: str or None
1228 @param iallocator: instance allocator plugin to use (for use with
1229 replace_auto mode)
1230 @type reason: string
1231 @param reason: the reason for executing this operation
1232
1233 @rtype: string
1234 @return: job id
1235
1236 """
1237 query = [
1238 ("mode", mode),
1239 ]
1240
1241
1242
1243 if disks is not None:
1244 _AppendIf(query, True,
1245 ("disks", ",".join(str(idx) for idx in disks)))
1246
1247 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1248 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1249 _AppendReason(query, reason)
1250
1251 return self._SendRequest(HTTP_POST,
1252 ("/%s/instances/%s/replace-disks" %
1253 (GANETI_RAPI_VERSION, instance)), query, None)
1254
1256 """Prepares an instance for an export.
1257
1258 @type instance: string
1259 @param instance: Instance name
1260 @type mode: string
1261 @param mode: Export mode
1262 @type reason: string
1263 @param reason: the reason for executing this operation
1264 @rtype: string
1265 @return: Job ID
1266
1267 """
1268 query = [("mode", mode)]
1269 _AppendReason(query, reason)
1270 return self._SendRequest(HTTP_PUT,
1271 ("/%s/instances/%s/prepare-export" %
1272 (GANETI_RAPI_VERSION, instance)), query, None)
1273
1274 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1275 remove_instance=None,
1276 x509_key_name=None, destination_x509_ca=None, reason=None):
1277 """Exports an instance.
1278
1279 @type instance: string
1280 @param instance: Instance name
1281 @type mode: string
1282 @param mode: Export mode
1283 @type reason: string
1284 @param reason: the reason for executing this operation
1285 @rtype: string
1286 @return: Job ID
1287
1288 """
1289 body = {
1290 "destination": destination,
1291 "mode": mode,
1292 }
1293
1294 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1295 _SetItemIf(body, remove_instance is not None,
1296 "remove_instance", remove_instance)
1297 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1298 _SetItemIf(body, destination_x509_ca is not None,
1299 "destination_x509_ca", destination_x509_ca)
1300
1301 query = []
1302 _AppendReason(query, reason)
1303
1304 return self._SendRequest(HTTP_PUT,
1305 ("/%s/instances/%s/export" %
1306 (GANETI_RAPI_VERSION, instance)), query, body)
1307
1308 - def MigrateInstance(self, instance, mode=None, cleanup=None,
1309 target_node=None, reason=None):
1310 """Migrates an instance.
1311
1312 @type instance: string
1313 @param instance: Instance name
1314 @type mode: string
1315 @param mode: Migration mode
1316 @type cleanup: bool
1317 @param cleanup: Whether to clean up a previously failed migration
1318 @type target_node: string
1319 @param target_node: Target Node for externally mirrored instances
1320 @type reason: string
1321 @param reason: the reason for executing this operation
1322 @rtype: string
1323 @return: job id
1324
1325 """
1326 body = {}
1327 _SetItemIf(body, mode is not None, "mode", mode)
1328 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1329 _SetItemIf(body, target_node is not None, "target_node", target_node)
1330
1331 query = []
1332 _AppendReason(query, reason)
1333
1334 return self._SendRequest(HTTP_PUT,
1335 ("/%s/instances/%s/migrate" %
1336 (GANETI_RAPI_VERSION, instance)), query, body)
1337
1338 - def FailoverInstance(self, instance, iallocator=None,
1339 ignore_consistency=None, target_node=None, reason=None):
1340 """Does a failover of an instance.
1341
1342 @type instance: string
1343 @param instance: Instance name
1344 @type iallocator: string
1345 @param iallocator: Iallocator for deciding the target node for
1346 shared-storage instances
1347 @type ignore_consistency: bool
1348 @param ignore_consistency: Whether to ignore disk consistency
1349 @type target_node: string
1350 @param target_node: Target node for shared-storage instances
1351 @type reason: string
1352 @param reason: the reason for executing this operation
1353 @rtype: string
1354 @return: job id
1355
1356 """
1357 body = {}
1358 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1359 _SetItemIf(body, ignore_consistency is not None,
1360 "ignore_consistency", ignore_consistency)
1361 _SetItemIf(body, target_node is not None, "target_node", target_node)
1362
1363 query = []
1364 _AppendReason(query, reason)
1365
1366 return self._SendRequest(HTTP_PUT,
1367 ("/%s/instances/%s/failover" %
1368 (GANETI_RAPI_VERSION, instance)), query, body)
1369
1370 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1371 reason=None):
1372 """Changes the name of an instance.
1373
1374 @type instance: string
1375 @param instance: Instance name
1376 @type new_name: string
1377 @param new_name: New instance name
1378 @type ip_check: bool
1379 @param ip_check: Whether to ensure instance's IP address is inactive
1380 @type name_check: bool
1381 @param name_check: Whether to ensure instance's name is resolvable
1382 @type reason: string
1383 @param reason: the reason for executing this operation
1384 @rtype: string
1385 @return: job id
1386
1387 """
1388 body = {
1389 "new_name": new_name,
1390 }
1391
1392 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1393 _SetItemIf(body, name_check is not None, "name_check", name_check)
1394
1395 query = []
1396 _AppendReason(query, reason)
1397
1398 return self._SendRequest(HTTP_PUT,
1399 ("/%s/instances/%s/rename" %
1400 (GANETI_RAPI_VERSION, instance)), query, body)
1401
1403 """Request information for connecting to instance's console.
1404
1405 @type instance: string
1406 @param instance: Instance name
1407 @type reason: string
1408 @param reason: the reason for executing this operation
1409 @rtype: dict
1410 @return: dictionary containing information about instance's console
1411
1412 """
1413 query = []
1414 _AppendReason(query, reason)
1415 return self._SendRequest(HTTP_GET,
1416 ("/%s/instances/%s/console" %
1417 (GANETI_RAPI_VERSION, instance)), query, None)
1418
1420 """Gets all jobs for the cluster.
1421
1422 @type bulk: bool
1423 @param bulk: Whether to return detailed information about jobs.
1424 @rtype: list of int
1425 @return: List of job ids for the cluster or list of dicts with detailed
1426 information about the jobs if bulk parameter was true.
1427
1428 """
1429 query = []
1430 _AppendIf(query, bulk, ("bulk", 1))
1431
1432 if bulk:
1433 return self._SendRequest(HTTP_GET,
1434 "/%s/jobs" % GANETI_RAPI_VERSION,
1435 query, None)
1436 else:
1437 return [int(j["id"])
1438 for j in self._SendRequest(HTTP_GET,
1439 "/%s/jobs" % GANETI_RAPI_VERSION,
1440 None, None)]
1441
1443 """Gets the status of a job.
1444
1445 @type job_id: string
1446 @param job_id: job id whose status to query
1447
1448 @rtype: dict
1449 @return: job status
1450
1451 """
1452 return self._SendRequest(HTTP_GET,
1453 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1454 None, None)
1455
1457 """Polls cluster for job status until completion.
1458
1459 Completion is defined as any of the following states listed in
1460 L{JOB_STATUS_FINALIZED}.
1461
1462 @type job_id: string
1463 @param job_id: job id to watch
1464 @type period: int
1465 @param period: how often to poll for status (optional, default 5s)
1466 @type retries: int
1467 @param retries: how many time to poll before giving up
1468 (optional, default -1 means unlimited)
1469
1470 @rtype: bool
1471 @return: C{True} if job succeeded or C{False} if failed/status timeout
1472 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1473 possible; L{WaitForJobChange} returns immediately after a job changed and
1474 does not use polling
1475
1476 """
1477 while retries != 0:
1478 job_result = self.GetJobStatus(job_id)
1479
1480 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1481 return True
1482 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1483 return False
1484
1485 if period:
1486 time.sleep(period)
1487
1488 if retries > 0:
1489 retries -= 1
1490
1491 return False
1492
1494 """Waits for job changes.
1495
1496 @type job_id: string
1497 @param job_id: Job ID for which to wait
1498 @return: C{None} if no changes have been detected and a dict with two keys,
1499 C{job_info} and C{log_entries} otherwise.
1500 @rtype: dict
1501
1502 """
1503 body = {
1504 "fields": fields,
1505 "previous_job_info": prev_job_info,
1506 "previous_log_serial": prev_log_serial,
1507 }
1508
1509 return self._SendRequest(HTTP_GET,
1510 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1511 None, body)
1512
1513 - def CancelJob(self, job_id, dry_run=False):
1514 """Cancels a job.
1515
1516 @type job_id: string
1517 @param job_id: id of the job to delete
1518 @type dry_run: bool
1519 @param dry_run: whether to perform a dry run
1520 @rtype: tuple
1521 @return: tuple containing the result, and a message (bool, string)
1522
1523 """
1524 query = []
1525 _AppendDryRunIf(query, dry_run)
1526
1527 return self._SendRequest(HTTP_DELETE,
1528 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1529 query, None)
1530
1531 - def GetNodes(self, bulk=False, reason=None):
1532 """Gets all nodes in the cluster.
1533
1534 @type bulk: bool
1535 @param bulk: whether to return all information about all instances
1536 @type reason: string
1537 @param reason: the reason for executing this operation
1538
1539 @rtype: list of dict or str
1540 @return: if bulk is true, info about nodes in the cluster,
1541 else list of nodes in the cluster
1542
1543 """
1544 query = []
1545 _AppendIf(query, bulk, ("bulk", 1))
1546 _AppendReason(query, reason)
1547
1548 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1549 query, None)
1550 if bulk:
1551 return nodes
1552 else:
1553 return [n["id"] for n in nodes]
1554
1555 - def GetNode(self, node, reason=None):
1556 """Gets information about a node.
1557
1558 @type node: str
1559 @param node: node whose info to return
1560 @type reason: string
1561 @param reason: the reason for executing this operation
1562
1563 @rtype: dict
1564 @return: info about the node
1565
1566 """
1567 query = []
1568 _AppendReason(query, reason)
1569
1570 return self._SendRequest(HTTP_GET,
1571 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1572 query, None)
1573
1574 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1575 dry_run=False, early_release=None,
1576 mode=None, accept_old=False, reason=None):
1577 """Evacuates instances from a Ganeti node.
1578
1579 @type node: str
1580 @param node: node to evacuate
1581 @type iallocator: str or None
1582 @param iallocator: instance allocator to use
1583 @type remote_node: str
1584 @param remote_node: node to evaucate to
1585 @type dry_run: bool
1586 @param dry_run: whether to perform a dry run
1587 @type early_release: bool
1588 @param early_release: whether to enable parallelization
1589 @type mode: string
1590 @param mode: Node evacuation mode
1591 @type accept_old: bool
1592 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1593 results
1594 @type reason: string
1595 @param reason: the reason for executing this operation
1596
1597 @rtype: string, or a list for pre-2.5 results
1598 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1599 list of (job ID, instance name, new secondary node); if dry_run was
1600 specified, then the actual move jobs were not submitted and the job IDs
1601 will be C{None}
1602
1603 @raises GanetiApiError: if an iallocator and remote_node are both
1604 specified
1605
1606 """
1607 if iallocator and remote_node:
1608 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1609
1610 query = []
1611 _AppendDryRunIf(query, dry_run)
1612 _AppendReason(query, reason)
1613
1614 if _NODE_EVAC_RES1 in self.GetFeatures():
1615
1616 body = {}
1617
1618 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1619 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1620 _SetItemIf(body, early_release is not None,
1621 "early_release", early_release)
1622 _SetItemIf(body, mode is not None, "mode", mode)
1623 else:
1624
1625 body = None
1626
1627 if not accept_old:
1628 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1629 " not accept old-style results (parameter"
1630 " accept_old)")
1631
1632
1633 if mode is not None and mode != NODE_EVAC_SEC:
1634 raise GanetiApiError("Server can only evacuate secondary instances")
1635
1636 _AppendIf(query, iallocator, ("iallocator", iallocator))
1637 _AppendIf(query, remote_node, ("remote_node", remote_node))
1638 _AppendIf(query, early_release, ("early_release", 1))
1639
1640 return self._SendRequest(HTTP_POST,
1641 ("/%s/nodes/%s/evacuate" %
1642 (GANETI_RAPI_VERSION, node)), query, body)
1643
1644 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1645 target_node=None, reason=None):
1646 """Migrates all primary instances from a node.
1647
1648 @type node: str
1649 @param node: node to migrate
1650 @type mode: string
1651 @param mode: if passed, it will overwrite the live migration type,
1652 otherwise the hypervisor default will be used
1653 @type dry_run: bool
1654 @param dry_run: whether to perform a dry run
1655 @type iallocator: string
1656 @param iallocator: instance allocator to use
1657 @type target_node: string
1658 @param target_node: Target node for shared-storage instances
1659 @type reason: string
1660 @param reason: the reason for executing this operation
1661
1662 @rtype: string
1663 @return: job id
1664
1665 """
1666 query = []
1667 _AppendDryRunIf(query, dry_run)
1668 _AppendReason(query, reason)
1669
1670 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1671 body = {}
1672
1673 _SetItemIf(body, mode is not None, "mode", mode)
1674 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1675 _SetItemIf(body, target_node is not None, "target_node", target_node)
1676
1677 assert len(query) <= 1
1678
1679 return self._SendRequest(HTTP_POST,
1680 ("/%s/nodes/%s/migrate" %
1681 (GANETI_RAPI_VERSION, node)), query, body)
1682 else:
1683
1684 if target_node is not None:
1685 raise GanetiApiError("Server does not support specifying target node"
1686 " for node migration")
1687
1688 _AppendIf(query, mode is not None, ("mode", mode))
1689
1690 return self._SendRequest(HTTP_POST,
1691 ("/%s/nodes/%s/migrate" %
1692 (GANETI_RAPI_VERSION, node)), query, None)
1693
1695 """Gets the current role for a node.
1696
1697 @type node: str
1698 @param node: node whose role to return
1699 @type reason: string
1700 @param reason: the reason for executing this operation
1701
1702 @rtype: str
1703 @return: the current role for a node
1704
1705 """
1706 query = []
1707 _AppendReason(query, reason)
1708
1709 return self._SendRequest(HTTP_GET,
1710 ("/%s/nodes/%s/role" %
1711 (GANETI_RAPI_VERSION, node)), query, None)
1712
1713 - def SetNodeRole(self, node, role, force=False, auto_promote=None,
1714 reason=None):
1715 """Sets the role for a node.
1716
1717 @type node: str
1718 @param node: the node whose role to set
1719 @type role: str
1720 @param role: the role to set for the node
1721 @type force: bool
1722 @param force: whether to force the role change
1723 @type auto_promote: bool
1724 @param auto_promote: Whether node(s) should be promoted to master candidate
1725 if necessary
1726 @type reason: string
1727 @param reason: the reason for executing this operation
1728
1729 @rtype: string
1730 @return: job id
1731
1732 """
1733 query = []
1734 _AppendForceIf(query, force)
1735 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1736 _AppendReason(query, reason)
1737
1738 return self._SendRequest(HTTP_PUT,
1739 ("/%s/nodes/%s/role" %
1740 (GANETI_RAPI_VERSION, node)), query, role)
1741
1743 """Powercycles a node.
1744
1745 @type node: string
1746 @param node: Node name
1747 @type force: bool
1748 @param force: Whether to force the operation
1749 @type reason: string
1750 @param reason: the reason for executing this operation
1751 @rtype: string
1752 @return: job id
1753
1754 """
1755 query = []
1756 _AppendForceIf(query, force)
1757 _AppendReason(query, reason)
1758
1759 return self._SendRequest(HTTP_POST,
1760 ("/%s/nodes/%s/powercycle" %
1761 (GANETI_RAPI_VERSION, node)), query, None)
1762
1763 - def ModifyNode(self, node, reason=None, **kwargs):
1764 """Modifies a node.
1765
1766 More details for parameters can be found in the RAPI documentation.
1767
1768 @type node: string
1769 @param node: Node name
1770 @type reason: string
1771 @param reason: the reason for executing this operation
1772 @rtype: string
1773 @return: job id
1774
1775 """
1776 query = []
1777 _AppendReason(query, reason)
1778
1779 return self._SendRequest(HTTP_POST,
1780 ("/%s/nodes/%s/modify" %
1781 (GANETI_RAPI_VERSION, node)), query, kwargs)
1782
1784 """Gets the storage units for a node.
1785
1786 @type node: str
1787 @param node: the node whose storage units to return
1788 @type storage_type: str
1789 @param storage_type: storage type whose units to return
1790 @type output_fields: str
1791 @param output_fields: storage type fields to return
1792 @type reason: string
1793 @param reason: the reason for executing this operation
1794
1795 @rtype: string
1796 @return: job id where results can be retrieved
1797
1798 """
1799 query = [
1800 ("storage_type", storage_type),
1801 ("output_fields", output_fields),
1802 ]
1803 _AppendReason(query, reason)
1804
1805 return self._SendRequest(HTTP_GET,
1806 ("/%s/nodes/%s/storage" %
1807 (GANETI_RAPI_VERSION, node)), query, None)
1808
1811 """Modifies parameters of storage units on the node.
1812
1813 @type node: str
1814 @param node: node whose storage units to modify
1815 @type storage_type: str
1816 @param storage_type: storage type whose units to modify
1817 @type name: str
1818 @param name: name of the storage unit
1819 @type allocatable: bool or None
1820 @param allocatable: Whether to set the "allocatable" flag on the storage
1821 unit (None=no modification, True=set, False=unset)
1822 @type reason: string
1823 @param reason: the reason for executing this operation
1824
1825 @rtype: string
1826 @return: job id
1827
1828 """
1829 query = [
1830 ("storage_type", storage_type),
1831 ("name", name),
1832 ]
1833
1834 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1835 _AppendReason(query, reason)
1836
1837 return self._SendRequest(HTTP_PUT,
1838 ("/%s/nodes/%s/storage/modify" %
1839 (GANETI_RAPI_VERSION, node)), query, None)
1840
1842 """Repairs a storage unit on the node.
1843
1844 @type node: str
1845 @param node: node whose storage units to repair
1846 @type storage_type: str
1847 @param storage_type: storage type to repair
1848 @type name: str
1849 @param name: name of the storage unit to repair
1850 @type reason: string
1851 @param reason: the reason for executing this operation
1852
1853 @rtype: string
1854 @return: job id
1855
1856 """
1857 query = [
1858 ("storage_type", storage_type),
1859 ("name", name),
1860 ]
1861 _AppendReason(query, reason)
1862
1863 return self._SendRequest(HTTP_PUT,
1864 ("/%s/nodes/%s/storage/repair" %
1865 (GANETI_RAPI_VERSION, node)), query, None)
1866
1885
1909
1933
1935 """Gets all networks in the cluster.
1936
1937 @type bulk: bool
1938 @param bulk: whether to return all information about the networks
1939
1940 @rtype: list of dict or str
1941 @return: if bulk is true, a list of dictionaries with info about all
1942 networks in the cluster, else a list of names of those networks
1943
1944 """
1945 query = []
1946 _AppendIf(query, bulk, ("bulk", 1))
1947 _AppendReason(query, reason)
1948
1949 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1950 query, None)
1951 if bulk:
1952 return networks
1953 else:
1954 return [n["name"] for n in networks]
1955
1957 """Gets information about a network.
1958
1959 @type network: str
1960 @param network: name of the network whose info to return
1961 @type reason: string
1962 @param reason: the reason for executing this operation
1963
1964 @rtype: dict
1965 @return: info about the network
1966
1967 """
1968 query = []
1969 _AppendReason(query, reason)
1970
1971 return self._SendRequest(HTTP_GET,
1972 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1973 query, None)
1974
1975 - def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1976 gateway6=None, mac_prefix=None,
1977 add_reserved_ips=None, tags=None, dry_run=False,
1978 reason=None):
1979 """Creates a new network.
1980
1981 @type network_name: str
1982 @param network_name: the name of network to create
1983 @type dry_run: bool
1984 @param dry_run: whether to peform a dry run
1985 @type reason: string
1986 @param reason: the reason for executing this operation
1987
1988 @rtype: string
1989 @return: job id
1990
1991 """
1992 query = []
1993 _AppendDryRunIf(query, dry_run)
1994 _AppendReason(query, reason)
1995
1996 if add_reserved_ips:
1997 add_reserved_ips = add_reserved_ips.split(",")
1998
1999 if tags:
2000 tags = tags.split(",")
2001
2002 body = {
2003 "network_name": network_name,
2004 "gateway": gateway,
2005 "network": network,
2006 "gateway6": gateway6,
2007 "network6": network6,
2008 "mac_prefix": mac_prefix,
2009 "add_reserved_ips": add_reserved_ips,
2010 "tags": tags,
2011 }
2012
2013 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2014 query, body)
2015
2016 - def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False,
2017 reason=None):
2034
2035 - def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2036 reason=None):
2051
2053 """Modifies a network.
2054
2055 More details for parameters can be found in the RAPI documentation.
2056
2057 @type network: string
2058 @param network: Network name
2059 @type reason: string
2060 @param reason: the reason for executing this operation
2061 @rtype: string
2062 @return: job id
2063
2064 """
2065 query = []
2066 _AppendReason(query, reason)
2067
2068 return self._SendRequest(HTTP_PUT,
2069 ("/%s/networks/%s/modify" %
2070 (GANETI_RAPI_VERSION, network)), None, kwargs)
2071
2073 """Deletes a network.
2074
2075 @type network: str
2076 @param network: the network to delete
2077 @type dry_run: bool
2078 @param dry_run: whether to peform a dry run
2079 @type reason: string
2080 @param reason: the reason for executing this operation
2081
2082 @rtype: string
2083 @return: job id
2084
2085 """
2086 query = []
2087 _AppendDryRunIf(query, dry_run)
2088 _AppendReason(query, reason)
2089
2090 return self._SendRequest(HTTP_DELETE,
2091 ("/%s/networks/%s" %
2092 (GANETI_RAPI_VERSION, network)), query, None)
2093
2112
2136
2159
2160 - def GetGroups(self, bulk=False, reason=None):
2161 """Gets all node groups in the cluster.
2162
2163 @type bulk: bool
2164 @param bulk: whether to return all information about the groups
2165 @type reason: string
2166 @param reason: the reason for executing this operation
2167
2168 @rtype: list of dict or str
2169 @return: if bulk is true, a list of dictionaries with info about all node
2170 groups in the cluster, else a list of names of those node groups
2171
2172 """
2173 query = []
2174 _AppendIf(query, bulk, ("bulk", 1))
2175 _AppendReason(query, reason)
2176
2177 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2178 query, None)
2179 if bulk:
2180 return groups
2181 else:
2182 return [g["name"] for g in groups]
2183
2184 - def GetGroup(self, group, reason=None):
2185 """Gets information about a node group.
2186
2187 @type group: str
2188 @param group: name of the node group whose info to return
2189 @type reason: string
2190 @param reason: the reason for executing this operation
2191
2192 @rtype: dict
2193 @return: info about the node group
2194
2195 """
2196 query = []
2197 _AppendReason(query, reason)
2198
2199 return self._SendRequest(HTTP_GET,
2200 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2201 query, None)
2202
2203 - def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2204 """Creates a new node group.
2205
2206 @type name: str
2207 @param name: the name of node group to create
2208 @type alloc_policy: str
2209 @param alloc_policy: the desired allocation policy for the group, if any
2210 @type dry_run: bool
2211 @param dry_run: whether to peform a dry run
2212 @type reason: string
2213 @param reason: the reason for executing this operation
2214
2215 @rtype: string
2216 @return: job id
2217
2218 """
2219 query = []
2220 _AppendDryRunIf(query, dry_run)
2221 _AppendReason(query, reason)
2222
2223 body = {
2224 "name": name,
2225 "alloc_policy": alloc_policy,
2226 }
2227
2228 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2229 query, body)
2230
2232 """Modifies a node group.
2233
2234 More details for parameters can be found in the RAPI documentation.
2235
2236 @type group: string
2237 @param group: Node group name
2238 @type reason: string
2239 @param reason: the reason for executing this operation
2240 @rtype: string
2241 @return: job id
2242
2243 """
2244 query = []
2245 _AppendReason(query, reason)
2246
2247 return self._SendRequest(HTTP_PUT,
2248 ("/%s/groups/%s/modify" %
2249 (GANETI_RAPI_VERSION, group)), query, kwargs)
2250
2251 - def DeleteGroup(self, group, dry_run=False, reason=None):
2252 """Deletes a node group.
2253
2254 @type group: str
2255 @param group: the node group to delete
2256 @type dry_run: bool
2257 @param dry_run: whether to peform a dry run
2258 @type reason: string
2259 @param reason: the reason for executing this operation
2260
2261 @rtype: string
2262 @return: job id
2263
2264 """
2265 query = []
2266 _AppendDryRunIf(query, dry_run)
2267 _AppendReason(query, reason)
2268
2269 return self._SendRequest(HTTP_DELETE,
2270 ("/%s/groups/%s" %
2271 (GANETI_RAPI_VERSION, group)), query, None)
2272
2274 """Changes the name of a node group.
2275
2276 @type group: string
2277 @param group: Node group name
2278 @type new_name: string
2279 @param new_name: New node group name
2280 @type reason: string
2281 @param reason: the reason for executing this operation
2282
2283 @rtype: string
2284 @return: job id
2285
2286 """
2287 body = {
2288 "new_name": new_name,
2289 }
2290
2291 query = []
2292 _AppendReason(query, reason)
2293
2294 return self._SendRequest(HTTP_PUT,
2295 ("/%s/groups/%s/rename" %
2296 (GANETI_RAPI_VERSION, group)), query, body)
2297
2298 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2299 reason=None):
2300 """Assigns nodes to a group.
2301
2302 @type group: string
2303 @param group: Node group name
2304 @type nodes: list of strings
2305 @param nodes: List of nodes to assign to the group
2306 @type reason: string
2307 @param reason: the reason for executing this operation
2308
2309 @rtype: string
2310 @return: job id
2311
2312 """
2313 query = []
2314 _AppendForceIf(query, force)
2315 _AppendDryRunIf(query, dry_run)
2316 _AppendReason(query, reason)
2317
2318 body = {
2319 "nodes": nodes,
2320 }
2321
2322 return self._SendRequest(HTTP_PUT,
2323 ("/%s/groups/%s/assign-nodes" %
2324 (GANETI_RAPI_VERSION, group)), query, body)
2325
2344
2368
2391
2392 - def Query(self, what, fields, qfilter=None, reason=None):
2393 """Retrieves information about resources.
2394
2395 @type what: string
2396 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2397 @type fields: list of string
2398 @param fields: Requested fields
2399 @type qfilter: None or list
2400 @param qfilter: Query filter
2401 @type reason: string
2402 @param reason: the reason for executing this operation
2403
2404 @rtype: string
2405 @return: job id
2406
2407 """
2408 query = []
2409 _AppendReason(query, reason)
2410
2411 body = {
2412 "fields": fields,
2413 }
2414
2415 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2416
2417 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2418
2419 return self._SendRequest(HTTP_PUT,
2420 ("/%s/query/%s" %
2421 (GANETI_RAPI_VERSION, what)), query, body)
2422
2423 - def QueryFields(self, what, fields=None, reason=None):
2424 """Retrieves available fields for a resource.
2425
2426 @type what: string
2427 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2428 @type fields: list of string
2429 @param fields: Requested fields
2430 @type reason: string
2431 @param reason: the reason for executing this operation
2432
2433 @rtype: string
2434 @return: job id
2435
2436 """
2437 query = []
2438 _AppendReason(query, reason)
2439
2440 if fields is not None:
2441 _AppendIf(query, True, ("fields", ",".join(fields)))
2442
2443 return self._SendRequest(HTTP_GET,
2444 ("/%s/query/%s/fields" %
2445 (GANETI_RAPI_VERSION, what)), query, None)
2446