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 else:
341 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
342 sslver)
343
344 curl.setopt(pycurl.VERBOSE, verbose)
345 curl.setopt(pycurl.NOSIGNAL, not use_signal)
346
347
348 if verify_hostname:
349
350
351
352
353
354 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
355 else:
356 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
357
358 if cafile or capath or use_curl_cabundle:
359
360 curl.setopt(pycurl.SSL_VERIFYPEER, True)
361 if cafile:
362 curl.setopt(pycurl.CAINFO, str(cafile))
363 if capath:
364 curl.setopt(pycurl.CAPATH, str(capath))
365
366 else:
367
368 curl.setopt(pycurl.SSL_VERIFYPEER, False)
369
370 if proxy is not None:
371 curl.setopt(pycurl.PROXY, str(proxy))
372
373
374 if connect_timeout is not None:
375 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
376 if timeout is not None:
377 curl.setopt(pycurl.TIMEOUT, timeout)
378
379 return _ConfigCurl
380
383 """Ganeti RAPI client.
384
385 """
386 USER_AGENT = "Ganeti RAPI Client"
387 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
388
389 - def __init__(self, host, port=GANETI_RAPI_PORT,
390 username=None, password=None, logger=logging,
391 curl_config_fn=None, curl_factory=None):
392 """Initializes this class.
393
394 @type host: string
395 @param host: the ganeti cluster master to interact with
396 @type port: int
397 @param port: the port on which the RAPI is running (default is 5080)
398 @type username: string
399 @param username: the username to connect with
400 @type password: string
401 @param password: the password to connect with
402 @type curl_config_fn: callable
403 @param curl_config_fn: Function to configure C{pycurl.Curl} object
404 @param logger: Logging object
405
406 """
407 self._username = username
408 self._password = password
409 self._logger = logger
410 self._curl_config_fn = curl_config_fn
411 self._curl_factory = curl_factory
412
413 try:
414 socket.inet_pton(socket.AF_INET6, host)
415 address = "[%s]:%s" % (host, port)
416 except socket.error:
417 address = "%s:%s" % (host, port)
418
419 self._base_url = "https://%s" % address
420
421 if username is not None:
422 if password is None:
423 raise Error("Password not specified")
424 elif password:
425 raise Error("Specified password without username")
426
428 """Creates a cURL object.
429
430 """
431
432 if self._curl_factory:
433 curl = self._curl_factory()
434 else:
435 curl = pycurl.Curl()
436
437
438 curl.setopt(pycurl.VERBOSE, False)
439 curl.setopt(pycurl.FOLLOWLOCATION, False)
440 curl.setopt(pycurl.MAXREDIRS, 5)
441 curl.setopt(pycurl.NOSIGNAL, True)
442 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
443 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
444 curl.setopt(pycurl.SSL_VERIFYPEER, False)
445 curl.setopt(pycurl.HTTPHEADER, [
446 "Accept: %s" % HTTP_APP_JSON,
447 "Content-type: %s" % HTTP_APP_JSON,
448 ])
449
450 assert ((self._username is None and self._password is None) ^
451 (self._username is not None and self._password is not None))
452
453 if self._username:
454
455 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
456 curl.setopt(pycurl.USERPWD,
457 str("%s:%s" % (self._username, self._password)))
458
459
460 if self._curl_config_fn:
461 self._curl_config_fn(curl, self._logger)
462
463 return curl
464
465 @staticmethod
467 """Encode query values for RAPI URL.
468
469 @type query: list of two-tuples
470 @param query: Query arguments
471 @rtype: list
472 @return: Query list with encoded values
473
474 """
475 result = []
476
477 for name, value in query:
478 if value is None:
479 result.append((name, ""))
480
481 elif isinstance(value, bool):
482
483 result.append((name, int(value)))
484
485 elif isinstance(value, (list, tuple, dict)):
486 raise ValueError("Invalid query data type %r" % type(value).__name__)
487
488 else:
489 result.append((name, value))
490
491 return result
492
494 """Sends an HTTP request.
495
496 This constructs a full URL, encodes and decodes HTTP bodies, and
497 handles invalid responses in a pythonic way.
498
499 @type method: string
500 @param method: HTTP method to use
501 @type path: string
502 @param path: HTTP URL path
503 @type query: list of two-tuples
504 @param query: query arguments to pass to urllib.urlencode
505 @type content: str or None
506 @param content: HTTP body content
507
508 @rtype: str
509 @return: JSON-Decoded response
510
511 @raises CertificateError: If an invalid SSL certificate is found
512 @raises GanetiApiError: If an invalid response is returned
513
514 """
515 assert path.startswith("/")
516
517 curl = self._CreateCurl()
518
519 if content is not None:
520 encoded_content = self._json_encoder.encode(content)
521 else:
522 encoded_content = ""
523
524
525 urlparts = [self._base_url, path]
526 if query:
527 urlparts.append("?")
528 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
529
530 url = "".join(urlparts)
531
532 self._logger.debug("Sending request %s %s (content=%r)",
533 method, url, encoded_content)
534
535
536 encoded_resp_body = StringIO()
537
538
539 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
540 curl.setopt(pycurl.URL, str(url))
541 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
542 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
543
544 try:
545
546 try:
547 curl.perform()
548 except pycurl.error, err:
549 if err.args[0] in _CURL_SSL_CERT_ERRORS:
550 raise CertificateError("SSL certificate error %s" % err,
551 code=err.args[0])
552
553 raise GanetiApiError(str(err), code=err.args[0])
554 finally:
555
556
557 curl.setopt(pycurl.POSTFIELDS, "")
558 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
559
560
561 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
562
563
564 if encoded_resp_body.tell():
565 response_content = simplejson.loads(encoded_resp_body.getvalue())
566 else:
567 response_content = None
568
569 if http_code != HTTP_OK:
570 if isinstance(response_content, dict):
571 msg = ("%s %s: %s" %
572 (response_content["code"],
573 response_content["message"],
574 response_content["explain"]))
575 else:
576 msg = str(response_content)
577
578 raise GanetiApiError(msg, code=http_code)
579
580 return response_content
581
583 """Gets the Remote API version running on the cluster.
584
585 @rtype: int
586 @return: Ganeti Remote API version
587
588 """
589 return self._SendRequest(HTTP_GET, "/version", None, None)
590
607
609 """Gets the Operating Systems running in the Ganeti cluster.
610
611 @rtype: list of str
612 @return: operating systems
613 @type reason: string
614 @param reason: the reason for executing this operation
615
616 """
617 query = []
618 _AppendReason(query, reason)
619 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
620 query, None)
621
635
637 """Tells the cluster to redistribute its configuration files.
638
639 @type reason: string
640 @param reason: the reason for executing this operation
641 @rtype: string
642 @return: job id
643
644 """
645 query = []
646 _AppendReason(query, reason)
647 return self._SendRequest(HTTP_PUT,
648 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
649 query, None)
650
652 """Modifies cluster parameters.
653
654 More details for parameters can be found in the RAPI documentation.
655
656 @type reason: string
657 @param reason: the reason for executing this operation
658 @rtype: string
659 @return: job id
660
661 """
662 query = []
663 _AppendReason(query, reason)
664
665 body = kwargs
666
667 return self._SendRequest(HTTP_PUT,
668 "/%s/modify" % GANETI_RAPI_VERSION, query, body)
669
683
704
724
726 """Gets information about instances on the cluster.
727
728 @type bulk: bool
729 @param bulk: whether to return all information about all instances
730 @type reason: string
731 @param reason: the reason for executing this operation
732
733 @rtype: list of dict or list of str
734 @return: if bulk is True, info about the instances, else a list of instances
735
736 """
737 query = []
738 _AppendIf(query, bulk, ("bulk", 1))
739 _AppendReason(query, reason)
740
741 instances = self._SendRequest(HTTP_GET,
742 "/%s/instances" % GANETI_RAPI_VERSION,
743 query, None)
744 if bulk:
745 return instances
746 else:
747 return [i["id"] for i in instances]
748
750 """Gets information about an instance.
751
752 @type instance: str
753 @param instance: instance whose info to return
754 @type reason: string
755 @param reason: the reason for executing this operation
756
757 @rtype: dict
758 @return: info about the instance
759
760 """
761 query = []
762 _AppendReason(query, reason)
763
764 return self._SendRequest(HTTP_GET,
765 ("/%s/instances/%s" %
766 (GANETI_RAPI_VERSION, instance)), query, None)
767
769 """Gets information about an instance.
770
771 @type instance: string
772 @param instance: Instance name
773 @type reason: string
774 @param reason: the reason for executing this operation
775 @rtype: string
776 @return: Job ID
777
778 """
779 query = []
780 if static is not None:
781 query.append(("static", static))
782 _AppendReason(query, reason)
783
784 return self._SendRequest(HTTP_GET,
785 ("/%s/instances/%s/info" %
786 (GANETI_RAPI_VERSION, instance)), query, None)
787
788 @staticmethod
790 """Updates the base with params from kwargs.
791
792 @param base: The base dict, filled with required fields
793
794 @note: This is an inplace update of base
795
796 """
797 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
798 if conflicts:
799 raise GanetiApiError("Required fields can not be specified as"
800 " keywords: %s" % ", ".join(conflicts))
801
802 base.update((key, value) for key, value in kwargs.iteritems()
803 if key != "dry_run")
804
807 """Generates an instance allocation as used by multiallocate.
808
809 More details for parameters can be found in the RAPI documentation.
810 It is the same as used by CreateInstance.
811
812 @type mode: string
813 @param mode: Instance creation mode
814 @type name: string
815 @param name: Hostname of the instance to create
816 @type disk_template: string
817 @param disk_template: Disk template for instance (e.g. plain, diskless,
818 file, or drbd)
819 @type disks: list of dicts
820 @param disks: List of disk definitions
821 @type nics: list of dicts
822 @param nics: List of NIC definitions
823
824 @return: A dict with the generated entry
825
826 """
827
828 alloc = {
829 "mode": mode,
830 "name": name,
831 "disk_template": disk_template,
832 "disks": disks,
833 "nics": nics,
834 }
835
836 self._UpdateWithKwargs(alloc, **kwargs)
837
838 return alloc
839
860
861 - def CreateInstance(self, mode, name, disk_template, disks, nics,
862 reason=None, **kwargs):
863 """Creates a new instance.
864
865 More details for parameters can be found in the RAPI documentation.
866
867 @type mode: string
868 @param mode: Instance creation mode
869 @type name: string
870 @param name: Hostname of the instance to create
871 @type disk_template: string
872 @param disk_template: Disk template for instance (e.g. plain, diskless,
873 file, or drbd)
874 @type disks: list of dicts
875 @param disks: List of disk definitions
876 @type nics: list of dicts
877 @param nics: List of NIC definitions
878 @type dry_run: bool
879 @keyword dry_run: whether to perform a dry run
880 @type reason: string
881 @param reason: the reason for executing this operation
882
883 @rtype: string
884 @return: job id
885
886 """
887 query = []
888
889 _AppendDryRunIf(query, kwargs.get("dry_run"))
890 _AppendReason(query, reason)
891
892 if _INST_CREATE_REQV1 in self.GetFeatures():
893 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
894 **kwargs)
895 body[_REQ_DATA_VERSION_FIELD] = 1
896 else:
897 raise GanetiApiError("Server does not support new-style (version 1)"
898 " instance creation requests")
899
900 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
901 query, body)
902
903 - def DeleteInstance(self, instance, dry_run=False, reason=None, **kwargs):
924
926 """Modifies an instance.
927
928 More details for parameters can be found in the RAPI documentation.
929
930 @type instance: string
931 @param instance: Instance name
932 @type reason: string
933 @param reason: the reason for executing this operation
934 @rtype: string
935 @return: job id
936
937 """
938 body = kwargs
939 query = []
940 _AppendReason(query, reason)
941
942 return self._SendRequest(HTTP_PUT,
943 ("/%s/instances/%s/modify" %
944 (GANETI_RAPI_VERSION, instance)), query, body)
945
947 """Activates an instance's disks.
948
949 @type instance: string
950 @param instance: Instance name
951 @type ignore_size: bool
952 @param ignore_size: Whether to ignore recorded size
953 @type reason: string
954 @param reason: the reason for executing this operation
955 @rtype: string
956 @return: job id
957
958 """
959 query = []
960 _AppendIf(query, ignore_size, ("ignore_size", 1))
961 _AppendReason(query, reason)
962
963 return self._SendRequest(HTTP_PUT,
964 ("/%s/instances/%s/activate-disks" %
965 (GANETI_RAPI_VERSION, instance)), query, None)
966
984
987 """Recreate an instance's disks.
988
989 @type instance: string
990 @param instance: Instance name
991 @type disks: list of int
992 @param disks: List of disk indexes
993 @type nodes: list of string
994 @param nodes: New instance nodes, if relocation is desired
995 @type reason: string
996 @param reason: the reason for executing this operation
997 @type iallocator: str or None
998 @param iallocator: instance allocator plugin to use
999 @rtype: string
1000 @return: job id
1001
1002 """
1003 body = {}
1004 _SetItemIf(body, disks is not None, "disks", disks)
1005 _SetItemIf(body, nodes is not None, "nodes", nodes)
1006 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1007
1008 query = []
1009 _AppendReason(query, reason)
1010
1011 return self._SendRequest(HTTP_POST,
1012 ("/%s/instances/%s/recreate-disks" %
1013 (GANETI_RAPI_VERSION, instance)), query, body)
1014
1015 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
1016 reason=None):
1017 """Grows a disk of an instance.
1018
1019 More details for parameters can be found in the RAPI documentation.
1020
1021 @type instance: string
1022 @param instance: Instance name
1023 @type disk: integer
1024 @param disk: Disk index
1025 @type amount: integer
1026 @param amount: Grow disk by this amount (MiB)
1027 @type wait_for_sync: bool
1028 @param wait_for_sync: Wait for disk to synchronize
1029 @type reason: string
1030 @param reason: the reason for executing this operation
1031 @rtype: string
1032 @return: job id
1033
1034 """
1035 body = {
1036 "amount": amount,
1037 }
1038
1039 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1040
1041 query = []
1042 _AppendReason(query, reason)
1043
1044 return self._SendRequest(HTTP_POST,
1045 ("/%s/instances/%s/disk/%s/grow" %
1046 (GANETI_RAPI_VERSION, instance, disk)),
1047 query, body)
1048
1066
1090
1113
1114 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1115 dry_run=False, reason=None, **kwargs):
1116 """Reboots an instance.
1117
1118 @type instance: str
1119 @param instance: instance to reboot
1120 @type reboot_type: str
1121 @param reboot_type: one of: hard, soft, full
1122 @type ignore_secondaries: bool
1123 @param ignore_secondaries: if True, ignores errors for the secondary node
1124 while re-assembling disks (in hard-reboot mode only)
1125 @type dry_run: bool
1126 @param dry_run: whether to perform a dry run
1127 @type reason: string
1128 @param reason: the reason for the reboot
1129 @rtype: string
1130 @return: job id
1131
1132 """
1133 query = []
1134 body = kwargs
1135
1136 _AppendDryRunIf(query, dry_run)
1137 _AppendIf(query, reboot_type, ("type", reboot_type))
1138 _AppendIf(query, ignore_secondaries is not None,
1139 ("ignore_secondaries", ignore_secondaries))
1140 _AppendReason(query, reason)
1141
1142 return self._SendRequest(HTTP_POST,
1143 ("/%s/instances/%s/reboot" %
1144 (GANETI_RAPI_VERSION, instance)), query, body)
1145
1146 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1147 reason=None, **kwargs):
1148 """Shuts down an instance.
1149
1150 @type instance: str
1151 @param instance: the instance to shut down
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 shutdown
1158 @rtype: string
1159 @return: job id
1160
1161 """
1162 query = []
1163 body = kwargs
1164
1165 _AppendDryRunIf(query, dry_run)
1166 _AppendIf(query, no_remember, ("no_remember", 1))
1167 _AppendReason(query, reason)
1168
1169 return self._SendRequest(HTTP_PUT,
1170 ("/%s/instances/%s/shutdown" %
1171 (GANETI_RAPI_VERSION, instance)), query, body)
1172
1173 - def StartupInstance(self, instance, dry_run=False, no_remember=False,
1174 reason=None):
1175 """Starts up an instance.
1176
1177 @type instance: str
1178 @param instance: the instance to start up
1179 @type dry_run: bool
1180 @param dry_run: whether to perform a dry run
1181 @type no_remember: bool
1182 @param no_remember: if true, will not record the state change
1183 @type reason: string
1184 @param reason: the reason for the startup
1185 @rtype: string
1186 @return: job id
1187
1188 """
1189 query = []
1190 _AppendDryRunIf(query, dry_run)
1191 _AppendIf(query, no_remember, ("no_remember", 1))
1192 _AppendReason(query, reason)
1193
1194 return self._SendRequest(HTTP_PUT,
1195 ("/%s/instances/%s/startup" %
1196 (GANETI_RAPI_VERSION, instance)), query, None)
1197
1198 - def ReinstallInstance(self, instance, os=None, no_startup=False,
1199 osparams=None, reason=None):
1200 """Reinstalls an instance.
1201
1202 @type instance: str
1203 @param instance: The instance to reinstall
1204 @type os: str or None
1205 @param os: The operating system to reinstall. If None, the instance's
1206 current operating system will be installed again
1207 @type no_startup: bool
1208 @param no_startup: Whether to start the instance automatically
1209 @type reason: string
1210 @param reason: the reason for executing this operation
1211 @rtype: string
1212 @return: job id
1213
1214 """
1215 query = []
1216 _AppendReason(query, reason)
1217
1218 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1219 body = {
1220 "start": not no_startup,
1221 }
1222 _SetItemIf(body, os is not None, "os", os)
1223 _SetItemIf(body, osparams is not None, "osparams", osparams)
1224 return self._SendRequest(HTTP_POST,
1225 ("/%s/instances/%s/reinstall" %
1226 (GANETI_RAPI_VERSION, instance)), query, body)
1227
1228
1229 if osparams:
1230 raise GanetiApiError("Server does not support specifying OS parameters"
1231 " for instance reinstallation")
1232
1233 query = []
1234 _AppendIf(query, os, ("os", os))
1235 _AppendIf(query, no_startup, ("nostartup", 1))
1236
1237 return self._SendRequest(HTTP_POST,
1238 ("/%s/instances/%s/reinstall" %
1239 (GANETI_RAPI_VERSION, instance)), query, None)
1240
1244 """Replaces disks on an instance.
1245
1246 @type instance: str
1247 @param instance: instance whose disks to replace
1248 @type disks: list of ints
1249 @param disks: Indexes of disks to replace
1250 @type mode: str
1251 @param mode: replacement mode to use (defaults to replace_auto)
1252 @type remote_node: str or None
1253 @param remote_node: new secondary node to use (for use with
1254 replace_new_secondary mode)
1255 @type iallocator: str or None
1256 @param iallocator: instance allocator plugin to use (for use with
1257 replace_auto mode)
1258 @type reason: string
1259 @param reason: the reason for executing this operation
1260 @type early_release: bool
1261 @param early_release: whether to release locks as soon as possible
1262
1263 @rtype: string
1264 @return: job id
1265
1266 """
1267 query = [
1268 ("mode", mode),
1269 ]
1270
1271
1272
1273 if disks is not None:
1274 _AppendIf(query, True,
1275 ("disks", ",".join(str(idx) for idx in disks)))
1276
1277 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1278 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1279 _AppendReason(query, reason)
1280 _AppendIf(query, early_release is not None,
1281 ("early_release", early_release))
1282
1283 return self._SendRequest(HTTP_POST,
1284 ("/%s/instances/%s/replace-disks" %
1285 (GANETI_RAPI_VERSION, instance)), query, None)
1286
1288 """Prepares an instance for an export.
1289
1290 @type instance: string
1291 @param instance: Instance name
1292 @type mode: string
1293 @param mode: Export mode
1294 @type reason: string
1295 @param reason: the reason for executing this operation
1296 @rtype: string
1297 @return: Job ID
1298
1299 """
1300 query = [("mode", mode)]
1301 _AppendReason(query, reason)
1302 return self._SendRequest(HTTP_PUT,
1303 ("/%s/instances/%s/prepare-export" %
1304 (GANETI_RAPI_VERSION, instance)), query, None)
1305
1306 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1307 remove_instance=None, x509_key_name=None,
1308 destination_x509_ca=None, compress=None, reason=None):
1309 """Exports an instance.
1310
1311 @type instance: string
1312 @param instance: Instance name
1313 @type mode: string
1314 @param mode: Export mode
1315 @type reason: string
1316 @param reason: the reason for executing this operation
1317 @rtype: string
1318 @return: Job ID
1319
1320 """
1321 body = {
1322 "destination": destination,
1323 "mode": mode,
1324 }
1325
1326 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1327 _SetItemIf(body, remove_instance is not None,
1328 "remove_instance", remove_instance)
1329 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1330 _SetItemIf(body, destination_x509_ca is not None,
1331 "destination_x509_ca", destination_x509_ca)
1332 _SetItemIf(body, compress is not None, "compress", compress)
1333
1334 query = []
1335 _AppendReason(query, reason)
1336
1337 return self._SendRequest(HTTP_PUT,
1338 ("/%s/instances/%s/export" %
1339 (GANETI_RAPI_VERSION, instance)), query, body)
1340
1341 - def MigrateInstance(self, instance, mode=None, cleanup=None,
1342 target_node=None, reason=None):
1343 """Migrates an instance.
1344
1345 @type instance: string
1346 @param instance: Instance name
1347 @type mode: string
1348 @param mode: Migration mode
1349 @type cleanup: bool
1350 @param cleanup: Whether to clean up a previously failed migration
1351 @type target_node: string
1352 @param target_node: Target Node for externally mirrored instances
1353 @type reason: string
1354 @param reason: the reason for executing this operation
1355 @rtype: string
1356 @return: job id
1357
1358 """
1359 body = {}
1360 _SetItemIf(body, mode is not None, "mode", mode)
1361 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1362 _SetItemIf(body, target_node is not None, "target_node", target_node)
1363
1364 query = []
1365 _AppendReason(query, reason)
1366
1367 return self._SendRequest(HTTP_PUT,
1368 ("/%s/instances/%s/migrate" %
1369 (GANETI_RAPI_VERSION, instance)), query, body)
1370
1371 - def FailoverInstance(self, instance, iallocator=None,
1372 ignore_consistency=None, target_node=None, reason=None):
1373 """Does a failover of an instance.
1374
1375 @type instance: string
1376 @param instance: Instance name
1377 @type iallocator: string
1378 @param iallocator: Iallocator for deciding the target node for
1379 shared-storage instances
1380 @type ignore_consistency: bool
1381 @param ignore_consistency: Whether to ignore disk consistency
1382 @type target_node: string
1383 @param target_node: Target node for shared-storage instances
1384 @type reason: string
1385 @param reason: the reason for executing this operation
1386 @rtype: string
1387 @return: job id
1388
1389 """
1390 body = {}
1391 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1392 _SetItemIf(body, ignore_consistency is not None,
1393 "ignore_consistency", ignore_consistency)
1394 _SetItemIf(body, target_node is not None, "target_node", target_node)
1395
1396 query = []
1397 _AppendReason(query, reason)
1398
1399 return self._SendRequest(HTTP_PUT,
1400 ("/%s/instances/%s/failover" %
1401 (GANETI_RAPI_VERSION, instance)), query, body)
1402
1403 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1404 reason=None):
1405 """Changes the name of an instance.
1406
1407 @type instance: string
1408 @param instance: Instance name
1409 @type new_name: string
1410 @param new_name: New instance name
1411 @type ip_check: bool
1412 @param ip_check: Whether to ensure instance's IP address is inactive
1413 @type name_check: bool
1414 @param name_check: Whether to ensure instance's name is resolvable
1415 @type reason: string
1416 @param reason: the reason for executing this operation
1417 @rtype: string
1418 @return: job id
1419
1420 """
1421 body = {
1422 "new_name": new_name,
1423 }
1424
1425 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1426 _SetItemIf(body, name_check is not None, "name_check", name_check)
1427
1428 query = []
1429 _AppendReason(query, reason)
1430
1431 return self._SendRequest(HTTP_PUT,
1432 ("/%s/instances/%s/rename" %
1433 (GANETI_RAPI_VERSION, instance)), query, body)
1434
1436 """Request information for connecting to instance's console.
1437
1438 @type instance: string
1439 @param instance: Instance name
1440 @type reason: string
1441 @param reason: the reason for executing this operation
1442 @rtype: dict
1443 @return: dictionary containing information about instance's console
1444
1445 """
1446 query = []
1447 _AppendReason(query, reason)
1448 return self._SendRequest(HTTP_GET,
1449 ("/%s/instances/%s/console" %
1450 (GANETI_RAPI_VERSION, instance)), query, None)
1451
1453 """Gets all jobs for the cluster.
1454
1455 @type bulk: bool
1456 @param bulk: Whether to return detailed information about jobs.
1457 @rtype: list of int
1458 @return: List of job ids for the cluster or list of dicts with detailed
1459 information about the jobs if bulk parameter was true.
1460
1461 """
1462 query = []
1463 _AppendIf(query, bulk, ("bulk", 1))
1464
1465 if bulk:
1466 return self._SendRequest(HTTP_GET,
1467 "/%s/jobs" % GANETI_RAPI_VERSION,
1468 query, None)
1469 else:
1470 return [int(j["id"])
1471 for j in self._SendRequest(HTTP_GET,
1472 "/%s/jobs" % GANETI_RAPI_VERSION,
1473 None, None)]
1474
1476 """Gets the status of a job.
1477
1478 @type job_id: string
1479 @param job_id: job id whose status to query
1480
1481 @rtype: dict
1482 @return: job status
1483
1484 """
1485 return self._SendRequest(HTTP_GET,
1486 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1487 None, None)
1488
1490 """Polls cluster for job status until completion.
1491
1492 Completion is defined as any of the following states listed in
1493 L{JOB_STATUS_FINALIZED}.
1494
1495 @type job_id: string
1496 @param job_id: job id to watch
1497 @type period: int
1498 @param period: how often to poll for status (optional, default 5s)
1499 @type retries: int
1500 @param retries: how many time to poll before giving up
1501 (optional, default -1 means unlimited)
1502
1503 @rtype: bool
1504 @return: C{True} if job succeeded or C{False} if failed/status timeout
1505 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1506 possible; L{WaitForJobChange} returns immediately after a job changed and
1507 does not use polling
1508
1509 """
1510 while retries != 0:
1511 job_result = self.GetJobStatus(job_id)
1512
1513 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1514 return True
1515 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1516 return False
1517
1518 if period:
1519 time.sleep(period)
1520
1521 if retries > 0:
1522 retries -= 1
1523
1524 return False
1525
1527 """Waits for job changes.
1528
1529 @type job_id: string
1530 @param job_id: Job ID for which to wait
1531 @return: C{None} if no changes have been detected and a dict with two keys,
1532 C{job_info} and C{log_entries} otherwise.
1533 @rtype: dict
1534
1535 """
1536 body = {
1537 "fields": fields,
1538 "previous_job_info": prev_job_info,
1539 "previous_log_serial": prev_log_serial,
1540 }
1541
1542 return self._SendRequest(HTTP_GET,
1543 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1544 None, body)
1545
1546 - def CancelJob(self, job_id, dry_run=False):
1547 """Cancels a job.
1548
1549 @type job_id: string
1550 @param job_id: id of the job to delete
1551 @type dry_run: bool
1552 @param dry_run: whether to perform a dry run
1553 @rtype: tuple
1554 @return: tuple containing the result, and a message (bool, string)
1555
1556 """
1557 query = []
1558 _AppendDryRunIf(query, dry_run)
1559
1560 return self._SendRequest(HTTP_DELETE,
1561 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1562 query, None)
1563
1564 - def GetNodes(self, bulk=False, reason=None):
1565 """Gets all nodes in the cluster.
1566
1567 @type bulk: bool
1568 @param bulk: whether to return all information about all instances
1569 @type reason: string
1570 @param reason: the reason for executing this operation
1571
1572 @rtype: list of dict or str
1573 @return: if bulk is true, info about nodes in the cluster,
1574 else list of nodes in the cluster
1575
1576 """
1577 query = []
1578 _AppendIf(query, bulk, ("bulk", 1))
1579 _AppendReason(query, reason)
1580
1581 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1582 query, None)
1583 if bulk:
1584 return nodes
1585 else:
1586 return [n["id"] for n in nodes]
1587
1588 - def GetNode(self, node, reason=None):
1589 """Gets information about a node.
1590
1591 @type node: str
1592 @param node: node whose info to return
1593 @type reason: string
1594 @param reason: the reason for executing this operation
1595
1596 @rtype: dict
1597 @return: info about the node
1598
1599 """
1600 query = []
1601 _AppendReason(query, reason)
1602
1603 return self._SendRequest(HTTP_GET,
1604 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1605 query, None)
1606
1607 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1608 dry_run=False, early_release=None,
1609 mode=None, accept_old=False, reason=None):
1610 """Evacuates instances from a Ganeti node.
1611
1612 @type node: str
1613 @param node: node to evacuate
1614 @type iallocator: str or None
1615 @param iallocator: instance allocator to use
1616 @type remote_node: str
1617 @param remote_node: node to evaucate to
1618 @type dry_run: bool
1619 @param dry_run: whether to perform a dry run
1620 @type early_release: bool
1621 @param early_release: whether to enable parallelization
1622 @type mode: string
1623 @param mode: Node evacuation mode
1624 @type accept_old: bool
1625 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1626 results
1627 @type reason: string
1628 @param reason: the reason for executing this operation
1629
1630 @rtype: string, or a list for pre-2.5 results
1631 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1632 list of (job ID, instance name, new secondary node); if dry_run was
1633 specified, then the actual move jobs were not submitted and the job IDs
1634 will be C{None}
1635
1636 @raises GanetiApiError: if an iallocator and remote_node are both
1637 specified
1638
1639 """
1640 if iallocator and remote_node:
1641 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1642
1643 query = []
1644 _AppendDryRunIf(query, dry_run)
1645 _AppendReason(query, reason)
1646
1647 if _NODE_EVAC_RES1 in self.GetFeatures():
1648
1649 body = {}
1650
1651 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1652 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1653 _SetItemIf(body, early_release is not None,
1654 "early_release", early_release)
1655 _SetItemIf(body, mode is not None, "mode", mode)
1656 else:
1657
1658 body = None
1659
1660 if not accept_old:
1661 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1662 " not accept old-style results (parameter"
1663 " accept_old)")
1664
1665
1666 if mode is not None and mode != NODE_EVAC_SEC:
1667 raise GanetiApiError("Server can only evacuate secondary instances")
1668
1669 _AppendIf(query, iallocator, ("iallocator", iallocator))
1670 _AppendIf(query, remote_node, ("remote_node", remote_node))
1671 _AppendIf(query, early_release, ("early_release", 1))
1672
1673 return self._SendRequest(HTTP_POST,
1674 ("/%s/nodes/%s/evacuate" %
1675 (GANETI_RAPI_VERSION, node)), query, body)
1676
1677 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1678 target_node=None, reason=None):
1679 """Migrates all primary instances from a node.
1680
1681 @type node: str
1682 @param node: node to migrate
1683 @type mode: string
1684 @param mode: if passed, it will overwrite the live migration type,
1685 otherwise the hypervisor default will be used
1686 @type dry_run: bool
1687 @param dry_run: whether to perform a dry run
1688 @type iallocator: string
1689 @param iallocator: instance allocator to use
1690 @type target_node: string
1691 @param target_node: Target node for shared-storage instances
1692 @type reason: string
1693 @param reason: the reason for executing this operation
1694
1695 @rtype: string
1696 @return: job id
1697
1698 """
1699 query = []
1700 _AppendDryRunIf(query, dry_run)
1701 _AppendReason(query, reason)
1702
1703 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1704 body = {}
1705
1706 _SetItemIf(body, mode is not None, "mode", mode)
1707 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1708 _SetItemIf(body, target_node is not None, "target_node", target_node)
1709
1710 assert len(query) <= 1
1711
1712 return self._SendRequest(HTTP_POST,
1713 ("/%s/nodes/%s/migrate" %
1714 (GANETI_RAPI_VERSION, node)), query, body)
1715 else:
1716
1717 if target_node is not None:
1718 raise GanetiApiError("Server does not support specifying target node"
1719 " for node migration")
1720
1721 _AppendIf(query, mode is not None, ("mode", mode))
1722
1723 return self._SendRequest(HTTP_POST,
1724 ("/%s/nodes/%s/migrate" %
1725 (GANETI_RAPI_VERSION, node)), query, None)
1726
1728 """Gets the current role for a node.
1729
1730 @type node: str
1731 @param node: node whose role to return
1732 @type reason: string
1733 @param reason: the reason for executing this operation
1734
1735 @rtype: str
1736 @return: the current role for a node
1737
1738 """
1739 query = []
1740 _AppendReason(query, reason)
1741
1742 return self._SendRequest(HTTP_GET,
1743 ("/%s/nodes/%s/role" %
1744 (GANETI_RAPI_VERSION, node)), query, None)
1745
1746 - def SetNodeRole(self, node, role, force=False, auto_promote=None,
1747 reason=None):
1748 """Sets the role for a node.
1749
1750 @type node: str
1751 @param node: the node whose role to set
1752 @type role: str
1753 @param role: the role to set for the node
1754 @type force: bool
1755 @param force: whether to force the role change
1756 @type auto_promote: bool
1757 @param auto_promote: Whether node(s) should be promoted to master candidate
1758 if necessary
1759 @type reason: string
1760 @param reason: the reason for executing this operation
1761
1762 @rtype: string
1763 @return: job id
1764
1765 """
1766 query = []
1767 _AppendForceIf(query, force)
1768 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1769 _AppendReason(query, reason)
1770
1771 return self._SendRequest(HTTP_PUT,
1772 ("/%s/nodes/%s/role" %
1773 (GANETI_RAPI_VERSION, node)), query, role)
1774
1776 """Powercycles a node.
1777
1778 @type node: string
1779 @param node: Node name
1780 @type force: bool
1781 @param force: Whether to force the operation
1782 @type reason: string
1783 @param reason: the reason for executing this operation
1784 @rtype: string
1785 @return: job id
1786
1787 """
1788 query = []
1789 _AppendForceIf(query, force)
1790 _AppendReason(query, reason)
1791
1792 return self._SendRequest(HTTP_POST,
1793 ("/%s/nodes/%s/powercycle" %
1794 (GANETI_RAPI_VERSION, node)), query, None)
1795
1796 - def ModifyNode(self, node, reason=None, **kwargs):
1797 """Modifies a node.
1798
1799 More details for parameters can be found in the RAPI documentation.
1800
1801 @type node: string
1802 @param node: Node name
1803 @type reason: string
1804 @param reason: the reason for executing this operation
1805 @rtype: string
1806 @return: job id
1807
1808 """
1809 query = []
1810 _AppendReason(query, reason)
1811
1812 return self._SendRequest(HTTP_POST,
1813 ("/%s/nodes/%s/modify" %
1814 (GANETI_RAPI_VERSION, node)), query, kwargs)
1815
1817 """Gets the storage units for a node.
1818
1819 @type node: str
1820 @param node: the node whose storage units to return
1821 @type storage_type: str
1822 @param storage_type: storage type whose units to return
1823 @type output_fields: str
1824 @param output_fields: storage type fields to return
1825 @type reason: string
1826 @param reason: the reason for executing this operation
1827
1828 @rtype: string
1829 @return: job id where results can be retrieved
1830
1831 """
1832 query = [
1833 ("storage_type", storage_type),
1834 ("output_fields", output_fields),
1835 ]
1836 _AppendReason(query, reason)
1837
1838 return self._SendRequest(HTTP_GET,
1839 ("/%s/nodes/%s/storage" %
1840 (GANETI_RAPI_VERSION, node)), query, None)
1841
1844 """Modifies parameters of storage units on the node.
1845
1846 @type node: str
1847 @param node: node whose storage units to modify
1848 @type storage_type: str
1849 @param storage_type: storage type whose units to modify
1850 @type name: str
1851 @param name: name of the storage unit
1852 @type allocatable: bool or None
1853 @param allocatable: Whether to set the "allocatable" flag on the storage
1854 unit (None=no modification, True=set, False=unset)
1855 @type reason: string
1856 @param reason: the reason for executing this operation
1857
1858 @rtype: string
1859 @return: job id
1860
1861 """
1862 query = [
1863 ("storage_type", storage_type),
1864 ("name", name),
1865 ]
1866
1867 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1868 _AppendReason(query, reason)
1869
1870 return self._SendRequest(HTTP_PUT,
1871 ("/%s/nodes/%s/storage/modify" %
1872 (GANETI_RAPI_VERSION, node)), query, None)
1873
1875 """Repairs a storage unit on the node.
1876
1877 @type node: str
1878 @param node: node whose storage units to repair
1879 @type storage_type: str
1880 @param storage_type: storage type to repair
1881 @type name: str
1882 @param name: name of the storage unit to repair
1883 @type reason: string
1884 @param reason: the reason for executing this operation
1885
1886 @rtype: string
1887 @return: job id
1888
1889 """
1890 query = [
1891 ("storage_type", storage_type),
1892 ("name", name),
1893 ]
1894 _AppendReason(query, reason)
1895
1896 return self._SendRequest(HTTP_PUT,
1897 ("/%s/nodes/%s/storage/repair" %
1898 (GANETI_RAPI_VERSION, node)), query, None)
1899
1918
1942
1966
1968 """Gets all networks in the cluster.
1969
1970 @type bulk: bool
1971 @param bulk: whether to return all information about the networks
1972
1973 @rtype: list of dict or str
1974 @return: if bulk is true, a list of dictionaries with info about all
1975 networks in the cluster, else a list of names of those networks
1976
1977 """
1978 query = []
1979 _AppendIf(query, bulk, ("bulk", 1))
1980 _AppendReason(query, reason)
1981
1982 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1983 query, None)
1984 if bulk:
1985 return networks
1986 else:
1987 return [n["name"] for n in networks]
1988
1990 """Gets information about a network.
1991
1992 @type network: str
1993 @param network: name of the network whose info to return
1994 @type reason: string
1995 @param reason: the reason for executing this operation
1996
1997 @rtype: dict
1998 @return: info about the network
1999
2000 """
2001 query = []
2002 _AppendReason(query, reason)
2003
2004 return self._SendRequest(HTTP_GET,
2005 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
2006 query, None)
2007
2008 - def CreateNetwork(self, network_name, network, gateway=None, network6=None,
2009 gateway6=None, mac_prefix=None,
2010 add_reserved_ips=None, tags=None, dry_run=False,
2011 reason=None):
2012 """Creates a new network.
2013
2014 @type network_name: str
2015 @param network_name: the name of network to create
2016 @type dry_run: bool
2017 @param dry_run: whether to peform a dry run
2018 @type reason: string
2019 @param reason: the reason for executing this operation
2020
2021 @rtype: string
2022 @return: job id
2023
2024 """
2025 query = []
2026 _AppendDryRunIf(query, dry_run)
2027 _AppendReason(query, reason)
2028
2029 if add_reserved_ips:
2030 add_reserved_ips = add_reserved_ips.split(",")
2031
2032 if tags:
2033 tags = tags.split(",")
2034
2035 body = {
2036 "network_name": network_name,
2037 "gateway": gateway,
2038 "network": network,
2039 "gateway6": gateway6,
2040 "network6": network6,
2041 "mac_prefix": mac_prefix,
2042 "add_reserved_ips": add_reserved_ips,
2043 "tags": tags,
2044 }
2045
2046 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2047 query, body)
2048
2049 - def ConnectNetwork(self, network_name, group_name, mode, link,
2050 vlan="", dry_run=False, reason=None):
2051 """Connects a Network to a NodeGroup with the given netparams
2052
2053 """
2054 body = {
2055 "group_name": group_name,
2056 "network_mode": mode,
2057 "network_link": link,
2058 "network_vlan": vlan,
2059 }
2060
2061 query = []
2062 _AppendDryRunIf(query, dry_run)
2063 _AppendReason(query, reason)
2064
2065 return self._SendRequest(HTTP_PUT,
2066 ("/%s/networks/%s/connect" %
2067 (GANETI_RAPI_VERSION, network_name)), query, body)
2068
2069 - def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2070 reason=None):
2085
2087 """Modifies a network.
2088
2089 More details for parameters can be found in the RAPI documentation.
2090
2091 @type network: string
2092 @param network: Network name
2093 @type reason: string
2094 @param reason: the reason for executing this operation
2095 @rtype: string
2096 @return: job id
2097
2098 """
2099 query = []
2100 _AppendReason(query, reason)
2101
2102 return self._SendRequest(HTTP_PUT,
2103 ("/%s/networks/%s/modify" %
2104 (GANETI_RAPI_VERSION, network)), None, kwargs)
2105
2107 """Deletes a network.
2108
2109 @type network: str
2110 @param network: the network to delete
2111 @type dry_run: bool
2112 @param dry_run: whether to peform a dry run
2113 @type reason: string
2114 @param reason: the reason for executing this operation
2115
2116 @rtype: string
2117 @return: job id
2118
2119 """
2120 query = []
2121 _AppendDryRunIf(query, dry_run)
2122 _AppendReason(query, reason)
2123
2124 return self._SendRequest(HTTP_DELETE,
2125 ("/%s/networks/%s" %
2126 (GANETI_RAPI_VERSION, network)), query, None)
2127
2146
2170
2193
2194 - def GetGroups(self, bulk=False, reason=None):
2195 """Gets all node groups in the cluster.
2196
2197 @type bulk: bool
2198 @param bulk: whether to return all information about the groups
2199 @type reason: string
2200 @param reason: the reason for executing this operation
2201
2202 @rtype: list of dict or str
2203 @return: if bulk is true, a list of dictionaries with info about all node
2204 groups in the cluster, else a list of names of those node groups
2205
2206 """
2207 query = []
2208 _AppendIf(query, bulk, ("bulk", 1))
2209 _AppendReason(query, reason)
2210
2211 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2212 query, None)
2213 if bulk:
2214 return groups
2215 else:
2216 return [g["name"] for g in groups]
2217
2218 - def GetGroup(self, group, reason=None):
2219 """Gets information about a node group.
2220
2221 @type group: str
2222 @param group: name of the node group whose info to return
2223 @type reason: string
2224 @param reason: the reason for executing this operation
2225
2226 @rtype: dict
2227 @return: info about the node group
2228
2229 """
2230 query = []
2231 _AppendReason(query, reason)
2232
2233 return self._SendRequest(HTTP_GET,
2234 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2235 query, None)
2236
2237 - def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2238 """Creates a new node group.
2239
2240 @type name: str
2241 @param name: the name of node group to create
2242 @type alloc_policy: str
2243 @param alloc_policy: the desired allocation policy for the group, if any
2244 @type dry_run: bool
2245 @param dry_run: whether to peform a dry run
2246 @type reason: string
2247 @param reason: the reason for executing this operation
2248
2249 @rtype: string
2250 @return: job id
2251
2252 """
2253 query = []
2254 _AppendDryRunIf(query, dry_run)
2255 _AppendReason(query, reason)
2256
2257 body = {
2258 "name": name,
2259 "alloc_policy": alloc_policy,
2260 }
2261
2262 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2263 query, body)
2264
2266 """Modifies a node group.
2267
2268 More details for parameters can be found in the RAPI documentation.
2269
2270 @type group: string
2271 @param group: Node group name
2272 @type reason: string
2273 @param reason: the reason for executing this operation
2274 @rtype: string
2275 @return: job id
2276
2277 """
2278 query = []
2279 _AppendReason(query, reason)
2280
2281 return self._SendRequest(HTTP_PUT,
2282 ("/%s/groups/%s/modify" %
2283 (GANETI_RAPI_VERSION, group)), query, kwargs)
2284
2285 - def DeleteGroup(self, group, dry_run=False, reason=None):
2286 """Deletes a node group.
2287
2288 @type group: str
2289 @param group: the node group to delete
2290 @type dry_run: bool
2291 @param dry_run: whether to peform a dry run
2292 @type reason: string
2293 @param reason: the reason for executing this operation
2294
2295 @rtype: string
2296 @return: job id
2297
2298 """
2299 query = []
2300 _AppendDryRunIf(query, dry_run)
2301 _AppendReason(query, reason)
2302
2303 return self._SendRequest(HTTP_DELETE,
2304 ("/%s/groups/%s" %
2305 (GANETI_RAPI_VERSION, group)), query, None)
2306
2308 """Changes the name of a node group.
2309
2310 @type group: string
2311 @param group: Node group name
2312 @type new_name: string
2313 @param new_name: New node group name
2314 @type reason: string
2315 @param reason: the reason for executing this operation
2316
2317 @rtype: string
2318 @return: job id
2319
2320 """
2321 body = {
2322 "new_name": new_name,
2323 }
2324
2325 query = []
2326 _AppendReason(query, reason)
2327
2328 return self._SendRequest(HTTP_PUT,
2329 ("/%s/groups/%s/rename" %
2330 (GANETI_RAPI_VERSION, group)), query, body)
2331
2332 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2333 reason=None):
2334 """Assigns nodes to a group.
2335
2336 @type group: string
2337 @param group: Node group name
2338 @type nodes: list of strings
2339 @param nodes: List of nodes to assign to the group
2340 @type reason: string
2341 @param reason: the reason for executing this operation
2342
2343 @rtype: string
2344 @return: job id
2345
2346 """
2347 query = []
2348 _AppendForceIf(query, force)
2349 _AppendDryRunIf(query, dry_run)
2350 _AppendReason(query, reason)
2351
2352 body = {
2353 "nodes": nodes,
2354 }
2355
2356 return self._SendRequest(HTTP_PUT,
2357 ("/%s/groups/%s/assign-nodes" %
2358 (GANETI_RAPI_VERSION, group)), query, body)
2359
2378
2402
2425
2426 - def Query(self, what, fields, qfilter=None, reason=None):
2427 """Retrieves information about resources.
2428
2429 @type what: string
2430 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2431 @type fields: list of string
2432 @param fields: Requested fields
2433 @type qfilter: None or list
2434 @param qfilter: Query filter
2435 @type reason: string
2436 @param reason: the reason for executing this operation
2437
2438 @rtype: string
2439 @return: job id
2440
2441 """
2442 query = []
2443 _AppendReason(query, reason)
2444
2445 body = {
2446 "fields": fields,
2447 }
2448
2449 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2450
2451 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2452
2453 return self._SendRequest(HTTP_PUT,
2454 ("/%s/query/%s" %
2455 (GANETI_RAPI_VERSION, what)), query, body)
2456
2457 - def QueryFields(self, what, fields=None, reason=None):
2458 """Retrieves available fields for a resource.
2459
2460 @type what: string
2461 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2462 @type fields: list of string
2463 @param fields: Requested fields
2464 @type reason: string
2465 @param reason: the reason for executing this operation
2466
2467 @rtype: string
2468 @return: job id
2469
2470 """
2471 query = []
2472 _AppendReason(query, reason)
2473
2474 if fields is not None:
2475 _AppendIf(query, True, ("fields", ",".join(fields)))
2476
2477 return self._SendRequest(HTTP_GET,
2478 ("/%s/query/%s/fields" %
2479 (GANETI_RAPI_VERSION, what)), query, None)
2480
2482 """Gets all filter rules in the cluster.
2483
2484 @type bulk: bool
2485 @param bulk: whether to return all information about the networks
2486
2487 @rtype: list of dict or str
2488 @return: if bulk is true, a list of dictionaries with info about all
2489 filter rules in the cluster, else a list of UUIDs of those
2490 filters
2491
2492 """
2493 query = []
2494 _AppendIf(query, bulk, ("bulk", 1))
2495
2496 filters = self._SendRequest(HTTP_GET, "/%s/filters" % GANETI_RAPI_VERSION,
2497 query, None)
2498 if bulk:
2499 return filters
2500 else:
2501 return [f["uuid"] for f in filters]
2502
2504 """Gets information about a filter rule.
2505
2506 @type filter_uuid: str
2507 @param filter_uuid: UUID of the filter whose info to return
2508
2509 @rtype: dict
2510 @return: info about the filter
2511
2512 """
2513 query = []
2514
2515 return self._SendRequest(HTTP_GET,
2516 "/%s/filters/%s" % (GANETI_RAPI_VERSION,
2517 filter_uuid),
2518 query, None)
2519
2520 - def AddFilter(self, priority, predicates, action, reason_trail=None):
2521 """Adds a filter rule
2522
2523 @type reason_trail: list of (str, str, int) triples
2524 @param reason_trail: the reason trail for executing this operation,
2525 or None
2526
2527 @rtype: string
2528 @return: filter UUID of the added filter
2529
2530 """
2531 if reason_trail is None:
2532 reason_trail = []
2533
2534 assert isinstance(reason_trail, list)
2535
2536 reason_trail.append(("gnt:client", "", EpochNano(),))
2537
2538 body = {
2539 "priority": priority,
2540 "predicates": predicates,
2541 "action": action,
2542 "reason": reason_trail,
2543 }
2544
2545 query = []
2546
2547 return self._SendRequest(HTTP_POST,
2548 ("/%s/filters" % (GANETI_RAPI_VERSION)),
2549 query, body)
2550
2551 - def ReplaceFilter(self, uuid, priority, predicates, action,
2552 reason_trail=None):
2553 """Replaces a filter rule, or creates one if it doesn't already exist
2554
2555 @type reason_trail: list of (str, str, int) triples
2556 @param reason_trail: the reason trail for executing this operation,
2557 or None
2558
2559 @rtype: string
2560 @return: filter UUID of the replaced/added filter
2561
2562 """
2563 if reason_trail is None:
2564 reason_trail = []
2565
2566 assert isinstance(reason_trail, list)
2567
2568 reason_trail.append(("gnt:client", "", EpochNano(),))
2569
2570 body = {
2571 "priority": priority,
2572 "predicates": predicates,
2573 "action": action,
2574 "reason": reason_trail,
2575 }
2576
2577 query = []
2578
2579 return self._SendRequest(HTTP_PUT,
2580 ("/%s/filters/%s" % (GANETI_RAPI_VERSION, uuid)),
2581 query, body)
2582
2595