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