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
968 """Deactivates an instance's disks.
969
970 @type instance: string
971 @param instance: Instance name
972 @type reason: string
973 @param reason: the reason for executing this operation
974 @rtype: string
975 @return: job id
976
977 """
978 query = []
979 _AppendReason(query, reason)
980 return self._SendRequest(HTTP_PUT,
981 ("/%s/instances/%s/deactivate-disks" %
982 (GANETI_RAPI_VERSION, instance)), query, None)
983
986 """Recreate an instance's disks.
987
988 @type instance: string
989 @param instance: Instance name
990 @type disks: list of int
991 @param disks: List of disk indexes
992 @type nodes: list of string
993 @param nodes: New instance nodes, if relocation is desired
994 @type reason: string
995 @param reason: the reason for executing this operation
996 @type iallocator: str or None
997 @param iallocator: instance allocator plugin to use
998 @rtype: string
999 @return: job id
1000
1001 """
1002 body = {}
1003 _SetItemIf(body, disks is not None, "disks", disks)
1004 _SetItemIf(body, nodes is not None, "nodes", nodes)
1005 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1006
1007 query = []
1008 _AppendReason(query, reason)
1009
1010 return self._SendRequest(HTTP_POST,
1011 ("/%s/instances/%s/recreate-disks" %
1012 (GANETI_RAPI_VERSION, instance)), query, body)
1013
1014 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
1015 reason=None):
1016 """Grows a disk of an instance.
1017
1018 More details for parameters can be found in the RAPI documentation.
1019
1020 @type instance: string
1021 @param instance: Instance name
1022 @type disk: integer
1023 @param disk: Disk index
1024 @type amount: integer
1025 @param amount: Grow disk by this amount (MiB)
1026 @type wait_for_sync: bool
1027 @param wait_for_sync: Wait for disk to synchronize
1028 @type reason: string
1029 @param reason: the reason for executing this operation
1030 @rtype: string
1031 @return: job id
1032
1033 """
1034 body = {
1035 "amount": amount,
1036 }
1037
1038 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1039
1040 query = []
1041 _AppendReason(query, reason)
1042
1043 return self._SendRequest(HTTP_POST,
1044 ("/%s/instances/%s/disk/%s/grow" %
1045 (GANETI_RAPI_VERSION, instance, disk)),
1046 query, body)
1047
1065
1089
1112
1113 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1114 dry_run=False, reason=None, **kwargs):
1115 """Reboots an instance.
1116
1117 @type instance: str
1118 @param instance: instance to reboot
1119 @type reboot_type: str
1120 @param reboot_type: one of: hard, soft, full
1121 @type ignore_secondaries: bool
1122 @param ignore_secondaries: if True, ignores errors for the secondary node
1123 while re-assembling disks (in hard-reboot mode only)
1124 @type dry_run: bool
1125 @param dry_run: whether to perform a dry run
1126 @type reason: string
1127 @param reason: the reason for the reboot
1128 @rtype: string
1129 @return: job id
1130
1131 """
1132 query = []
1133 body = kwargs
1134
1135 _AppendDryRunIf(query, dry_run)
1136 _AppendIf(query, reboot_type, ("type", reboot_type))
1137 _AppendIf(query, ignore_secondaries is not None,
1138 ("ignore_secondaries", ignore_secondaries))
1139 _AppendReason(query, reason)
1140
1141 return self._SendRequest(HTTP_POST,
1142 ("/%s/instances/%s/reboot" %
1143 (GANETI_RAPI_VERSION, instance)), query, body)
1144
1145 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1146 reason=None, **kwargs):
1147 """Shuts down an instance.
1148
1149 @type instance: str
1150 @param instance: the instance to shut down
1151 @type dry_run: bool
1152 @param dry_run: whether to perform a dry run
1153 @type no_remember: bool
1154 @param no_remember: if true, will not record the state change
1155 @type reason: string
1156 @param reason: the reason for the shutdown
1157 @rtype: string
1158 @return: job id
1159
1160 """
1161 query = []
1162 body = kwargs
1163
1164 _AppendDryRunIf(query, dry_run)
1165 _AppendIf(query, no_remember, ("no_remember", 1))
1166 _AppendReason(query, reason)
1167
1168 return self._SendRequest(HTTP_PUT,
1169 ("/%s/instances/%s/shutdown" %
1170 (GANETI_RAPI_VERSION, instance)), query, body)
1171
1172 - def StartupInstance(self, instance, dry_run=False, no_remember=False,
1173 reason=None):
1174 """Starts up an instance.
1175
1176 @type instance: str
1177 @param instance: the instance to start up
1178 @type dry_run: bool
1179 @param dry_run: whether to perform a dry run
1180 @type no_remember: bool
1181 @param no_remember: if true, will not record the state change
1182 @type reason: string
1183 @param reason: the reason for the startup
1184 @rtype: string
1185 @return: job id
1186
1187 """
1188 query = []
1189 _AppendDryRunIf(query, dry_run)
1190 _AppendIf(query, no_remember, ("no_remember", 1))
1191 _AppendReason(query, reason)
1192
1193 return self._SendRequest(HTTP_PUT,
1194 ("/%s/instances/%s/startup" %
1195 (GANETI_RAPI_VERSION, instance)), query, None)
1196
1197 - def ReinstallInstance(self, instance, os=None, no_startup=False,
1198 osparams=None, reason=None):
1199 """Reinstalls an instance.
1200
1201 @type instance: str
1202 @param instance: The instance to reinstall
1203 @type os: str or None
1204 @param os: The operating system to reinstall. If None, the instance's
1205 current operating system will be installed again
1206 @type no_startup: bool
1207 @param no_startup: Whether to start the instance automatically
1208 @type reason: string
1209 @param reason: the reason for executing this operation
1210 @rtype: string
1211 @return: job id
1212
1213 """
1214 query = []
1215 _AppendReason(query, reason)
1216
1217 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1218 body = {
1219 "start": not no_startup,
1220 }
1221 _SetItemIf(body, os is not None, "os", os)
1222 _SetItemIf(body, osparams is not None, "osparams", osparams)
1223 return self._SendRequest(HTTP_POST,
1224 ("/%s/instances/%s/reinstall" %
1225 (GANETI_RAPI_VERSION, instance)), query, body)
1226
1227
1228 if osparams:
1229 raise GanetiApiError("Server does not support specifying OS parameters"
1230 " for instance reinstallation")
1231
1232 query = []
1233 _AppendIf(query, os, ("os", os))
1234 _AppendIf(query, no_startup, ("nostartup", 1))
1235
1236 return self._SendRequest(HTTP_POST,
1237 ("/%s/instances/%s/reinstall" %
1238 (GANETI_RAPI_VERSION, instance)), query, None)
1239
1243 """Replaces disks on an instance.
1244
1245 @type instance: str
1246 @param instance: instance whose disks to replace
1247 @type disks: list of ints
1248 @param disks: Indexes of disks to replace
1249 @type mode: str
1250 @param mode: replacement mode to use (defaults to replace_auto)
1251 @type remote_node: str or None
1252 @param remote_node: new secondary node to use (for use with
1253 replace_new_secondary mode)
1254 @type iallocator: str or None
1255 @param iallocator: instance allocator plugin to use (for use with
1256 replace_auto mode)
1257 @type reason: string
1258 @param reason: the reason for executing this operation
1259 @type early_release: bool
1260 @param early_release: whether to release locks as soon as possible
1261
1262 @rtype: string
1263 @return: job id
1264
1265 """
1266 query = [
1267 ("mode", mode),
1268 ]
1269
1270
1271
1272 if disks is not None:
1273 _AppendIf(query, True,
1274 ("disks", ",".join(str(idx) for idx in disks)))
1275
1276 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1277 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1278 _AppendReason(query, reason)
1279 _AppendIf(query, early_release is not None,
1280 ("early_release", early_release))
1281
1282 return self._SendRequest(HTTP_POST,
1283 ("/%s/instances/%s/replace-disks" %
1284 (GANETI_RAPI_VERSION, instance)), query, None)
1285
1287 """Prepares an instance for an export.
1288
1289 @type instance: string
1290 @param instance: Instance name
1291 @type mode: string
1292 @param mode: Export mode
1293 @type reason: string
1294 @param reason: the reason for executing this operation
1295 @rtype: string
1296 @return: Job ID
1297
1298 """
1299 query = [("mode", mode)]
1300 _AppendReason(query, reason)
1301 return self._SendRequest(HTTP_PUT,
1302 ("/%s/instances/%s/prepare-export" %
1303 (GANETI_RAPI_VERSION, instance)), query, None)
1304
1305 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1306 remove_instance=None, x509_key_name=None,
1307 destination_x509_ca=None, compress=None, reason=None):
1308 """Exports an instance.
1309
1310 @type instance: string
1311 @param instance: Instance name
1312 @type mode: string
1313 @param mode: Export mode
1314 @type reason: string
1315 @param reason: the reason for executing this operation
1316 @rtype: string
1317 @return: Job ID
1318
1319 """
1320 body = {
1321 "destination": destination,
1322 "mode": mode,
1323 }
1324
1325 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1326 _SetItemIf(body, remove_instance is not None,
1327 "remove_instance", remove_instance)
1328 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1329 _SetItemIf(body, destination_x509_ca is not None,
1330 "destination_x509_ca", destination_x509_ca)
1331 _SetItemIf(body, compress is not None, "compress", compress)
1332
1333 query = []
1334 _AppendReason(query, reason)
1335
1336 return self._SendRequest(HTTP_PUT,
1337 ("/%s/instances/%s/export" %
1338 (GANETI_RAPI_VERSION, instance)), query, body)
1339
1340 - def MigrateInstance(self, instance, mode=None, cleanup=None,
1341 target_node=None, reason=None):
1342 """Migrates an instance.
1343
1344 @type instance: string
1345 @param instance: Instance name
1346 @type mode: string
1347 @param mode: Migration mode
1348 @type cleanup: bool
1349 @param cleanup: Whether to clean up a previously failed migration
1350 @type target_node: string
1351 @param target_node: Target Node for externally mirrored instances
1352 @type reason: string
1353 @param reason: the reason for executing this operation
1354 @rtype: string
1355 @return: job id
1356
1357 """
1358 body = {}
1359 _SetItemIf(body, mode is not None, "mode", mode)
1360 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1361 _SetItemIf(body, target_node is not None, "target_node", target_node)
1362
1363 query = []
1364 _AppendReason(query, reason)
1365
1366 return self._SendRequest(HTTP_PUT,
1367 ("/%s/instances/%s/migrate" %
1368 (GANETI_RAPI_VERSION, instance)), query, body)
1369
1370 - def FailoverInstance(self, instance, iallocator=None,
1371 ignore_consistency=None, target_node=None, reason=None):
1372 """Does a failover of an instance.
1373
1374 @type instance: string
1375 @param instance: Instance name
1376 @type iallocator: string
1377 @param iallocator: Iallocator for deciding the target node for
1378 shared-storage instances
1379 @type ignore_consistency: bool
1380 @param ignore_consistency: Whether to ignore disk consistency
1381 @type target_node: string
1382 @param target_node: Target node for shared-storage instances
1383 @type reason: string
1384 @param reason: the reason for executing this operation
1385 @rtype: string
1386 @return: job id
1387
1388 """
1389 body = {}
1390 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1391 _SetItemIf(body, ignore_consistency is not None,
1392 "ignore_consistency", ignore_consistency)
1393 _SetItemIf(body, target_node is not None, "target_node", target_node)
1394
1395 query = []
1396 _AppendReason(query, reason)
1397
1398 return self._SendRequest(HTTP_PUT,
1399 ("/%s/instances/%s/failover" %
1400 (GANETI_RAPI_VERSION, instance)), query, body)
1401
1402 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1403 reason=None):
1404 """Changes the name of an instance.
1405
1406 @type instance: string
1407 @param instance: Instance name
1408 @type new_name: string
1409 @param new_name: New instance name
1410 @type ip_check: bool
1411 @param ip_check: Whether to ensure instance's IP address is inactive
1412 @type name_check: bool
1413 @param name_check: Whether to ensure instance's name is resolvable
1414 @type reason: string
1415 @param reason: the reason for executing this operation
1416 @rtype: string
1417 @return: job id
1418
1419 """
1420 body = {
1421 "new_name": new_name,
1422 }
1423
1424 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1425 _SetItemIf(body, name_check is not None, "name_check", name_check)
1426
1427 query = []
1428 _AppendReason(query, reason)
1429
1430 return self._SendRequest(HTTP_PUT,
1431 ("/%s/instances/%s/rename" %
1432 (GANETI_RAPI_VERSION, instance)), query, body)
1433
1435 """Request information for connecting to instance's console.
1436
1437 @type instance: string
1438 @param instance: Instance name
1439 @type reason: string
1440 @param reason: the reason for executing this operation
1441 @rtype: dict
1442 @return: dictionary containing information about instance's console
1443
1444 """
1445 query = []
1446 _AppendReason(query, reason)
1447 return self._SendRequest(HTTP_GET,
1448 ("/%s/instances/%s/console" %
1449 (GANETI_RAPI_VERSION, instance)), query, None)
1450
1452 """Gets all jobs for the cluster.
1453
1454 @type bulk: bool
1455 @param bulk: Whether to return detailed information about jobs.
1456 @rtype: list of int
1457 @return: List of job ids for the cluster or list of dicts with detailed
1458 information about the jobs if bulk parameter was true.
1459
1460 """
1461 query = []
1462 _AppendIf(query, bulk, ("bulk", 1))
1463
1464 if bulk:
1465 return self._SendRequest(HTTP_GET,
1466 "/%s/jobs" % GANETI_RAPI_VERSION,
1467 query, None)
1468 else:
1469 return [int(j["id"])
1470 for j in self._SendRequest(HTTP_GET,
1471 "/%s/jobs" % GANETI_RAPI_VERSION,
1472 None, None)]
1473
1475 """Gets the status of a job.
1476
1477 @type job_id: string
1478 @param job_id: job id whose status to query
1479
1480 @rtype: dict
1481 @return: job status
1482
1483 """
1484 return self._SendRequest(HTTP_GET,
1485 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1486 None, None)
1487
1489 """Polls cluster for job status until completion.
1490
1491 Completion is defined as any of the following states listed in
1492 L{JOB_STATUS_FINALIZED}.
1493
1494 @type job_id: string
1495 @param job_id: job id to watch
1496 @type period: int
1497 @param period: how often to poll for status (optional, default 5s)
1498 @type retries: int
1499 @param retries: how many time to poll before giving up
1500 (optional, default -1 means unlimited)
1501
1502 @rtype: bool
1503 @return: C{True} if job succeeded or C{False} if failed/status timeout
1504 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1505 possible; L{WaitForJobChange} returns immediately after a job changed and
1506 does not use polling
1507
1508 """
1509 while retries != 0:
1510 job_result = self.GetJobStatus(job_id)
1511
1512 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1513 return True
1514 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1515 return False
1516
1517 if period:
1518 time.sleep(period)
1519
1520 if retries > 0:
1521 retries -= 1
1522
1523 return False
1524
1526 """Waits for job changes.
1527
1528 @type job_id: string
1529 @param job_id: Job ID for which to wait
1530 @return: C{None} if no changes have been detected and a dict with two keys,
1531 C{job_info} and C{log_entries} otherwise.
1532 @rtype: dict
1533
1534 """
1535 body = {
1536 "fields": fields,
1537 "previous_job_info": prev_job_info,
1538 "previous_log_serial": prev_log_serial,
1539 }
1540
1541 return self._SendRequest(HTTP_GET,
1542 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1543 None, body)
1544
1545 - def CancelJob(self, job_id, dry_run=False):
1546 """Cancels a job.
1547
1548 @type job_id: string
1549 @param job_id: id of the job to delete
1550 @type dry_run: bool
1551 @param dry_run: whether to perform a dry run
1552 @rtype: tuple
1553 @return: tuple containing the result, and a message (bool, string)
1554
1555 """
1556 query = []
1557 _AppendDryRunIf(query, dry_run)
1558
1559 return self._SendRequest(HTTP_DELETE,
1560 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1561 query, None)
1562
1563 - def GetNodes(self, bulk=False, reason=None):
1564 """Gets all nodes in the cluster.
1565
1566 @type bulk: bool
1567 @param bulk: whether to return all information about all instances
1568 @type reason: string
1569 @param reason: the reason for executing this operation
1570
1571 @rtype: list of dict or str
1572 @return: if bulk is true, info about nodes in the cluster,
1573 else list of nodes in the cluster
1574
1575 """
1576 query = []
1577 _AppendIf(query, bulk, ("bulk", 1))
1578 _AppendReason(query, reason)
1579
1580 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1581 query, None)
1582 if bulk:
1583 return nodes
1584 else:
1585 return [n["id"] for n in nodes]
1586
1587 - def GetNode(self, node, reason=None):
1588 """Gets information about a node.
1589
1590 @type node: str
1591 @param node: node whose info to return
1592 @type reason: string
1593 @param reason: the reason for executing this operation
1594
1595 @rtype: dict
1596 @return: info about the node
1597
1598 """
1599 query = []
1600 _AppendReason(query, reason)
1601
1602 return self._SendRequest(HTTP_GET,
1603 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1604 query, None)
1605
1606 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1607 dry_run=False, early_release=None,
1608 mode=None, accept_old=False, reason=None):
1609 """Evacuates instances from a Ganeti node.
1610
1611 @type node: str
1612 @param node: node to evacuate
1613 @type iallocator: str or None
1614 @param iallocator: instance allocator to use
1615 @type remote_node: str
1616 @param remote_node: node to evaucate to
1617 @type dry_run: bool
1618 @param dry_run: whether to perform a dry run
1619 @type early_release: bool
1620 @param early_release: whether to enable parallelization
1621 @type mode: string
1622 @param mode: Node evacuation mode
1623 @type accept_old: bool
1624 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1625 results
1626 @type reason: string
1627 @param reason: the reason for executing this operation
1628
1629 @rtype: string, or a list for pre-2.5 results
1630 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1631 list of (job ID, instance name, new secondary node); if dry_run was
1632 specified, then the actual move jobs were not submitted and the job IDs
1633 will be C{None}
1634
1635 @raises GanetiApiError: if an iallocator and remote_node are both
1636 specified
1637
1638 """
1639 if iallocator and remote_node:
1640 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1641
1642 query = []
1643 _AppendDryRunIf(query, dry_run)
1644 _AppendReason(query, reason)
1645
1646 if _NODE_EVAC_RES1 in self.GetFeatures():
1647
1648 body = {}
1649
1650 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1651 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1652 _SetItemIf(body, early_release is not None,
1653 "early_release", early_release)
1654 _SetItemIf(body, mode is not None, "mode", mode)
1655 else:
1656
1657 body = None
1658
1659 if not accept_old:
1660 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1661 " not accept old-style results (parameter"
1662 " accept_old)")
1663
1664
1665 if mode is not None and mode != NODE_EVAC_SEC:
1666 raise GanetiApiError("Server can only evacuate secondary instances")
1667
1668 _AppendIf(query, iallocator, ("iallocator", iallocator))
1669 _AppendIf(query, remote_node, ("remote_node", remote_node))
1670 _AppendIf(query, early_release, ("early_release", 1))
1671
1672 return self._SendRequest(HTTP_POST,
1673 ("/%s/nodes/%s/evacuate" %
1674 (GANETI_RAPI_VERSION, node)), query, body)
1675
1676 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1677 target_node=None, reason=None):
1678 """Migrates all primary instances from a node.
1679
1680 @type node: str
1681 @param node: node to migrate
1682 @type mode: string
1683 @param mode: if passed, it will overwrite the live migration type,
1684 otherwise the hypervisor default will be used
1685 @type dry_run: bool
1686 @param dry_run: whether to perform a dry run
1687 @type iallocator: string
1688 @param iallocator: instance allocator to use
1689 @type target_node: string
1690 @param target_node: Target node for shared-storage instances
1691 @type reason: string
1692 @param reason: the reason for executing this operation
1693
1694 @rtype: string
1695 @return: job id
1696
1697 """
1698 query = []
1699 _AppendDryRunIf(query, dry_run)
1700 _AppendReason(query, reason)
1701
1702 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1703 body = {}
1704
1705 _SetItemIf(body, mode is not None, "mode", mode)
1706 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1707 _SetItemIf(body, target_node is not None, "target_node", target_node)
1708
1709 assert len(query) <= 1
1710
1711 return self._SendRequest(HTTP_POST,
1712 ("/%s/nodes/%s/migrate" %
1713 (GANETI_RAPI_VERSION, node)), query, body)
1714 else:
1715
1716 if target_node is not None:
1717 raise GanetiApiError("Server does not support specifying target node"
1718 " for node migration")
1719
1720 _AppendIf(query, mode is not None, ("mode", mode))
1721
1722 return self._SendRequest(HTTP_POST,
1723 ("/%s/nodes/%s/migrate" %
1724 (GANETI_RAPI_VERSION, node)), query, None)
1725
1727 """Gets the current role for a node.
1728
1729 @type node: str
1730 @param node: node whose role to return
1731 @type reason: string
1732 @param reason: the reason for executing this operation
1733
1734 @rtype: str
1735 @return: the current role for a node
1736
1737 """
1738 query = []
1739 _AppendReason(query, reason)
1740
1741 return self._SendRequest(HTTP_GET,
1742 ("/%s/nodes/%s/role" %
1743 (GANETI_RAPI_VERSION, node)), query, None)
1744
1745 - def SetNodeRole(self, node, role, force=False, auto_promote=None,
1746 reason=None):
1747 """Sets the role for a node.
1748
1749 @type node: str
1750 @param node: the node whose role to set
1751 @type role: str
1752 @param role: the role to set for the node
1753 @type force: bool
1754 @param force: whether to force the role change
1755 @type auto_promote: bool
1756 @param auto_promote: Whether node(s) should be promoted to master candidate
1757 if necessary
1758 @type reason: string
1759 @param reason: the reason for executing this operation
1760
1761 @rtype: string
1762 @return: job id
1763
1764 """
1765 query = []
1766 _AppendForceIf(query, force)
1767 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1768 _AppendReason(query, reason)
1769
1770 return self._SendRequest(HTTP_PUT,
1771 ("/%s/nodes/%s/role" %
1772 (GANETI_RAPI_VERSION, node)), query, role)
1773
1775 """Powercycles a node.
1776
1777 @type node: string
1778 @param node: Node name
1779 @type force: bool
1780 @param force: Whether to force the operation
1781 @type reason: string
1782 @param reason: the reason for executing this operation
1783 @rtype: string
1784 @return: job id
1785
1786 """
1787 query = []
1788 _AppendForceIf(query, force)
1789 _AppendReason(query, reason)
1790
1791 return self._SendRequest(HTTP_POST,
1792 ("/%s/nodes/%s/powercycle" %
1793 (GANETI_RAPI_VERSION, node)), query, None)
1794
1795 - def ModifyNode(self, node, reason=None, **kwargs):
1796 """Modifies a node.
1797
1798 More details for parameters can be found in the RAPI documentation.
1799
1800 @type node: string
1801 @param node: Node name
1802 @type reason: string
1803 @param reason: the reason for executing this operation
1804 @rtype: string
1805 @return: job id
1806
1807 """
1808 query = []
1809 _AppendReason(query, reason)
1810
1811 return self._SendRequest(HTTP_POST,
1812 ("/%s/nodes/%s/modify" %
1813 (GANETI_RAPI_VERSION, node)), query, kwargs)
1814
1816 """Gets the storage units for a node.
1817
1818 @type node: str
1819 @param node: the node whose storage units to return
1820 @type storage_type: str
1821 @param storage_type: storage type whose units to return
1822 @type output_fields: str
1823 @param output_fields: storage type fields to return
1824 @type reason: string
1825 @param reason: the reason for executing this operation
1826
1827 @rtype: string
1828 @return: job id where results can be retrieved
1829
1830 """
1831 query = [
1832 ("storage_type", storage_type),
1833 ("output_fields", output_fields),
1834 ]
1835 _AppendReason(query, reason)
1836
1837 return self._SendRequest(HTTP_GET,
1838 ("/%s/nodes/%s/storage" %
1839 (GANETI_RAPI_VERSION, node)), query, None)
1840
1843 """Modifies parameters of storage units on the node.
1844
1845 @type node: str
1846 @param node: node whose storage units to modify
1847 @type storage_type: str
1848 @param storage_type: storage type whose units to modify
1849 @type name: str
1850 @param name: name of the storage unit
1851 @type allocatable: bool or None
1852 @param allocatable: Whether to set the "allocatable" flag on the storage
1853 unit (None=no modification, True=set, False=unset)
1854 @type reason: string
1855 @param reason: the reason for executing this operation
1856
1857 @rtype: string
1858 @return: job id
1859
1860 """
1861 query = [
1862 ("storage_type", storage_type),
1863 ("name", name),
1864 ]
1865
1866 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1867 _AppendReason(query, reason)
1868
1869 return self._SendRequest(HTTP_PUT,
1870 ("/%s/nodes/%s/storage/modify" %
1871 (GANETI_RAPI_VERSION, node)), query, None)
1872
1874 """Repairs a storage unit on the node.
1875
1876 @type node: str
1877 @param node: node whose storage units to repair
1878 @type storage_type: str
1879 @param storage_type: storage type to repair
1880 @type name: str
1881 @param name: name of the storage unit to repair
1882 @type reason: string
1883 @param reason: the reason for executing this operation
1884
1885 @rtype: string
1886 @return: job id
1887
1888 """
1889 query = [
1890 ("storage_type", storage_type),
1891 ("name", name),
1892 ]
1893 _AppendReason(query, reason)
1894
1895 return self._SendRequest(HTTP_PUT,
1896 ("/%s/nodes/%s/storage/repair" %
1897 (GANETI_RAPI_VERSION, node)), query, None)
1898
1917
1941
1965
1967 """Gets all networks in the cluster.
1968
1969 @type bulk: bool
1970 @param bulk: whether to return all information about the networks
1971
1972 @rtype: list of dict or str
1973 @return: if bulk is true, a list of dictionaries with info about all
1974 networks in the cluster, else a list of names of those networks
1975
1976 """
1977 query = []
1978 _AppendIf(query, bulk, ("bulk", 1))
1979 _AppendReason(query, reason)
1980
1981 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1982 query, None)
1983 if bulk:
1984 return networks
1985 else:
1986 return [n["name"] for n in networks]
1987
1989 """Gets information about a network.
1990
1991 @type network: str
1992 @param network: name of the network whose info to return
1993 @type reason: string
1994 @param reason: the reason for executing this operation
1995
1996 @rtype: dict
1997 @return: info about the network
1998
1999 """
2000 query = []
2001 _AppendReason(query, reason)
2002
2003 return self._SendRequest(HTTP_GET,
2004 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
2005 query, None)
2006
2007 - def CreateNetwork(self, network_name, network, gateway=None, network6=None,
2008 gateway6=None, mac_prefix=None,
2009 add_reserved_ips=None, tags=None, dry_run=False,
2010 reason=None):
2011 """Creates a new network.
2012
2013 @type network_name: str
2014 @param network_name: the name of network to create
2015 @type dry_run: bool
2016 @param dry_run: whether to peform a dry run
2017 @type reason: string
2018 @param reason: the reason for executing this operation
2019
2020 @rtype: string
2021 @return: job id
2022
2023 """
2024 query = []
2025 _AppendDryRunIf(query, dry_run)
2026 _AppendReason(query, reason)
2027
2028 if add_reserved_ips:
2029 add_reserved_ips = add_reserved_ips.split(",")
2030
2031 if tags:
2032 tags = tags.split(",")
2033
2034 body = {
2035 "network_name": network_name,
2036 "gateway": gateway,
2037 "network": network,
2038 "gateway6": gateway6,
2039 "network6": network6,
2040 "mac_prefix": mac_prefix,
2041 "add_reserved_ips": add_reserved_ips,
2042 "tags": tags,
2043 }
2044
2045 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2046 query, body)
2047
2048 - def ConnectNetwork(self, network_name, group_name, mode, link,
2049 vlan="", dry_run=False, reason=None):
2050 """Connects a Network to a NodeGroup with the given netparams
2051
2052 """
2053 body = {
2054 "group_name": group_name,
2055 "network_mode": mode,
2056 "network_link": link,
2057 "network_vlan": vlan,
2058 }
2059
2060 query = []
2061 _AppendDryRunIf(query, dry_run)
2062 _AppendReason(query, reason)
2063
2064 return self._SendRequest(HTTP_PUT,
2065 ("/%s/networks/%s/connect" %
2066 (GANETI_RAPI_VERSION, network_name)), query, body)
2067
2068 - def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2069 reason=None):
2084
2086 """Modifies a network.
2087
2088 More details for parameters can be found in the RAPI documentation.
2089
2090 @type network: string
2091 @param network: Network name
2092 @type reason: string
2093 @param reason: the reason for executing this operation
2094 @rtype: string
2095 @return: job id
2096
2097 """
2098 query = []
2099 _AppendReason(query, reason)
2100
2101 return self._SendRequest(HTTP_PUT,
2102 ("/%s/networks/%s/modify" %
2103 (GANETI_RAPI_VERSION, network)), None, kwargs)
2104
2106 """Deletes a network.
2107
2108 @type network: str
2109 @param network: the network to delete
2110 @type dry_run: bool
2111 @param dry_run: whether to peform a dry run
2112 @type reason: string
2113 @param reason: the reason for executing this operation
2114
2115 @rtype: string
2116 @return: job id
2117
2118 """
2119 query = []
2120 _AppendDryRunIf(query, dry_run)
2121 _AppendReason(query, reason)
2122
2123 return self._SendRequest(HTTP_DELETE,
2124 ("/%s/networks/%s" %
2125 (GANETI_RAPI_VERSION, network)), query, None)
2126
2145
2169
2192
2193 - def GetGroups(self, bulk=False, reason=None):
2194 """Gets all node groups in the cluster.
2195
2196 @type bulk: bool
2197 @param bulk: whether to return all information about the groups
2198 @type reason: string
2199 @param reason: the reason for executing this operation
2200
2201 @rtype: list of dict or str
2202 @return: if bulk is true, a list of dictionaries with info about all node
2203 groups in the cluster, else a list of names of those node groups
2204
2205 """
2206 query = []
2207 _AppendIf(query, bulk, ("bulk", 1))
2208 _AppendReason(query, reason)
2209
2210 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2211 query, None)
2212 if bulk:
2213 return groups
2214 else:
2215 return [g["name"] for g in groups]
2216
2217 - def GetGroup(self, group, reason=None):
2218 """Gets information about a node group.
2219
2220 @type group: str
2221 @param group: name of the node group whose info to return
2222 @type reason: string
2223 @param reason: the reason for executing this operation
2224
2225 @rtype: dict
2226 @return: info about the node group
2227
2228 """
2229 query = []
2230 _AppendReason(query, reason)
2231
2232 return self._SendRequest(HTTP_GET,
2233 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2234 query, None)
2235
2236 - def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2237 """Creates a new node group.
2238
2239 @type name: str
2240 @param name: the name of node group to create
2241 @type alloc_policy: str
2242 @param alloc_policy: the desired allocation policy for the group, if any
2243 @type dry_run: bool
2244 @param dry_run: whether to peform a dry run
2245 @type reason: string
2246 @param reason: the reason for executing this operation
2247
2248 @rtype: string
2249 @return: job id
2250
2251 """
2252 query = []
2253 _AppendDryRunIf(query, dry_run)
2254 _AppendReason(query, reason)
2255
2256 body = {
2257 "name": name,
2258 "alloc_policy": alloc_policy,
2259 }
2260
2261 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2262 query, body)
2263
2265 """Modifies a node group.
2266
2267 More details for parameters can be found in the RAPI documentation.
2268
2269 @type group: string
2270 @param group: Node group name
2271 @type reason: string
2272 @param reason: the reason for executing this operation
2273 @rtype: string
2274 @return: job id
2275
2276 """
2277 query = []
2278 _AppendReason(query, reason)
2279
2280 return self._SendRequest(HTTP_PUT,
2281 ("/%s/groups/%s/modify" %
2282 (GANETI_RAPI_VERSION, group)), query, kwargs)
2283
2284 - def DeleteGroup(self, group, dry_run=False, reason=None):
2285 """Deletes a node group.
2286
2287 @type group: str
2288 @param group: the node group to delete
2289 @type dry_run: bool
2290 @param dry_run: whether to peform a dry run
2291 @type reason: string
2292 @param reason: the reason for executing this operation
2293
2294 @rtype: string
2295 @return: job id
2296
2297 """
2298 query = []
2299 _AppendDryRunIf(query, dry_run)
2300 _AppendReason(query, reason)
2301
2302 return self._SendRequest(HTTP_DELETE,
2303 ("/%s/groups/%s" %
2304 (GANETI_RAPI_VERSION, group)), query, None)
2305
2307 """Changes the name of a node group.
2308
2309 @type group: string
2310 @param group: Node group name
2311 @type new_name: string
2312 @param new_name: New node group name
2313 @type reason: string
2314 @param reason: the reason for executing this operation
2315
2316 @rtype: string
2317 @return: job id
2318
2319 """
2320 body = {
2321 "new_name": new_name,
2322 }
2323
2324 query = []
2325 _AppendReason(query, reason)
2326
2327 return self._SendRequest(HTTP_PUT,
2328 ("/%s/groups/%s/rename" %
2329 (GANETI_RAPI_VERSION, group)), query, body)
2330
2331 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2332 reason=None):
2333 """Assigns nodes to a group.
2334
2335 @type group: string
2336 @param group: Node group name
2337 @type nodes: list of strings
2338 @param nodes: List of nodes to assign to the group
2339 @type reason: string
2340 @param reason: the reason for executing this operation
2341
2342 @rtype: string
2343 @return: job id
2344
2345 """
2346 query = []
2347 _AppendForceIf(query, force)
2348 _AppendDryRunIf(query, dry_run)
2349 _AppendReason(query, reason)
2350
2351 body = {
2352 "nodes": nodes,
2353 }
2354
2355 return self._SendRequest(HTTP_PUT,
2356 ("/%s/groups/%s/assign-nodes" %
2357 (GANETI_RAPI_VERSION, group)), query, body)
2358
2377
2401
2424
2425 - def Query(self, what, fields, qfilter=None, reason=None):
2426 """Retrieves information about resources.
2427
2428 @type what: string
2429 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2430 @type fields: list of string
2431 @param fields: Requested fields
2432 @type qfilter: None or list
2433 @param qfilter: Query filter
2434 @type reason: string
2435 @param reason: the reason for executing this operation
2436
2437 @rtype: string
2438 @return: job id
2439
2440 """
2441 query = []
2442 _AppendReason(query, reason)
2443
2444 body = {
2445 "fields": fields,
2446 }
2447
2448 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2449
2450 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2451
2452 return self._SendRequest(HTTP_PUT,
2453 ("/%s/query/%s" %
2454 (GANETI_RAPI_VERSION, what)), query, body)
2455
2456 - def QueryFields(self, what, fields=None, reason=None):
2457 """Retrieves available fields for a resource.
2458
2459 @type what: string
2460 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2461 @type fields: list of string
2462 @param fields: Requested fields
2463 @type reason: string
2464 @param reason: the reason for executing this operation
2465
2466 @rtype: string
2467 @return: job id
2468
2469 """
2470 query = []
2471 _AppendReason(query, reason)
2472
2473 if fields is not None:
2474 _AppendIf(query, True, ("fields", ",".join(fields)))
2475
2476 return self._SendRequest(HTTP_GET,
2477 ("/%s/query/%s/fields" %
2478 (GANETI_RAPI_VERSION, what)), query, None)
2479
2481 """Gets all filter rules in the cluster.
2482
2483 @type bulk: bool
2484 @param bulk: whether to return all information about the networks
2485
2486 @rtype: list of dict or str
2487 @return: if bulk is true, a list of dictionaries with info about all
2488 filter rules in the cluster, else a list of UUIDs of those
2489 filters
2490
2491 """
2492 query = []
2493 _AppendIf(query, bulk, ("bulk", 1))
2494
2495 filters = self._SendRequest(HTTP_GET, "/%s/filters" % GANETI_RAPI_VERSION,
2496 query, None)
2497 if bulk:
2498 return filters
2499 else:
2500 return [f["uuid"] for f in filters]
2501
2503 """Gets information about a filter rule.
2504
2505 @type filter_uuid: str
2506 @param filter_uuid: UUID of the filter whose info to return
2507
2508 @rtype: dict
2509 @return: info about the filter
2510
2511 """
2512 query = []
2513
2514 return self._SendRequest(HTTP_GET,
2515 "/%s/filters/%s" % (GANETI_RAPI_VERSION,
2516 filter_uuid),
2517 query, None)
2518
2519 - def AddFilter(self, priority, predicates, action, reason_trail=None):
2520 """Adds a filter rule
2521
2522 @type reason_trail: list of (str, str, int) triples
2523 @param reason_trail: the reason trail for executing this operation,
2524 or None
2525
2526 @rtype: string
2527 @return: filter UUID of the added filter
2528
2529 """
2530 if reason_trail is None:
2531 reason_trail = []
2532
2533 assert isinstance(reason_trail, list)
2534
2535 reason_trail.append(("gnt:client", "", EpochNano(),))
2536
2537 body = {
2538 "priority": priority,
2539 "predicates": predicates,
2540 "action": action,
2541 "reason": reason_trail,
2542 }
2543
2544 query = []
2545
2546 return self._SendRequest(HTTP_POST,
2547 ("/%s/filters" % (GANETI_RAPI_VERSION)),
2548 query, body)
2549
2550 - def ReplaceFilter(self, uuid, priority, predicates, action,
2551 reason_trail=None):
2552 """Replaces a filter rule, or creates one if it doesn't already exist
2553
2554 @type reason_trail: list of (str, str, int) triples
2555 @param reason_trail: the reason trail for executing this operation,
2556 or None
2557
2558 @rtype: string
2559 @return: filter UUID of the replaced/added filter
2560
2561 """
2562 if reason_trail is None:
2563 reason_trail = []
2564
2565 assert isinstance(reason_trail, list)
2566
2567 reason_trail.append(("gnt:client", "", EpochNano(),))
2568
2569 body = {
2570 "priority": priority,
2571 "predicates": predicates,
2572 "action": action,
2573 "reason": reason_trail,
2574 }
2575
2576 query = []
2577
2578 return self._SendRequest(HTTP_PUT,
2579 ("/%s/filters/%s" % (GANETI_RAPI_VERSION, uuid)),
2580 query, body)
2581
2594