1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """Ganeti RAPI client.
32
33 @attention: To use the RAPI client, the application B{must} call
34 C{pycurl.global_init} during initialization and
35 C{pycurl.global_cleanup} before exiting the process. This is very
36 important in multi-threaded programs. See curl_global_init(3) and
37 curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
38 can be used.
39
40 """
41
42
43
44
45 import logging
46 import simplejson
47 import socket
48 import urllib
49 import threading
50 import pycurl
51 import time
52
53 try:
54 from cStringIO import StringIO
55 except ImportError:
56 from StringIO import StringIO
57
58
59 GANETI_RAPI_PORT = 5080
60 GANETI_RAPI_VERSION = 2
61
62 HTTP_DELETE = "DELETE"
63 HTTP_GET = "GET"
64 HTTP_PUT = "PUT"
65 HTTP_POST = "POST"
66 HTTP_OK = 200
67 HTTP_NOT_FOUND = 404
68 HTTP_APP_JSON = "application/json"
69
70 REPLACE_DISK_PRI = "replace_on_primary"
71 REPLACE_DISK_SECONDARY = "replace_on_secondary"
72 REPLACE_DISK_CHG = "replace_new_secondary"
73 REPLACE_DISK_AUTO = "replace_auto"
74
75 NODE_EVAC_PRI = "primary-only"
76 NODE_EVAC_SEC = "secondary-only"
77 NODE_EVAC_ALL = "all"
78
79 NODE_ROLE_DRAINED = "drained"
80 NODE_ROLE_MASTER_CANDIDATE = "master-candidate"
81 NODE_ROLE_MASTER = "master"
82 NODE_ROLE_OFFLINE = "offline"
83 NODE_ROLE_REGULAR = "regular"
84
85 JOB_STATUS_QUEUED = "queued"
86 JOB_STATUS_WAITING = "waiting"
87 JOB_STATUS_CANCELING = "canceling"
88 JOB_STATUS_RUNNING = "running"
89 JOB_STATUS_CANCELED = "canceled"
90 JOB_STATUS_SUCCESS = "success"
91 JOB_STATUS_ERROR = "error"
92 JOB_STATUS_PENDING = frozenset([
93 JOB_STATUS_QUEUED,
94 JOB_STATUS_WAITING,
95 JOB_STATUS_CANCELING,
96 ])
97 JOB_STATUS_FINALIZED = frozenset([
98 JOB_STATUS_CANCELED,
99 JOB_STATUS_SUCCESS,
100 JOB_STATUS_ERROR,
101 ])
102 JOB_STATUS_ALL = frozenset([
103 JOB_STATUS_RUNNING,
104 ]) | JOB_STATUS_PENDING | JOB_STATUS_FINALIZED
105
106
107 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
108
109
110 _REQ_DATA_VERSION_FIELD = "__version__"
111 _QPARAM_DRY_RUN = "dry-run"
112 _QPARAM_FORCE = "force"
113
114
115 INST_CREATE_REQV1 = "instance-create-reqv1"
116 INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
117 NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
118 NODE_EVAC_RES1 = "node-evac-res1"
119
120
121 _INST_CREATE_REQV1 = INST_CREATE_REQV1
122 _INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
123 _NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
124 _NODE_EVAC_RES1 = NODE_EVAC_RES1
125
126
127 ECODE_RESOLVER = "resolver_error"
128
129
130 ECODE_NORES = "insufficient_resources"
131
132
133 ECODE_TEMP_NORES = "temp_insufficient_resources"
134
135
136 ECODE_INVAL = "wrong_input"
137
138
139 ECODE_STATE = "wrong_state"
140
141
142 ECODE_NOENT = "unknown_entity"
143
144
145 ECODE_EXISTS = "already_exists"
146
147
148 ECODE_NOTUNIQUE = "resource_not_unique"
149
150
151 ECODE_FAULT = "internal_error"
152
153
154 ECODE_ENVIRON = "environment_error"
155
156
157 ECODE_ALL = frozenset([
158 ECODE_RESOLVER,
159 ECODE_NORES,
160 ECODE_TEMP_NORES,
161 ECODE_INVAL,
162 ECODE_STATE,
163 ECODE_NOENT,
164 ECODE_EXISTS,
165 ECODE_NOTUNIQUE,
166 ECODE_FAULT,
167 ECODE_ENVIRON,
168 ])
169
170
171 try:
172 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
173 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
174 except AttributeError:
175 _CURLE_SSL_CACERT = 60
176 _CURLE_SSL_CACERT_BADFILE = 77
177
178 _CURL_SSL_CERT_ERRORS = frozenset([
179 _CURLE_SSL_CACERT,
180 _CURLE_SSL_CACERT_BADFILE,
181 ])
182
183
184 -class Error(Exception):
185 """Base error class for this module.
186
187 """
188 pass
189
192 """Generic error raised from Ganeti API.
193
194 """
198
201 """Raised when a problem is found with the SSL certificate.
202
203 """
204 pass
205
208 """Return the current timestamp expressed as number of nanoseconds since the
209 unix epoch
210
211 @return: nanoseconds since the Unix epoch
212
213 """
214 return int(time.time() * 1000000000)
215
216
217 -def _AppendIf(container, condition, value):
218 """Appends to a list if a condition evaluates to truth.
219
220 """
221 if condition:
222 container.append(value)
223
224 return condition
225
232
239
242 """Appends an element to the reason trail.
243
244 If the user provided a reason, it is added to the reason trail.
245
246 """
247 return _AppendIf(container, reason, ("reason", reason))
248
249
250 -def _SetItemIf(container, condition, item, value):
251 """Sets an item if a condition evaluates to truth.
252
253 """
254 if condition:
255 container[item] = value
256
257 return condition
258
261 """Decorator for code using RAPI client to initialize pycURL.
262
263 """
264 def wrapper(*args, **kwargs):
265
266
267
268 assert threading.activeCount() == 1, \
269 "Found active threads when initializing pycURL"
270
271 pycurl.global_init(pycurl.GLOBAL_ALL)
272 try:
273 return fn(*args, **kwargs)
274 finally:
275 pycurl.global_cleanup()
276
277 return wrapper
278
279
280 -def GenericCurlConfig(verbose=False, use_signal=False,
281 use_curl_cabundle=False, cafile=None, capath=None,
282 proxy=None, verify_hostname=False,
283 connect_timeout=None, timeout=None,
284 _pycurl_version_fn=pycurl.version_info):
285 """Curl configuration function generator.
286
287 @type verbose: bool
288 @param verbose: Whether to set cURL to verbose mode
289 @type use_signal: bool
290 @param use_signal: Whether to allow cURL to use signals
291 @type use_curl_cabundle: bool
292 @param use_curl_cabundle: Whether to use cURL's default CA bundle
293 @type cafile: string
294 @param cafile: In which file we can find the certificates
295 @type capath: string
296 @param capath: In which directory we can find the certificates
297 @type proxy: string
298 @param proxy: Proxy to use, None for default behaviour and empty string for
299 disabling proxies (see curl_easy_setopt(3))
300 @type verify_hostname: bool
301 @param verify_hostname: Whether to verify the remote peer certificate's
302 commonName
303 @type connect_timeout: number
304 @param connect_timeout: Timeout for establishing connection in seconds
305 @type timeout: number
306 @param timeout: Timeout for complete transfer in seconds (see
307 curl_easy_setopt(3)).
308
309 """
310 if use_curl_cabundle and (cafile or capath):
311 raise Error("Can not use default CA bundle when CA file or path is set")
312
313 def _ConfigCurl(curl, logger):
314 """Configures a cURL object
315
316 @type curl: pycurl.Curl
317 @param curl: cURL object
318
319 """
320 logger.debug("Using cURL version %s", pycurl.version)
321
322
323
324
325
326 sslver = _pycurl_version_fn()[5]
327 if not sslver:
328 raise Error("No SSL support in cURL")
329
330 lcsslver = sslver.lower()
331 if lcsslver.startswith("openssl/"):
332 pass
333 elif lcsslver.startswith("nss/"):
334
335 pass
336 elif lcsslver.startswith("gnutls/"):
337 if capath:
338 raise Error("cURL linked against GnuTLS has no support for a"
339 " CA path (%s)" % (pycurl.version, ))
340 elif lcsslver.startswith("boringssl"):
341 pass
342 else:
343 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
344 sslver)
345
346 curl.setopt(pycurl.VERBOSE, verbose)
347 curl.setopt(pycurl.NOSIGNAL, not use_signal)
348
349
350 if verify_hostname:
351
352
353
354
355
356 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
357 else:
358 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
359
360 if cafile or capath or use_curl_cabundle:
361
362 curl.setopt(pycurl.SSL_VERIFYPEER, True)
363 if cafile:
364 curl.setopt(pycurl.CAINFO, str(cafile))
365 if capath:
366 curl.setopt(pycurl.CAPATH, str(capath))
367
368 else:
369
370 curl.setopt(pycurl.SSL_VERIFYPEER, False)
371
372 if proxy is not None:
373 curl.setopt(pycurl.PROXY, str(proxy))
374
375
376 if connect_timeout is not None:
377 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
378 if timeout is not None:
379 curl.setopt(pycurl.TIMEOUT, timeout)
380
381 return _ConfigCurl
382
385 """Ganeti RAPI client.
386
387 """
388 USER_AGENT = "Ganeti RAPI Client"
389 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
390
391 - def __init__(self, host, port=GANETI_RAPI_PORT,
392 username=None, password=None, logger=logging,
393 curl_config_fn=None, curl_factory=None):
394 """Initializes this class.
395
396 @type host: string
397 @param host: the ganeti cluster master to interact with
398 @type port: int
399 @param port: the port on which the RAPI is running (default is 5080)
400 @type username: string
401 @param username: the username to connect with
402 @type password: string
403 @param password: the password to connect with
404 @type curl_config_fn: callable
405 @param curl_config_fn: Function to configure C{pycurl.Curl} object
406 @param logger: Logging object
407
408 """
409 self._username = username
410 self._password = password
411 self._logger = logger
412 self._curl_config_fn = curl_config_fn
413 self._curl_factory = curl_factory
414
415 try:
416 socket.inet_pton(socket.AF_INET6, host)
417 address = "[%s]:%s" % (host, port)
418 except socket.error:
419 address = "%s:%s" % (host, port)
420
421 self._base_url = "https://%s" % address
422
423 if username is not None:
424 if password is None:
425 raise Error("Password not specified")
426 elif password:
427 raise Error("Specified password without username")
428
430 """Creates a cURL object.
431
432 """
433
434 if self._curl_factory:
435 curl = self._curl_factory()
436 else:
437 curl = pycurl.Curl()
438
439
440 curl.setopt(pycurl.VERBOSE, False)
441 curl.setopt(pycurl.FOLLOWLOCATION, False)
442 curl.setopt(pycurl.MAXREDIRS, 5)
443 curl.setopt(pycurl.NOSIGNAL, True)
444 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
445 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
446 curl.setopt(pycurl.SSL_VERIFYPEER, False)
447 curl.setopt(pycurl.HTTPHEADER, [
448 "Accept: %s" % HTTP_APP_JSON,
449 "Content-type: %s" % HTTP_APP_JSON,
450 ])
451
452 assert ((self._username is None and self._password is None) ^
453 (self._username is not None and self._password is not None))
454
455 if self._username:
456
457 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
458 curl.setopt(pycurl.USERPWD,
459 str("%s:%s" % (self._username, self._password)))
460
461
462 if self._curl_config_fn:
463 self._curl_config_fn(curl, self._logger)
464
465 return curl
466
467 @staticmethod
469 """Encode query values for RAPI URL.
470
471 @type query: list of two-tuples
472 @param query: Query arguments
473 @rtype: list
474 @return: Query list with encoded values
475
476 """
477 result = []
478
479 for name, value in query:
480 if value is None:
481 result.append((name, ""))
482
483 elif isinstance(value, bool):
484
485 result.append((name, int(value)))
486
487 elif isinstance(value, (list, tuple, dict)):
488 raise ValueError("Invalid query data type %r" % type(value).__name__)
489
490 else:
491 result.append((name, value))
492
493 return result
494
496 """Sends an HTTP request.
497
498 This constructs a full URL, encodes and decodes HTTP bodies, and
499 handles invalid responses in a pythonic way.
500
501 @type method: string
502 @param method: HTTP method to use
503 @type path: string
504 @param path: HTTP URL path
505 @type query: list of two-tuples
506 @param query: query arguments to pass to urllib.urlencode
507 @type content: str or None
508 @param content: HTTP body content
509
510 @rtype: str
511 @return: JSON-Decoded response
512
513 @raises CertificateError: If an invalid SSL certificate is found
514 @raises GanetiApiError: If an invalid response is returned
515
516 """
517 assert path.startswith("/")
518
519 curl = self._CreateCurl()
520
521 if content is not None:
522 encoded_content = self._json_encoder.encode(content)
523 else:
524 encoded_content = ""
525
526
527 urlparts = [self._base_url, path]
528 if query:
529 urlparts.append("?")
530 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
531
532 url = "".join(urlparts)
533
534 self._logger.debug("Sending request %s %s (content=%r)",
535 method, url, encoded_content)
536
537
538 encoded_resp_body = StringIO()
539
540
541 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
542 curl.setopt(pycurl.URL, str(url))
543 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
544 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
545
546 try:
547
548 try:
549 curl.perform()
550 except pycurl.error, err:
551 if err.args[0] in _CURL_SSL_CERT_ERRORS:
552 raise CertificateError("SSL certificate error %s" % err,
553 code=err.args[0])
554
555 raise GanetiApiError(str(err), code=err.args[0])
556 finally:
557
558
559 curl.setopt(pycurl.POSTFIELDS, "")
560 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
561
562
563 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
564
565
566 if encoded_resp_body.tell():
567 response_content = simplejson.loads(encoded_resp_body.getvalue())
568 else:
569 response_content = None
570
571 if http_code != HTTP_OK:
572 if isinstance(response_content, dict):
573 msg = ("%s %s: %s" %
574 (response_content["code"],
575 response_content["message"],
576 response_content["explain"]))
577 else:
578 msg = str(response_content)
579
580 raise GanetiApiError(msg, code=http_code)
581
582 return response_content
583
585 """Gets the Remote API version running on the cluster.
586
587 @rtype: int
588 @return: Ganeti Remote API version
589
590 """
591 return self._SendRequest(HTTP_GET, "/version", None, None)
592
609
611 """Gets the Operating Systems running in the Ganeti cluster.
612
613 @rtype: list of str
614 @return: operating systems
615 @type reason: string
616 @param reason: the reason for executing this operation
617
618 """
619 query = []
620 _AppendReason(query, reason)
621 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
622 query, None)
623
637
639 """Tells the cluster to redistribute its configuration files.
640
641 @type reason: string
642 @param reason: the reason for executing this operation
643 @rtype: string
644 @return: job id
645
646 """
647 query = []
648 _AppendReason(query, reason)
649 return self._SendRequest(HTTP_PUT,
650 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
651 query, None)
652
654 """Modifies cluster parameters.
655
656 More details for parameters can be found in the RAPI documentation.
657
658 @type reason: string
659 @param reason: the reason for executing this operation
660 @rtype: string
661 @return: job id
662
663 """
664 query = []
665 _AppendReason(query, reason)
666
667 body = kwargs
668
669 return self._SendRequest(HTTP_PUT,
670 "/%s/modify" % GANETI_RAPI_VERSION, query, body)
671
685
706
726
728 """Gets information about instances on the cluster.
729
730 @type bulk: bool
731 @param bulk: whether to return all information about all instances
732 @type reason: string
733 @param reason: the reason for executing this operation
734
735 @rtype: list of dict or list of str
736 @return: if bulk is True, info about the instances, else a list of instances
737
738 """
739 query = []
740 _AppendIf(query, bulk, ("bulk", 1))
741 _AppendReason(query, reason)
742
743 instances = self._SendRequest(HTTP_GET,
744 "/%s/instances" % GANETI_RAPI_VERSION,
745 query, None)
746 if bulk:
747 return instances
748 else:
749 return [i["id"] for i in instances]
750
752 """Gets information about an instance.
753
754 @type instance: str
755 @param instance: instance whose info to return
756 @type reason: string
757 @param reason: the reason for executing this operation
758
759 @rtype: dict
760 @return: info about the instance
761
762 """
763 query = []
764 _AppendReason(query, reason)
765
766 return self._SendRequest(HTTP_GET,
767 ("/%s/instances/%s" %
768 (GANETI_RAPI_VERSION, instance)), query, None)
769
771 """Gets information about an instance.
772
773 @type instance: string
774 @param instance: Instance name
775 @type reason: string
776 @param reason: the reason for executing this operation
777 @rtype: string
778 @return: Job ID
779
780 """
781 query = []
782 if static is not None:
783 query.append(("static", static))
784 _AppendReason(query, reason)
785
786 return self._SendRequest(HTTP_GET,
787 ("/%s/instances/%s/info" %
788 (GANETI_RAPI_VERSION, instance)), query, None)
789
790 @staticmethod
792 """Updates the base with params from kwargs.
793
794 @param base: The base dict, filled with required fields
795
796 @note: This is an inplace update of base
797
798 """
799 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
800 if conflicts:
801 raise GanetiApiError("Required fields can not be specified as"
802 " keywords: %s" % ", ".join(conflicts))
803
804 base.update((key, value) for key, value in kwargs.iteritems()
805 if key != "dry_run")
806
809 """Generates an instance allocation as used by multiallocate.
810
811 More details for parameters can be found in the RAPI documentation.
812 It is the same as used by CreateInstance.
813
814 @type mode: string
815 @param mode: Instance creation mode
816 @type name: string
817 @param name: Hostname of the instance to create
818 @type disk_template: string
819 @param disk_template: Disk template for instance (e.g. plain, diskless,
820 file, or drbd)
821 @type disks: list of dicts
822 @param disks: List of disk definitions
823 @type nics: list of dicts
824 @param nics: List of NIC definitions
825
826 @return: A dict with the generated entry
827
828 """
829
830 alloc = {
831 "mode": mode,
832 "name": name,
833 "disk_template": disk_template,
834 "disks": disks,
835 "nics": nics,
836 }
837
838 self._UpdateWithKwargs(alloc, **kwargs)
839
840 return alloc
841
862
863 - def CreateInstance(self, mode, name, disk_template, disks, nics,
864 reason=None, **kwargs):
865 """Creates a new instance.
866
867 More details for parameters can be found in the RAPI documentation.
868
869 @type mode: string
870 @param mode: Instance creation mode
871 @type name: string
872 @param name: Hostname of the instance to create
873 @type disk_template: string
874 @param disk_template: Disk template for instance (e.g. plain, diskless,
875 file, or drbd)
876 @type disks: list of dicts
877 @param disks: List of disk definitions
878 @type nics: list of dicts
879 @param nics: List of NIC definitions
880 @type dry_run: bool
881 @keyword dry_run: whether to perform a dry run
882 @type reason: string
883 @param reason: the reason for executing this operation
884
885 @rtype: string
886 @return: job id
887
888 """
889 query = []
890
891 _AppendDryRunIf(query, kwargs.get("dry_run"))
892 _AppendReason(query, reason)
893
894 if _INST_CREATE_REQV1 in self.GetFeatures():
895 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
896 **kwargs)
897 body[_REQ_DATA_VERSION_FIELD] = 1
898 else:
899 raise GanetiApiError("Server does not support new-style (version 1)"
900 " instance creation requests")
901
902 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
903 query, body)
904
905 - def DeleteInstance(self, instance, dry_run=False, reason=None, **kwargs):
926
928 """Modifies an instance.
929
930 More details for parameters can be found in the RAPI documentation.
931
932 @type instance: string
933 @param instance: Instance name
934 @type reason: string
935 @param reason: the reason for executing this operation
936 @rtype: string
937 @return: job id
938
939 """
940 body = kwargs
941 query = []
942 _AppendReason(query, reason)
943
944 return self._SendRequest(HTTP_PUT,
945 ("/%s/instances/%s/modify" %
946 (GANETI_RAPI_VERSION, instance)), query, body)
947
949 """Activates an instance's disks.
950
951 @type instance: string
952 @param instance: Instance name
953 @type ignore_size: bool
954 @param ignore_size: Whether to ignore recorded size
955 @type reason: string
956 @param reason: the reason for executing this operation
957 @rtype: string
958 @return: job id
959
960 """
961 query = []
962 _AppendIf(query, ignore_size, ("ignore_size", 1))
963 _AppendReason(query, reason)
964
965 return self._SendRequest(HTTP_PUT,
966 ("/%s/instances/%s/activate-disks" %
967 (GANETI_RAPI_VERSION, instance)), query, None)
968
986
989 """Recreate an instance's disks.
990
991 @type instance: string
992 @param instance: Instance name
993 @type disks: list of int
994 @param disks: List of disk indexes
995 @type nodes: list of string
996 @param nodes: New instance nodes, if relocation is desired
997 @type reason: string
998 @param reason: the reason for executing this operation
999 @type iallocator: str or None
1000 @param iallocator: instance allocator plugin to use
1001 @rtype: string
1002 @return: job id
1003
1004 """
1005 body = {}
1006 _SetItemIf(body, disks is not None, "disks", disks)
1007 _SetItemIf(body, nodes is not None, "nodes", nodes)
1008 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1009
1010 query = []
1011 _AppendReason(query, reason)
1012
1013 return self._SendRequest(HTTP_POST,
1014 ("/%s/instances/%s/recreate-disks" %
1015 (GANETI_RAPI_VERSION, instance)), query, body)
1016
1017 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
1018 reason=None):
1019 """Grows a disk of an instance.
1020
1021 More details for parameters can be found in the RAPI documentation.
1022
1023 @type instance: string
1024 @param instance: Instance name
1025 @type disk: integer
1026 @param disk: Disk index
1027 @type amount: integer
1028 @param amount: Grow disk by this amount (MiB)
1029 @type wait_for_sync: bool
1030 @param wait_for_sync: Wait for disk to synchronize
1031 @type reason: string
1032 @param reason: the reason for executing this operation
1033 @rtype: string
1034 @return: job id
1035
1036 """
1037 body = {
1038 "amount": amount,
1039 }
1040
1041 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1042
1043 query = []
1044 _AppendReason(query, reason)
1045
1046 return self._SendRequest(HTTP_POST,
1047 ("/%s/instances/%s/disk/%s/grow" %
1048 (GANETI_RAPI_VERSION, instance, disk)),
1049 query, body)
1050
1068
1092
1115
1116 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1117 dry_run=False, reason=None, **kwargs):
1118 """Reboots an instance.
1119
1120 @type instance: str
1121 @param instance: instance to reboot
1122 @type reboot_type: str
1123 @param reboot_type: one of: hard, soft, full
1124 @type ignore_secondaries: bool
1125 @param ignore_secondaries: if True, ignores errors for the secondary node
1126 while re-assembling disks (in hard-reboot mode only)
1127 @type dry_run: bool
1128 @param dry_run: whether to perform a dry run
1129 @type reason: string
1130 @param reason: the reason for the reboot
1131 @rtype: string
1132 @return: job id
1133
1134 """
1135 query = []
1136 body = kwargs
1137
1138 _AppendDryRunIf(query, dry_run)
1139 _AppendIf(query, reboot_type, ("type", reboot_type))
1140 _AppendIf(query, ignore_secondaries is not None,
1141 ("ignore_secondaries", ignore_secondaries))
1142 _AppendReason(query, reason)
1143
1144 return self._SendRequest(HTTP_POST,
1145 ("/%s/instances/%s/reboot" %
1146 (GANETI_RAPI_VERSION, instance)), query, body)
1147
1148 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1149 reason=None, **kwargs):
1150 """Shuts down an instance.
1151
1152 @type instance: str
1153 @param instance: the instance to shut down
1154 @type dry_run: bool
1155 @param dry_run: whether to perform a dry run
1156 @type no_remember: bool
1157 @param no_remember: if true, will not record the state change
1158 @type reason: string
1159 @param reason: the reason for the shutdown
1160 @rtype: string
1161 @return: job id
1162
1163 """
1164 query = []
1165 body = kwargs
1166
1167 _AppendDryRunIf(query, dry_run)
1168 _AppendIf(query, no_remember, ("no_remember", 1))
1169 _AppendReason(query, reason)
1170
1171 return self._SendRequest(HTTP_PUT,
1172 ("/%s/instances/%s/shutdown" %
1173 (GANETI_RAPI_VERSION, instance)), query, body)
1174
1175 - def StartupInstance(self, instance, dry_run=False, no_remember=False,
1176 reason=None):
1177 """Starts up an instance.
1178
1179 @type instance: str
1180 @param instance: the instance to start up
1181 @type dry_run: bool
1182 @param dry_run: whether to perform a dry run
1183 @type no_remember: bool
1184 @param no_remember: if true, will not record the state change
1185 @type reason: string
1186 @param reason: the reason for the startup
1187 @rtype: string
1188 @return: job id
1189
1190 """
1191 query = []
1192 _AppendDryRunIf(query, dry_run)
1193 _AppendIf(query, no_remember, ("no_remember", 1))
1194 _AppendReason(query, reason)
1195
1196 return self._SendRequest(HTTP_PUT,
1197 ("/%s/instances/%s/startup" %
1198 (GANETI_RAPI_VERSION, instance)), query, None)
1199
1200 - def ReinstallInstance(self, instance, os=None, no_startup=False,
1201 osparams=None, reason=None):
1202 """Reinstalls an instance.
1203
1204 @type instance: str
1205 @param instance: The instance to reinstall
1206 @type os: str or None
1207 @param os: The operating system to reinstall. If None, the instance's
1208 current operating system will be installed again
1209 @type no_startup: bool
1210 @param no_startup: Whether to start the instance automatically
1211 @type reason: string
1212 @param reason: the reason for executing this operation
1213 @rtype: string
1214 @return: job id
1215
1216 """
1217 query = []
1218 _AppendReason(query, reason)
1219
1220 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1221 body = {
1222 "start": not no_startup,
1223 }
1224 _SetItemIf(body, os is not None, "os", os)
1225 _SetItemIf(body, osparams is not None, "osparams", osparams)
1226 return self._SendRequest(HTTP_POST,
1227 ("/%s/instances/%s/reinstall" %
1228 (GANETI_RAPI_VERSION, instance)), query, body)
1229
1230
1231 if osparams:
1232 raise GanetiApiError("Server does not support specifying OS parameters"
1233 " for instance reinstallation")
1234
1235 query = []
1236 _AppendIf(query, os, ("os", os))
1237 _AppendIf(query, no_startup, ("nostartup", 1))
1238
1239 return self._SendRequest(HTTP_POST,
1240 ("/%s/instances/%s/reinstall" %
1241 (GANETI_RAPI_VERSION, instance)), query, None)
1242
1246 """Replaces disks on an instance.
1247
1248 @type instance: str
1249 @param instance: instance whose disks to replace
1250 @type disks: list of ints
1251 @param disks: Indexes of disks to replace
1252 @type mode: str
1253 @param mode: replacement mode to use (defaults to replace_auto)
1254 @type remote_node: str or None
1255 @param remote_node: new secondary node to use (for use with
1256 replace_new_secondary mode)
1257 @type iallocator: str or None
1258 @param iallocator: instance allocator plugin to use (for use with
1259 replace_auto mode)
1260 @type reason: string
1261 @param reason: the reason for executing this operation
1262 @type early_release: bool
1263 @param early_release: whether to release locks as soon as possible
1264
1265 @rtype: string
1266 @return: job id
1267
1268 """
1269 query = [
1270 ("mode", mode),
1271 ]
1272
1273
1274
1275 if disks is not None:
1276 _AppendIf(query, True,
1277 ("disks", ",".join(str(idx) for idx in disks)))
1278
1279 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1280 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1281 _AppendReason(query, reason)
1282 _AppendIf(query, early_release is not None,
1283 ("early_release", early_release))
1284
1285 return self._SendRequest(HTTP_POST,
1286 ("/%s/instances/%s/replace-disks" %
1287 (GANETI_RAPI_VERSION, instance)), query, None)
1288
1290 """Prepares an instance for an export.
1291
1292 @type instance: string
1293 @param instance: Instance name
1294 @type mode: string
1295 @param mode: Export mode
1296 @type reason: string
1297 @param reason: the reason for executing this operation
1298 @rtype: string
1299 @return: Job ID
1300
1301 """
1302 query = [("mode", mode)]
1303 _AppendReason(query, reason)
1304 return self._SendRequest(HTTP_PUT,
1305 ("/%s/instances/%s/prepare-export" %
1306 (GANETI_RAPI_VERSION, instance)), query, None)
1307
1308 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1309 remove_instance=None, x509_key_name=None,
1310 destination_x509_ca=None, compress=None, reason=None):
1311 """Exports an instance.
1312
1313 @type instance: string
1314 @param instance: Instance name
1315 @type mode: string
1316 @param mode: Export mode
1317 @type reason: string
1318 @param reason: the reason for executing this operation
1319 @rtype: string
1320 @return: Job ID
1321
1322 """
1323 body = {
1324 "destination": destination,
1325 "mode": mode,
1326 }
1327
1328 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1329 _SetItemIf(body, remove_instance is not None,
1330 "remove_instance", remove_instance)
1331 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1332 _SetItemIf(body, destination_x509_ca is not None,
1333 "destination_x509_ca", destination_x509_ca)
1334 _SetItemIf(body, compress is not None, "compress", compress)
1335
1336 query = []
1337 _AppendReason(query, reason)
1338
1339 return self._SendRequest(HTTP_PUT,
1340 ("/%s/instances/%s/export" %
1341 (GANETI_RAPI_VERSION, instance)), query, body)
1342
1343 - def MigrateInstance(self, instance, mode=None, cleanup=None,
1344 target_node=None, reason=None):
1345 """Migrates an instance.
1346
1347 @type instance: string
1348 @param instance: Instance name
1349 @type mode: string
1350 @param mode: Migration mode
1351 @type cleanup: bool
1352 @param cleanup: Whether to clean up a previously failed migration
1353 @type target_node: string
1354 @param target_node: Target Node for externally mirrored instances
1355 @type reason: string
1356 @param reason: the reason for executing this operation
1357 @rtype: string
1358 @return: job id
1359
1360 """
1361 body = {}
1362 _SetItemIf(body, mode is not None, "mode", mode)
1363 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1364 _SetItemIf(body, target_node is not None, "target_node", target_node)
1365
1366 query = []
1367 _AppendReason(query, reason)
1368
1369 return self._SendRequest(HTTP_PUT,
1370 ("/%s/instances/%s/migrate" %
1371 (GANETI_RAPI_VERSION, instance)), query, body)
1372
1373 - def FailoverInstance(self, instance, iallocator=None,
1374 ignore_consistency=None, target_node=None, reason=None):
1375 """Does a failover of an instance.
1376
1377 @type instance: string
1378 @param instance: Instance name
1379 @type iallocator: string
1380 @param iallocator: Iallocator for deciding the target node for
1381 shared-storage instances
1382 @type ignore_consistency: bool
1383 @param ignore_consistency: Whether to ignore disk consistency
1384 @type target_node: string
1385 @param target_node: Target node for shared-storage instances
1386 @type reason: string
1387 @param reason: the reason for executing this operation
1388 @rtype: string
1389 @return: job id
1390
1391 """
1392 body = {}
1393 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1394 _SetItemIf(body, ignore_consistency is not None,
1395 "ignore_consistency", ignore_consistency)
1396 _SetItemIf(body, target_node is not None, "target_node", target_node)
1397
1398 query = []
1399 _AppendReason(query, reason)
1400
1401 return self._SendRequest(HTTP_PUT,
1402 ("/%s/instances/%s/failover" %
1403 (GANETI_RAPI_VERSION, instance)), query, body)
1404
1405 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1406 reason=None):
1407 """Changes the name of an instance.
1408
1409 @type instance: string
1410 @param instance: Instance name
1411 @type new_name: string
1412 @param new_name: New instance name
1413 @type ip_check: bool
1414 @param ip_check: Whether to ensure instance's IP address is inactive
1415 @type name_check: bool
1416 @param name_check: Whether to ensure instance's name is resolvable
1417 @type reason: string
1418 @param reason: the reason for executing this operation
1419 @rtype: string
1420 @return: job id
1421
1422 """
1423 body = {
1424 "new_name": new_name,
1425 }
1426
1427 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1428 _SetItemIf(body, name_check is not None, "name_check", name_check)
1429
1430 query = []
1431 _AppendReason(query, reason)
1432
1433 return self._SendRequest(HTTP_PUT,
1434 ("/%s/instances/%s/rename" %
1435 (GANETI_RAPI_VERSION, instance)), query, body)
1436
1438 """Request information for connecting to instance's console.
1439
1440 @type instance: string
1441 @param instance: Instance name
1442 @type reason: string
1443 @param reason: the reason for executing this operation
1444 @rtype: dict
1445 @return: dictionary containing information about instance's console
1446
1447 """
1448 query = []
1449 _AppendReason(query, reason)
1450 return self._SendRequest(HTTP_GET,
1451 ("/%s/instances/%s/console" %
1452 (GANETI_RAPI_VERSION, instance)), query, None)
1453
1455 """Gets all jobs for the cluster.
1456
1457 @type bulk: bool
1458 @param bulk: Whether to return detailed information about jobs.
1459 @rtype: list of int
1460 @return: List of job ids for the cluster or list of dicts with detailed
1461 information about the jobs if bulk parameter was true.
1462
1463 """
1464 query = []
1465 _AppendIf(query, bulk, ("bulk", 1))
1466
1467 if bulk:
1468 return self._SendRequest(HTTP_GET,
1469 "/%s/jobs" % GANETI_RAPI_VERSION,
1470 query, None)
1471 else:
1472 return [int(j["id"])
1473 for j in self._SendRequest(HTTP_GET,
1474 "/%s/jobs" % GANETI_RAPI_VERSION,
1475 None, None)]
1476
1478 """Gets the status of a job.
1479
1480 @type job_id: string
1481 @param job_id: job id whose status to query
1482
1483 @rtype: dict
1484 @return: job status
1485
1486 """
1487 return self._SendRequest(HTTP_GET,
1488 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1489 None, None)
1490
1492 """Polls cluster for job status until completion.
1493
1494 Completion is defined as any of the following states listed in
1495 L{JOB_STATUS_FINALIZED}.
1496
1497 @type job_id: string
1498 @param job_id: job id to watch
1499 @type period: int
1500 @param period: how often to poll for status (optional, default 5s)
1501 @type retries: int
1502 @param retries: how many time to poll before giving up
1503 (optional, default -1 means unlimited)
1504
1505 @rtype: bool
1506 @return: C{True} if job succeeded or C{False} if failed/status timeout
1507 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1508 possible; L{WaitForJobChange} returns immediately after a job changed and
1509 does not use polling
1510
1511 """
1512 while retries != 0:
1513 job_result = self.GetJobStatus(job_id)
1514
1515 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1516 return True
1517 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1518 return False
1519
1520 if period:
1521 time.sleep(period)
1522
1523 if retries > 0:
1524 retries -= 1
1525
1526 return False
1527
1529 """Waits for job changes.
1530
1531 @type job_id: string
1532 @param job_id: Job ID for which to wait
1533 @return: C{None} if no changes have been detected and a dict with two keys,
1534 C{job_info} and C{log_entries} otherwise.
1535 @rtype: dict
1536
1537 """
1538 body = {
1539 "fields": fields,
1540 "previous_job_info": prev_job_info,
1541 "previous_log_serial": prev_log_serial,
1542 }
1543
1544 return self._SendRequest(HTTP_GET,
1545 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1546 None, body)
1547
1548 - def CancelJob(self, job_id, dry_run=False):
1549 """Cancels a job.
1550
1551 @type job_id: string
1552 @param job_id: id of the job to delete
1553 @type dry_run: bool
1554 @param dry_run: whether to perform a dry run
1555 @rtype: tuple
1556 @return: tuple containing the result, and a message (bool, string)
1557
1558 """
1559 query = []
1560 _AppendDryRunIf(query, dry_run)
1561
1562 return self._SendRequest(HTTP_DELETE,
1563 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1564 query, None)
1565
1566 - def GetNodes(self, bulk=False, reason=None):
1567 """Gets all nodes in the cluster.
1568
1569 @type bulk: bool
1570 @param bulk: whether to return all information about all instances
1571 @type reason: string
1572 @param reason: the reason for executing this operation
1573
1574 @rtype: list of dict or str
1575 @return: if bulk is true, info about nodes in the cluster,
1576 else list of nodes in the cluster
1577
1578 """
1579 query = []
1580 _AppendIf(query, bulk, ("bulk", 1))
1581 _AppendReason(query, reason)
1582
1583 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1584 query, None)
1585 if bulk:
1586 return nodes
1587 else:
1588 return [n["id"] for n in nodes]
1589
1590 - def GetNode(self, node, reason=None):
1591 """Gets information about a node.
1592
1593 @type node: str
1594 @param node: node whose info to return
1595 @type reason: string
1596 @param reason: the reason for executing this operation
1597
1598 @rtype: dict
1599 @return: info about the node
1600
1601 """
1602 query = []
1603 _AppendReason(query, reason)
1604
1605 return self._SendRequest(HTTP_GET,
1606 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1607 query, None)
1608
1609 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1610 dry_run=False, early_release=None,
1611 mode=None, accept_old=False, reason=None):
1612 """Evacuates instances from a Ganeti node.
1613
1614 @type node: str
1615 @param node: node to evacuate
1616 @type iallocator: str or None
1617 @param iallocator: instance allocator to use
1618 @type remote_node: str
1619 @param remote_node: node to evaucate to
1620 @type dry_run: bool
1621 @param dry_run: whether to perform a dry run
1622 @type early_release: bool
1623 @param early_release: whether to enable parallelization
1624 @type mode: string
1625 @param mode: Node evacuation mode
1626 @type accept_old: bool
1627 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1628 results
1629 @type reason: string
1630 @param reason: the reason for executing this operation
1631
1632 @rtype: string, or a list for pre-2.5 results
1633 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1634 list of (job ID, instance name, new secondary node); if dry_run was
1635 specified, then the actual move jobs were not submitted and the job IDs
1636 will be C{None}
1637
1638 @raises GanetiApiError: if an iallocator and remote_node are both
1639 specified
1640
1641 """
1642 if iallocator and remote_node:
1643 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1644
1645 query = []
1646 _AppendDryRunIf(query, dry_run)
1647 _AppendReason(query, reason)
1648
1649 if _NODE_EVAC_RES1 in self.GetFeatures():
1650
1651 body = {}
1652
1653 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1654 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1655 _SetItemIf(body, early_release is not None,
1656 "early_release", early_release)
1657 _SetItemIf(body, mode is not None, "mode", mode)
1658 else:
1659
1660 body = None
1661
1662 if not accept_old:
1663 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1664 " not accept old-style results (parameter"
1665 " accept_old)")
1666
1667
1668 if mode is not None and mode != NODE_EVAC_SEC:
1669 raise GanetiApiError("Server can only evacuate secondary instances")
1670
1671 _AppendIf(query, iallocator, ("iallocator", iallocator))
1672 _AppendIf(query, remote_node, ("remote_node", remote_node))
1673 _AppendIf(query, early_release, ("early_release", 1))
1674
1675 return self._SendRequest(HTTP_POST,
1676 ("/%s/nodes/%s/evacuate" %
1677 (GANETI_RAPI_VERSION, node)), query, body)
1678
1679 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1680 target_node=None, reason=None):
1681 """Migrates all primary instances from a node.
1682
1683 @type node: str
1684 @param node: node to migrate
1685 @type mode: string
1686 @param mode: if passed, it will overwrite the live migration type,
1687 otherwise the hypervisor default will be used
1688 @type dry_run: bool
1689 @param dry_run: whether to perform a dry run
1690 @type iallocator: string
1691 @param iallocator: instance allocator to use
1692 @type target_node: string
1693 @param target_node: Target node for shared-storage instances
1694 @type reason: string
1695 @param reason: the reason for executing this operation
1696
1697 @rtype: string
1698 @return: job id
1699
1700 """
1701 query = []
1702 _AppendDryRunIf(query, dry_run)
1703 _AppendReason(query, reason)
1704
1705 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1706 body = {}
1707
1708 _SetItemIf(body, mode is not None, "mode", mode)
1709 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1710 _SetItemIf(body, target_node is not None, "target_node", target_node)
1711
1712 assert len(query) <= 1
1713
1714 return self._SendRequest(HTTP_POST,
1715 ("/%s/nodes/%s/migrate" %
1716 (GANETI_RAPI_VERSION, node)), query, body)
1717 else:
1718
1719 if target_node is not None:
1720 raise GanetiApiError("Server does not support specifying target node"
1721 " for node migration")
1722
1723 _AppendIf(query, mode is not None, ("mode", mode))
1724
1725 return self._SendRequest(HTTP_POST,
1726 ("/%s/nodes/%s/migrate" %
1727 (GANETI_RAPI_VERSION, node)), query, None)
1728
1730 """Gets the current role for a node.
1731
1732 @type node: str
1733 @param node: node whose role to return
1734 @type reason: string
1735 @param reason: the reason for executing this operation
1736
1737 @rtype: str
1738 @return: the current role for a node
1739
1740 """
1741 query = []
1742 _AppendReason(query, reason)
1743
1744 return self._SendRequest(HTTP_GET,
1745 ("/%s/nodes/%s/role" %
1746 (GANETI_RAPI_VERSION, node)), query, None)
1747
1748 - def SetNodeRole(self, node, role, force=False, auto_promote=None,
1749 reason=None):
1750 """Sets the role for a node.
1751
1752 @type node: str
1753 @param node: the node whose role to set
1754 @type role: str
1755 @param role: the role to set for the node
1756 @type force: bool
1757 @param force: whether to force the role change
1758 @type auto_promote: bool
1759 @param auto_promote: Whether node(s) should be promoted to master candidate
1760 if necessary
1761 @type reason: string
1762 @param reason: the reason for executing this operation
1763
1764 @rtype: string
1765 @return: job id
1766
1767 """
1768 query = []
1769 _AppendForceIf(query, force)
1770 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1771 _AppendReason(query, reason)
1772
1773 return self._SendRequest(HTTP_PUT,
1774 ("/%s/nodes/%s/role" %
1775 (GANETI_RAPI_VERSION, node)), query, role)
1776
1778 """Powercycles a node.
1779
1780 @type node: string
1781 @param node: Node name
1782 @type force: bool
1783 @param force: Whether to force the operation
1784 @type reason: string
1785 @param reason: the reason for executing this operation
1786 @rtype: string
1787 @return: job id
1788
1789 """
1790 query = []
1791 _AppendForceIf(query, force)
1792 _AppendReason(query, reason)
1793
1794 return self._SendRequest(HTTP_POST,
1795 ("/%s/nodes/%s/powercycle" %
1796 (GANETI_RAPI_VERSION, node)), query, None)
1797
1798 - def ModifyNode(self, node, reason=None, **kwargs):
1799 """Modifies a node.
1800
1801 More details for parameters can be found in the RAPI documentation.
1802
1803 @type node: string
1804 @param node: Node name
1805 @type reason: string
1806 @param reason: the reason for executing this operation
1807 @rtype: string
1808 @return: job id
1809
1810 """
1811 query = []
1812 _AppendReason(query, reason)
1813
1814 return self._SendRequest(HTTP_POST,
1815 ("/%s/nodes/%s/modify" %
1816 (GANETI_RAPI_VERSION, node)), query, kwargs)
1817
1819 """Gets the storage units for a node.
1820
1821 @type node: str
1822 @param node: the node whose storage units to return
1823 @type storage_type: str
1824 @param storage_type: storage type whose units to return
1825 @type output_fields: str
1826 @param output_fields: storage type fields to return
1827 @type reason: string
1828 @param reason: the reason for executing this operation
1829
1830 @rtype: string
1831 @return: job id where results can be retrieved
1832
1833 """
1834 query = [
1835 ("storage_type", storage_type),
1836 ("output_fields", output_fields),
1837 ]
1838 _AppendReason(query, reason)
1839
1840 return self._SendRequest(HTTP_GET,
1841 ("/%s/nodes/%s/storage" %
1842 (GANETI_RAPI_VERSION, node)), query, None)
1843
1846 """Modifies parameters of storage units on the node.
1847
1848 @type node: str
1849 @param node: node whose storage units to modify
1850 @type storage_type: str
1851 @param storage_type: storage type whose units to modify
1852 @type name: str
1853 @param name: name of the storage unit
1854 @type allocatable: bool or None
1855 @param allocatable: Whether to set the "allocatable" flag on the storage
1856 unit (None=no modification, True=set, False=unset)
1857 @type reason: string
1858 @param reason: the reason for executing this operation
1859
1860 @rtype: string
1861 @return: job id
1862
1863 """
1864 query = [
1865 ("storage_type", storage_type),
1866 ("name", name),
1867 ]
1868
1869 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1870 _AppendReason(query, reason)
1871
1872 return self._SendRequest(HTTP_PUT,
1873 ("/%s/nodes/%s/storage/modify" %
1874 (GANETI_RAPI_VERSION, node)), query, None)
1875
1877 """Repairs a storage unit on the node.
1878
1879 @type node: str
1880 @param node: node whose storage units to repair
1881 @type storage_type: str
1882 @param storage_type: storage type to repair
1883 @type name: str
1884 @param name: name of the storage unit to repair
1885 @type reason: string
1886 @param reason: the reason for executing this operation
1887
1888 @rtype: string
1889 @return: job id
1890
1891 """
1892 query = [
1893 ("storage_type", storage_type),
1894 ("name", name),
1895 ]
1896 _AppendReason(query, reason)
1897
1898 return self._SendRequest(HTTP_PUT,
1899 ("/%s/nodes/%s/storage/repair" %
1900 (GANETI_RAPI_VERSION, node)), query, None)
1901
1920
1944
1968
1970 """Gets all networks in the cluster.
1971
1972 @type bulk: bool
1973 @param bulk: whether to return all information about the networks
1974
1975 @rtype: list of dict or str
1976 @return: if bulk is true, a list of dictionaries with info about all
1977 networks in the cluster, else a list of names of those networks
1978
1979 """
1980 query = []
1981 _AppendIf(query, bulk, ("bulk", 1))
1982 _AppendReason(query, reason)
1983
1984 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1985 query, None)
1986 if bulk:
1987 return networks
1988 else:
1989 return [n["name"] for n in networks]
1990
1992 """Gets information about a network.
1993
1994 @type network: str
1995 @param network: name of the network whose info to return
1996 @type reason: string
1997 @param reason: the reason for executing this operation
1998
1999 @rtype: dict
2000 @return: info about the network
2001
2002 """
2003 query = []
2004 _AppendReason(query, reason)
2005
2006 return self._SendRequest(HTTP_GET,
2007 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
2008 query, None)
2009
2010 - def CreateNetwork(self, network_name, network, gateway=None, network6=None,
2011 gateway6=None, mac_prefix=None,
2012 add_reserved_ips=None, tags=None, dry_run=False,
2013 reason=None):
2014 """Creates a new network.
2015
2016 @type network_name: str
2017 @param network_name: the name of network to create
2018 @type dry_run: bool
2019 @param dry_run: whether to peform a dry run
2020 @type reason: string
2021 @param reason: the reason for executing this operation
2022
2023 @rtype: string
2024 @return: job id
2025
2026 """
2027 query = []
2028 _AppendDryRunIf(query, dry_run)
2029 _AppendReason(query, reason)
2030
2031 if add_reserved_ips:
2032 add_reserved_ips = add_reserved_ips.split(",")
2033
2034 if tags:
2035 tags = tags.split(",")
2036
2037 body = {
2038 "network_name": network_name,
2039 "gateway": gateway,
2040 "network": network,
2041 "gateway6": gateway6,
2042 "network6": network6,
2043 "mac_prefix": mac_prefix,
2044 "add_reserved_ips": add_reserved_ips,
2045 "tags": tags,
2046 }
2047
2048 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2049 query, body)
2050
2051 - def ConnectNetwork(self, network_name, group_name, mode, link,
2052 vlan="", dry_run=False, reason=None):
2053 """Connects a Network to a NodeGroup with the given netparams
2054
2055 """
2056 body = {
2057 "group_name": group_name,
2058 "network_mode": mode,
2059 "network_link": link,
2060 "network_vlan": vlan,
2061 }
2062
2063 query = []
2064 _AppendDryRunIf(query, dry_run)
2065 _AppendReason(query, reason)
2066
2067 return self._SendRequest(HTTP_PUT,
2068 ("/%s/networks/%s/connect" %
2069 (GANETI_RAPI_VERSION, network_name)), query, body)
2070
2071 - def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2072 reason=None):
2087
2089 """Modifies a network.
2090
2091 More details for parameters can be found in the RAPI documentation.
2092
2093 @type network: string
2094 @param network: Network name
2095 @type reason: string
2096 @param reason: the reason for executing this operation
2097 @rtype: string
2098 @return: job id
2099
2100 """
2101 query = []
2102 _AppendReason(query, reason)
2103
2104 return self._SendRequest(HTTP_PUT,
2105 ("/%s/networks/%s/modify" %
2106 (GANETI_RAPI_VERSION, network)), None, kwargs)
2107
2109 """Deletes a network.
2110
2111 @type network: str
2112 @param network: the network to delete
2113 @type dry_run: bool
2114 @param dry_run: whether to peform a dry run
2115 @type reason: string
2116 @param reason: the reason for executing this operation
2117
2118 @rtype: string
2119 @return: job id
2120
2121 """
2122 query = []
2123 _AppendDryRunIf(query, dry_run)
2124 _AppendReason(query, reason)
2125
2126 return self._SendRequest(HTTP_DELETE,
2127 ("/%s/networks/%s" %
2128 (GANETI_RAPI_VERSION, network)), query, None)
2129
2148
2172
2195
2196 - def GetGroups(self, bulk=False, reason=None):
2197 """Gets all node groups in the cluster.
2198
2199 @type bulk: bool
2200 @param bulk: whether to return all information about the groups
2201 @type reason: string
2202 @param reason: the reason for executing this operation
2203
2204 @rtype: list of dict or str
2205 @return: if bulk is true, a list of dictionaries with info about all node
2206 groups in the cluster, else a list of names of those node groups
2207
2208 """
2209 query = []
2210 _AppendIf(query, bulk, ("bulk", 1))
2211 _AppendReason(query, reason)
2212
2213 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2214 query, None)
2215 if bulk:
2216 return groups
2217 else:
2218 return [g["name"] for g in groups]
2219
2220 - def GetGroup(self, group, reason=None):
2221 """Gets information about a node group.
2222
2223 @type group: str
2224 @param group: name of the node group whose info to return
2225 @type reason: string
2226 @param reason: the reason for executing this operation
2227
2228 @rtype: dict
2229 @return: info about the node group
2230
2231 """
2232 query = []
2233 _AppendReason(query, reason)
2234
2235 return self._SendRequest(HTTP_GET,
2236 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2237 query, None)
2238
2239 - def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2240 """Creates a new node group.
2241
2242 @type name: str
2243 @param name: the name of node group to create
2244 @type alloc_policy: str
2245 @param alloc_policy: the desired allocation policy for the group, if any
2246 @type dry_run: bool
2247 @param dry_run: whether to peform a dry run
2248 @type reason: string
2249 @param reason: the reason for executing this operation
2250
2251 @rtype: string
2252 @return: job id
2253
2254 """
2255 query = []
2256 _AppendDryRunIf(query, dry_run)
2257 _AppendReason(query, reason)
2258
2259 body = {
2260 "name": name,
2261 "alloc_policy": alloc_policy,
2262 }
2263
2264 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2265 query, body)
2266
2268 """Modifies a node group.
2269
2270 More details for parameters can be found in the RAPI documentation.
2271
2272 @type group: string
2273 @param group: Node group name
2274 @type reason: string
2275 @param reason: the reason for executing this operation
2276 @rtype: string
2277 @return: job id
2278
2279 """
2280 query = []
2281 _AppendReason(query, reason)
2282
2283 return self._SendRequest(HTTP_PUT,
2284 ("/%s/groups/%s/modify" %
2285 (GANETI_RAPI_VERSION, group)), query, kwargs)
2286
2287 - def DeleteGroup(self, group, dry_run=False, reason=None):
2288 """Deletes a node group.
2289
2290 @type group: str
2291 @param group: the node group to delete
2292 @type dry_run: bool
2293 @param dry_run: whether to peform a dry run
2294 @type reason: string
2295 @param reason: the reason for executing this operation
2296
2297 @rtype: string
2298 @return: job id
2299
2300 """
2301 query = []
2302 _AppendDryRunIf(query, dry_run)
2303 _AppendReason(query, reason)
2304
2305 return self._SendRequest(HTTP_DELETE,
2306 ("/%s/groups/%s" %
2307 (GANETI_RAPI_VERSION, group)), query, None)
2308
2310 """Changes the name of a node group.
2311
2312 @type group: string
2313 @param group: Node group name
2314 @type new_name: string
2315 @param new_name: New node group name
2316 @type reason: string
2317 @param reason: the reason for executing this operation
2318
2319 @rtype: string
2320 @return: job id
2321
2322 """
2323 body = {
2324 "new_name": new_name,
2325 }
2326
2327 query = []
2328 _AppendReason(query, reason)
2329
2330 return self._SendRequest(HTTP_PUT,
2331 ("/%s/groups/%s/rename" %
2332 (GANETI_RAPI_VERSION, group)), query, body)
2333
2334 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2335 reason=None):
2336 """Assigns nodes to a group.
2337
2338 @type group: string
2339 @param group: Node group name
2340 @type nodes: list of strings
2341 @param nodes: List of nodes to assign to the group
2342 @type reason: string
2343 @param reason: the reason for executing this operation
2344
2345 @rtype: string
2346 @return: job id
2347
2348 """
2349 query = []
2350 _AppendForceIf(query, force)
2351 _AppendDryRunIf(query, dry_run)
2352 _AppendReason(query, reason)
2353
2354 body = {
2355 "nodes": nodes,
2356 }
2357
2358 return self._SendRequest(HTTP_PUT,
2359 ("/%s/groups/%s/assign-nodes" %
2360 (GANETI_RAPI_VERSION, group)), query, body)
2361
2380
2404
2427
2428 - def Query(self, what, fields, qfilter=None, reason=None):
2429 """Retrieves information about resources.
2430
2431 @type what: string
2432 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2433 @type fields: list of string
2434 @param fields: Requested fields
2435 @type qfilter: None or list
2436 @param qfilter: Query filter
2437 @type reason: string
2438 @param reason: the reason for executing this operation
2439
2440 @rtype: string
2441 @return: job id
2442
2443 """
2444 query = []
2445 _AppendReason(query, reason)
2446
2447 body = {
2448 "fields": fields,
2449 }
2450
2451 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2452
2453 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2454
2455 return self._SendRequest(HTTP_PUT,
2456 ("/%s/query/%s" %
2457 (GANETI_RAPI_VERSION, what)), query, body)
2458
2459 - def QueryFields(self, what, fields=None, reason=None):
2460 """Retrieves available fields for a resource.
2461
2462 @type what: string
2463 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2464 @type fields: list of string
2465 @param fields: Requested fields
2466 @type reason: string
2467 @param reason: the reason for executing this operation
2468
2469 @rtype: string
2470 @return: job id
2471
2472 """
2473 query = []
2474 _AppendReason(query, reason)
2475
2476 if fields is not None:
2477 _AppendIf(query, True, ("fields", ",".join(fields)))
2478
2479 return self._SendRequest(HTTP_GET,
2480 ("/%s/query/%s/fields" %
2481 (GANETI_RAPI_VERSION, what)), query, None)
2482
2484 """Gets all filter rules in the cluster.
2485
2486 @type bulk: bool
2487 @param bulk: whether to return all information about the networks
2488
2489 @rtype: list of dict or str
2490 @return: if bulk is true, a list of dictionaries with info about all
2491 filter rules in the cluster, else a list of UUIDs of those
2492 filters
2493
2494 """
2495 query = []
2496 _AppendIf(query, bulk, ("bulk", 1))
2497
2498 filters = self._SendRequest(HTTP_GET, "/%s/filters" % GANETI_RAPI_VERSION,
2499 query, None)
2500 if bulk:
2501 return filters
2502 else:
2503 return [f["uuid"] for f in filters]
2504
2506 """Gets information about a filter rule.
2507
2508 @type filter_uuid: str
2509 @param filter_uuid: UUID of the filter whose info to return
2510
2511 @rtype: dict
2512 @return: info about the filter
2513
2514 """
2515 query = []
2516
2517 return self._SendRequest(HTTP_GET,
2518 "/%s/filters/%s" % (GANETI_RAPI_VERSION,
2519 filter_uuid),
2520 query, None)
2521
2522 - def AddFilter(self, priority, predicates, action, reason_trail=None):
2523 """Adds a filter rule
2524
2525 @type reason_trail: list of (str, str, int) triples
2526 @param reason_trail: the reason trail for executing this operation,
2527 or None
2528
2529 @rtype: string
2530 @return: filter UUID of the added filter
2531
2532 """
2533 if reason_trail is None:
2534 reason_trail = []
2535
2536 assert isinstance(reason_trail, list)
2537
2538 reason_trail.append(("gnt:client", "", EpochNano(),))
2539
2540 body = {
2541 "priority": priority,
2542 "predicates": predicates,
2543 "action": action,
2544 "reason": reason_trail,
2545 }
2546
2547 query = []
2548
2549 return self._SendRequest(HTTP_POST,
2550 ("/%s/filters" % (GANETI_RAPI_VERSION)),
2551 query, body)
2552
2553 - def ReplaceFilter(self, uuid, priority, predicates, action,
2554 reason_trail=None):
2555 """Replaces a filter rule, or creates one if it doesn't already exist
2556
2557 @type reason_trail: list of (str, str, int) triples
2558 @param reason_trail: the reason trail for executing this operation,
2559 or None
2560
2561 @rtype: string
2562 @return: filter UUID of the replaced/added filter
2563
2564 """
2565 if reason_trail is None:
2566 reason_trail = []
2567
2568 assert isinstance(reason_trail, list)
2569
2570 reason_trail.append(("gnt:client", "", EpochNano(),))
2571
2572 body = {
2573 "priority": priority,
2574 "predicates": predicates,
2575 "action": action,
2576 "reason": reason_trail,
2577 }
2578
2579 query = []
2580
2581 return self._SendRequest(HTTP_PUT,
2582 ("/%s/filters/%s" % (GANETI_RAPI_VERSION, uuid)),
2583 query, body)
2584
2597