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,
1294 x509_key_name=None, destination_x509_ca=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
1319 query = []
1320 _AppendReason(query, reason)
1321
1322 return self._SendRequest(HTTP_PUT,
1323 ("/%s/instances/%s/export" %
1324 (GANETI_RAPI_VERSION, instance)), query, body)
1325
1326 - def MigrateInstance(self, instance, mode=None, cleanup=None,
1327 target_node=None, reason=None):
1328 """Migrates an instance.
1329
1330 @type instance: string
1331 @param instance: Instance name
1332 @type mode: string
1333 @param mode: Migration mode
1334 @type cleanup: bool
1335 @param cleanup: Whether to clean up a previously failed migration
1336 @type target_node: string
1337 @param target_node: Target Node for externally mirrored instances
1338 @type reason: string
1339 @param reason: the reason for executing this operation
1340 @rtype: string
1341 @return: job id
1342
1343 """
1344 body = {}
1345 _SetItemIf(body, mode is not None, "mode", mode)
1346 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1347 _SetItemIf(body, target_node is not None, "target_node", target_node)
1348
1349 query = []
1350 _AppendReason(query, reason)
1351
1352 return self._SendRequest(HTTP_PUT,
1353 ("/%s/instances/%s/migrate" %
1354 (GANETI_RAPI_VERSION, instance)), query, body)
1355
1356 - def FailoverInstance(self, instance, iallocator=None,
1357 ignore_consistency=None, target_node=None, reason=None):
1358 """Does a failover of an instance.
1359
1360 @type instance: string
1361 @param instance: Instance name
1362 @type iallocator: string
1363 @param iallocator: Iallocator for deciding the target node for
1364 shared-storage instances
1365 @type ignore_consistency: bool
1366 @param ignore_consistency: Whether to ignore disk consistency
1367 @type target_node: string
1368 @param target_node: Target node for shared-storage instances
1369 @type reason: string
1370 @param reason: the reason for executing this operation
1371 @rtype: string
1372 @return: job id
1373
1374 """
1375 body = {}
1376 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1377 _SetItemIf(body, ignore_consistency is not None,
1378 "ignore_consistency", ignore_consistency)
1379 _SetItemIf(body, target_node is not None, "target_node", target_node)
1380
1381 query = []
1382 _AppendReason(query, reason)
1383
1384 return self._SendRequest(HTTP_PUT,
1385 ("/%s/instances/%s/failover" %
1386 (GANETI_RAPI_VERSION, instance)), query, body)
1387
1388 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1389 reason=None):
1390 """Changes the name of an instance.
1391
1392 @type instance: string
1393 @param instance: Instance name
1394 @type new_name: string
1395 @param new_name: New instance name
1396 @type ip_check: bool
1397 @param ip_check: Whether to ensure instance's IP address is inactive
1398 @type name_check: bool
1399 @param name_check: Whether to ensure instance's name is resolvable
1400 @type reason: string
1401 @param reason: the reason for executing this operation
1402 @rtype: string
1403 @return: job id
1404
1405 """
1406 body = {
1407 "new_name": new_name,
1408 }
1409
1410 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1411 _SetItemIf(body, name_check is not None, "name_check", name_check)
1412
1413 query = []
1414 _AppendReason(query, reason)
1415
1416 return self._SendRequest(HTTP_PUT,
1417 ("/%s/instances/%s/rename" %
1418 (GANETI_RAPI_VERSION, instance)), query, body)
1419
1421 """Request information for connecting to instance's console.
1422
1423 @type instance: string
1424 @param instance: Instance name
1425 @type reason: string
1426 @param reason: the reason for executing this operation
1427 @rtype: dict
1428 @return: dictionary containing information about instance's console
1429
1430 """
1431 query = []
1432 _AppendReason(query, reason)
1433 return self._SendRequest(HTTP_GET,
1434 ("/%s/instances/%s/console" %
1435 (GANETI_RAPI_VERSION, instance)), query, None)
1436
1438 """Gets all jobs for the cluster.
1439
1440 @type bulk: bool
1441 @param bulk: Whether to return detailed information about jobs.
1442 @rtype: list of int
1443 @return: List of job ids for the cluster or list of dicts with detailed
1444 information about the jobs if bulk parameter was true.
1445
1446 """
1447 query = []
1448 _AppendIf(query, bulk, ("bulk", 1))
1449
1450 if bulk:
1451 return self._SendRequest(HTTP_GET,
1452 "/%s/jobs" % GANETI_RAPI_VERSION,
1453 query, None)
1454 else:
1455 return [int(j["id"])
1456 for j in self._SendRequest(HTTP_GET,
1457 "/%s/jobs" % GANETI_RAPI_VERSION,
1458 None, None)]
1459
1461 """Gets the status of a job.
1462
1463 @type job_id: string
1464 @param job_id: job id whose status to query
1465
1466 @rtype: dict
1467 @return: job status
1468
1469 """
1470 return self._SendRequest(HTTP_GET,
1471 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1472 None, None)
1473
1475 """Polls cluster for job status until completion.
1476
1477 Completion is defined as any of the following states listed in
1478 L{JOB_STATUS_FINALIZED}.
1479
1480 @type job_id: string
1481 @param job_id: job id to watch
1482 @type period: int
1483 @param period: how often to poll for status (optional, default 5s)
1484 @type retries: int
1485 @param retries: how many time to poll before giving up
1486 (optional, default -1 means unlimited)
1487
1488 @rtype: bool
1489 @return: C{True} if job succeeded or C{False} if failed/status timeout
1490 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1491 possible; L{WaitForJobChange} returns immediately after a job changed and
1492 does not use polling
1493
1494 """
1495 while retries != 0:
1496 job_result = self.GetJobStatus(job_id)
1497
1498 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1499 return True
1500 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1501 return False
1502
1503 if period:
1504 time.sleep(period)
1505
1506 if retries > 0:
1507 retries -= 1
1508
1509 return False
1510
1512 """Waits for job changes.
1513
1514 @type job_id: string
1515 @param job_id: Job ID for which to wait
1516 @return: C{None} if no changes have been detected and a dict with two keys,
1517 C{job_info} and C{log_entries} otherwise.
1518 @rtype: dict
1519
1520 """
1521 body = {
1522 "fields": fields,
1523 "previous_job_info": prev_job_info,
1524 "previous_log_serial": prev_log_serial,
1525 }
1526
1527 return self._SendRequest(HTTP_GET,
1528 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1529 None, body)
1530
1531 - def CancelJob(self, job_id, dry_run=False):
1532 """Cancels a job.
1533
1534 @type job_id: string
1535 @param job_id: id of the job to delete
1536 @type dry_run: bool
1537 @param dry_run: whether to perform a dry run
1538 @rtype: tuple
1539 @return: tuple containing the result, and a message (bool, string)
1540
1541 """
1542 query = []
1543 _AppendDryRunIf(query, dry_run)
1544
1545 return self._SendRequest(HTTP_DELETE,
1546 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1547 query, None)
1548
1549 - def GetNodes(self, bulk=False, reason=None):
1550 """Gets all nodes in the cluster.
1551
1552 @type bulk: bool
1553 @param bulk: whether to return all information about all instances
1554 @type reason: string
1555 @param reason: the reason for executing this operation
1556
1557 @rtype: list of dict or str
1558 @return: if bulk is true, info about nodes in the cluster,
1559 else list of nodes in the cluster
1560
1561 """
1562 query = []
1563 _AppendIf(query, bulk, ("bulk", 1))
1564 _AppendReason(query, reason)
1565
1566 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1567 query, None)
1568 if bulk:
1569 return nodes
1570 else:
1571 return [n["id"] for n in nodes]
1572
1573 - def GetNode(self, node, reason=None):
1574 """Gets information about a node.
1575
1576 @type node: str
1577 @param node: node whose info to return
1578 @type reason: string
1579 @param reason: the reason for executing this operation
1580
1581 @rtype: dict
1582 @return: info about the node
1583
1584 """
1585 query = []
1586 _AppendReason(query, reason)
1587
1588 return self._SendRequest(HTTP_GET,
1589 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1590 query, None)
1591
1592 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1593 dry_run=False, early_release=None,
1594 mode=None, accept_old=False, reason=None):
1595 """Evacuates instances from a Ganeti node.
1596
1597 @type node: str
1598 @param node: node to evacuate
1599 @type iallocator: str or None
1600 @param iallocator: instance allocator to use
1601 @type remote_node: str
1602 @param remote_node: node to evaucate to
1603 @type dry_run: bool
1604 @param dry_run: whether to perform a dry run
1605 @type early_release: bool
1606 @param early_release: whether to enable parallelization
1607 @type mode: string
1608 @param mode: Node evacuation mode
1609 @type accept_old: bool
1610 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1611 results
1612 @type reason: string
1613 @param reason: the reason for executing this operation
1614
1615 @rtype: string, or a list for pre-2.5 results
1616 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1617 list of (job ID, instance name, new secondary node); if dry_run was
1618 specified, then the actual move jobs were not submitted and the job IDs
1619 will be C{None}
1620
1621 @raises GanetiApiError: if an iallocator and remote_node are both
1622 specified
1623
1624 """
1625 if iallocator and remote_node:
1626 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1627
1628 query = []
1629 _AppendDryRunIf(query, dry_run)
1630 _AppendReason(query, reason)
1631
1632 if _NODE_EVAC_RES1 in self.GetFeatures():
1633
1634 body = {}
1635
1636 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1637 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1638 _SetItemIf(body, early_release is not None,
1639 "early_release", early_release)
1640 _SetItemIf(body, mode is not None, "mode", mode)
1641 else:
1642
1643 body = None
1644
1645 if not accept_old:
1646 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1647 " not accept old-style results (parameter"
1648 " accept_old)")
1649
1650
1651 if mode is not None and mode != NODE_EVAC_SEC:
1652 raise GanetiApiError("Server can only evacuate secondary instances")
1653
1654 _AppendIf(query, iallocator, ("iallocator", iallocator))
1655 _AppendIf(query, remote_node, ("remote_node", remote_node))
1656 _AppendIf(query, early_release, ("early_release", 1))
1657
1658 return self._SendRequest(HTTP_POST,
1659 ("/%s/nodes/%s/evacuate" %
1660 (GANETI_RAPI_VERSION, node)), query, body)
1661
1662 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1663 target_node=None, reason=None):
1664 """Migrates all primary instances from a node.
1665
1666 @type node: str
1667 @param node: node to migrate
1668 @type mode: string
1669 @param mode: if passed, it will overwrite the live migration type,
1670 otherwise the hypervisor default will be used
1671 @type dry_run: bool
1672 @param dry_run: whether to perform a dry run
1673 @type iallocator: string
1674 @param iallocator: instance allocator to use
1675 @type target_node: string
1676 @param target_node: Target node for shared-storage instances
1677 @type reason: string
1678 @param reason: the reason for executing this operation
1679
1680 @rtype: string
1681 @return: job id
1682
1683 """
1684 query = []
1685 _AppendDryRunIf(query, dry_run)
1686 _AppendReason(query, reason)
1687
1688 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1689 body = {}
1690
1691 _SetItemIf(body, mode is not None, "mode", mode)
1692 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1693 _SetItemIf(body, target_node is not None, "target_node", target_node)
1694
1695 assert len(query) <= 1
1696
1697 return self._SendRequest(HTTP_POST,
1698 ("/%s/nodes/%s/migrate" %
1699 (GANETI_RAPI_VERSION, node)), query, body)
1700 else:
1701
1702 if target_node is not None:
1703 raise GanetiApiError("Server does not support specifying target node"
1704 " for node migration")
1705
1706 _AppendIf(query, mode is not None, ("mode", mode))
1707
1708 return self._SendRequest(HTTP_POST,
1709 ("/%s/nodes/%s/migrate" %
1710 (GANETI_RAPI_VERSION, node)), query, None)
1711
1713 """Gets the current role for a node.
1714
1715 @type node: str
1716 @param node: node whose role to return
1717 @type reason: string
1718 @param reason: the reason for executing this operation
1719
1720 @rtype: str
1721 @return: the current role for a node
1722
1723 """
1724 query = []
1725 _AppendReason(query, reason)
1726
1727 return self._SendRequest(HTTP_GET,
1728 ("/%s/nodes/%s/role" %
1729 (GANETI_RAPI_VERSION, node)), query, None)
1730
1731 - def SetNodeRole(self, node, role, force=False, auto_promote=None,
1732 reason=None):
1733 """Sets the role for a node.
1734
1735 @type node: str
1736 @param node: the node whose role to set
1737 @type role: str
1738 @param role: the role to set for the node
1739 @type force: bool
1740 @param force: whether to force the role change
1741 @type auto_promote: bool
1742 @param auto_promote: Whether node(s) should be promoted to master candidate
1743 if necessary
1744 @type reason: string
1745 @param reason: the reason for executing this operation
1746
1747 @rtype: string
1748 @return: job id
1749
1750 """
1751 query = []
1752 _AppendForceIf(query, force)
1753 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1754 _AppendReason(query, reason)
1755
1756 return self._SendRequest(HTTP_PUT,
1757 ("/%s/nodes/%s/role" %
1758 (GANETI_RAPI_VERSION, node)), query, role)
1759
1761 """Powercycles a node.
1762
1763 @type node: string
1764 @param node: Node name
1765 @type force: bool
1766 @param force: Whether to force the operation
1767 @type reason: string
1768 @param reason: the reason for executing this operation
1769 @rtype: string
1770 @return: job id
1771
1772 """
1773 query = []
1774 _AppendForceIf(query, force)
1775 _AppendReason(query, reason)
1776
1777 return self._SendRequest(HTTP_POST,
1778 ("/%s/nodes/%s/powercycle" %
1779 (GANETI_RAPI_VERSION, node)), query, None)
1780
1781 - def ModifyNode(self, node, reason=None, **kwargs):
1782 """Modifies a node.
1783
1784 More details for parameters can be found in the RAPI documentation.
1785
1786 @type node: string
1787 @param node: Node name
1788 @type reason: string
1789 @param reason: the reason for executing this operation
1790 @rtype: string
1791 @return: job id
1792
1793 """
1794 query = []
1795 _AppendReason(query, reason)
1796
1797 return self._SendRequest(HTTP_POST,
1798 ("/%s/nodes/%s/modify" %
1799 (GANETI_RAPI_VERSION, node)), query, kwargs)
1800
1802 """Gets the storage units for a node.
1803
1804 @type node: str
1805 @param node: the node whose storage units to return
1806 @type storage_type: str
1807 @param storage_type: storage type whose units to return
1808 @type output_fields: str
1809 @param output_fields: storage type fields to return
1810 @type reason: string
1811 @param reason: the reason for executing this operation
1812
1813 @rtype: string
1814 @return: job id where results can be retrieved
1815
1816 """
1817 query = [
1818 ("storage_type", storage_type),
1819 ("output_fields", output_fields),
1820 ]
1821 _AppendReason(query, reason)
1822
1823 return self._SendRequest(HTTP_GET,
1824 ("/%s/nodes/%s/storage" %
1825 (GANETI_RAPI_VERSION, node)), query, None)
1826
1829 """Modifies parameters of storage units on the node.
1830
1831 @type node: str
1832 @param node: node whose storage units to modify
1833 @type storage_type: str
1834 @param storage_type: storage type whose units to modify
1835 @type name: str
1836 @param name: name of the storage unit
1837 @type allocatable: bool or None
1838 @param allocatable: Whether to set the "allocatable" flag on the storage
1839 unit (None=no modification, True=set, False=unset)
1840 @type reason: string
1841 @param reason: the reason for executing this operation
1842
1843 @rtype: string
1844 @return: job id
1845
1846 """
1847 query = [
1848 ("storage_type", storage_type),
1849 ("name", name),
1850 ]
1851
1852 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1853 _AppendReason(query, reason)
1854
1855 return self._SendRequest(HTTP_PUT,
1856 ("/%s/nodes/%s/storage/modify" %
1857 (GANETI_RAPI_VERSION, node)), query, None)
1858
1860 """Repairs a storage unit on the node.
1861
1862 @type node: str
1863 @param node: node whose storage units to repair
1864 @type storage_type: str
1865 @param storage_type: storage type to repair
1866 @type name: str
1867 @param name: name of the storage unit to repair
1868 @type reason: string
1869 @param reason: the reason for executing this operation
1870
1871 @rtype: string
1872 @return: job id
1873
1874 """
1875 query = [
1876 ("storage_type", storage_type),
1877 ("name", name),
1878 ]
1879 _AppendReason(query, reason)
1880
1881 return self._SendRequest(HTTP_PUT,
1882 ("/%s/nodes/%s/storage/repair" %
1883 (GANETI_RAPI_VERSION, node)), query, None)
1884
1903
1927
1951
1953 """Gets all networks in the cluster.
1954
1955 @type bulk: bool
1956 @param bulk: whether to return all information about the networks
1957
1958 @rtype: list of dict or str
1959 @return: if bulk is true, a list of dictionaries with info about all
1960 networks in the cluster, else a list of names of those networks
1961
1962 """
1963 query = []
1964 _AppendIf(query, bulk, ("bulk", 1))
1965 _AppendReason(query, reason)
1966
1967 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1968 query, None)
1969 if bulk:
1970 return networks
1971 else:
1972 return [n["name"] for n in networks]
1973
1975 """Gets information about a network.
1976
1977 @type network: str
1978 @param network: name of the network whose info to return
1979 @type reason: string
1980 @param reason: the reason for executing this operation
1981
1982 @rtype: dict
1983 @return: info about the network
1984
1985 """
1986 query = []
1987 _AppendReason(query, reason)
1988
1989 return self._SendRequest(HTTP_GET,
1990 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1991 query, None)
1992
1993 - def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1994 gateway6=None, mac_prefix=None,
1995 add_reserved_ips=None, tags=None, dry_run=False,
1996 reason=None):
1997 """Creates a new network.
1998
1999 @type network_name: str
2000 @param network_name: the name of network to create
2001 @type dry_run: bool
2002 @param dry_run: whether to peform a dry run
2003 @type reason: string
2004 @param reason: the reason for executing this operation
2005
2006 @rtype: string
2007 @return: job id
2008
2009 """
2010 query = []
2011 _AppendDryRunIf(query, dry_run)
2012 _AppendReason(query, reason)
2013
2014 if add_reserved_ips:
2015 add_reserved_ips = add_reserved_ips.split(",")
2016
2017 if tags:
2018 tags = tags.split(",")
2019
2020 body = {
2021 "network_name": network_name,
2022 "gateway": gateway,
2023 "network": network,
2024 "gateway6": gateway6,
2025 "network6": network6,
2026 "mac_prefix": mac_prefix,
2027 "add_reserved_ips": add_reserved_ips,
2028 "tags": tags,
2029 }
2030
2031 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2032 query, body)
2033
2034 - def ConnectNetwork(self, network_name, group_name, mode, link,
2035 vlan="", dry_run=False, reason=None):
2036 """Connects a Network to a NodeGroup with the given netparams
2037
2038 """
2039 body = {
2040 "group_name": group_name,
2041 "network_mode": mode,
2042 "network_link": link,
2043 "network_vlan": vlan,
2044 }
2045
2046 query = []
2047 _AppendDryRunIf(query, dry_run)
2048 _AppendReason(query, reason)
2049
2050 return self._SendRequest(HTTP_PUT,
2051 ("/%s/networks/%s/connect" %
2052 (GANETI_RAPI_VERSION, network_name)), query, body)
2053
2054 - def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2055 reason=None):
2070
2072 """Modifies a network.
2073
2074 More details for parameters can be found in the RAPI documentation.
2075
2076 @type network: string
2077 @param network: Network name
2078 @type reason: string
2079 @param reason: the reason for executing this operation
2080 @rtype: string
2081 @return: job id
2082
2083 """
2084 query = []
2085 _AppendReason(query, reason)
2086
2087 return self._SendRequest(HTTP_PUT,
2088 ("/%s/networks/%s/modify" %
2089 (GANETI_RAPI_VERSION, network)), None, kwargs)
2090
2092 """Deletes a network.
2093
2094 @type network: str
2095 @param network: the network to delete
2096 @type dry_run: bool
2097 @param dry_run: whether to peform a dry run
2098 @type reason: string
2099 @param reason: the reason for executing this operation
2100
2101 @rtype: string
2102 @return: job id
2103
2104 """
2105 query = []
2106 _AppendDryRunIf(query, dry_run)
2107 _AppendReason(query, reason)
2108
2109 return self._SendRequest(HTTP_DELETE,
2110 ("/%s/networks/%s" %
2111 (GANETI_RAPI_VERSION, network)), query, None)
2112
2131
2155
2178
2179 - def GetGroups(self, bulk=False, reason=None):
2180 """Gets all node groups in the cluster.
2181
2182 @type bulk: bool
2183 @param bulk: whether to return all information about the groups
2184 @type reason: string
2185 @param reason: the reason for executing this operation
2186
2187 @rtype: list of dict or str
2188 @return: if bulk is true, a list of dictionaries with info about all node
2189 groups in the cluster, else a list of names of those node groups
2190
2191 """
2192 query = []
2193 _AppendIf(query, bulk, ("bulk", 1))
2194 _AppendReason(query, reason)
2195
2196 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2197 query, None)
2198 if bulk:
2199 return groups
2200 else:
2201 return [g["name"] for g in groups]
2202
2203 - def GetGroup(self, group, reason=None):
2204 """Gets information about a node group.
2205
2206 @type group: str
2207 @param group: name of the node group whose info to return
2208 @type reason: string
2209 @param reason: the reason for executing this operation
2210
2211 @rtype: dict
2212 @return: info about the node group
2213
2214 """
2215 query = []
2216 _AppendReason(query, reason)
2217
2218 return self._SendRequest(HTTP_GET,
2219 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2220 query, None)
2221
2222 - def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2223 """Creates a new node group.
2224
2225 @type name: str
2226 @param name: the name of node group to create
2227 @type alloc_policy: str
2228 @param alloc_policy: the desired allocation policy for the group, if any
2229 @type dry_run: bool
2230 @param dry_run: whether to peform a dry run
2231 @type reason: string
2232 @param reason: the reason for executing this operation
2233
2234 @rtype: string
2235 @return: job id
2236
2237 """
2238 query = []
2239 _AppendDryRunIf(query, dry_run)
2240 _AppendReason(query, reason)
2241
2242 body = {
2243 "name": name,
2244 "alloc_policy": alloc_policy,
2245 }
2246
2247 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2248 query, body)
2249
2251 """Modifies a node group.
2252
2253 More details for parameters can be found in the RAPI documentation.
2254
2255 @type group: string
2256 @param group: Node group name
2257 @type reason: string
2258 @param reason: the reason for executing this operation
2259 @rtype: string
2260 @return: job id
2261
2262 """
2263 query = []
2264 _AppendReason(query, reason)
2265
2266 return self._SendRequest(HTTP_PUT,
2267 ("/%s/groups/%s/modify" %
2268 (GANETI_RAPI_VERSION, group)), query, kwargs)
2269
2270 - def DeleteGroup(self, group, dry_run=False, reason=None):
2271 """Deletes a node group.
2272
2273 @type group: str
2274 @param group: the node group to delete
2275 @type dry_run: bool
2276 @param dry_run: whether to peform a dry run
2277 @type reason: string
2278 @param reason: the reason for executing this operation
2279
2280 @rtype: string
2281 @return: job id
2282
2283 """
2284 query = []
2285 _AppendDryRunIf(query, dry_run)
2286 _AppendReason(query, reason)
2287
2288 return self._SendRequest(HTTP_DELETE,
2289 ("/%s/groups/%s" %
2290 (GANETI_RAPI_VERSION, group)), query, None)
2291
2293 """Changes the name of a node group.
2294
2295 @type group: string
2296 @param group: Node group name
2297 @type new_name: string
2298 @param new_name: New node group name
2299 @type reason: string
2300 @param reason: the reason for executing this operation
2301
2302 @rtype: string
2303 @return: job id
2304
2305 """
2306 body = {
2307 "new_name": new_name,
2308 }
2309
2310 query = []
2311 _AppendReason(query, reason)
2312
2313 return self._SendRequest(HTTP_PUT,
2314 ("/%s/groups/%s/rename" %
2315 (GANETI_RAPI_VERSION, group)), query, body)
2316
2317 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2318 reason=None):
2319 """Assigns nodes to a group.
2320
2321 @type group: string
2322 @param group: Node group name
2323 @type nodes: list of strings
2324 @param nodes: List of nodes to assign to the group
2325 @type reason: string
2326 @param reason: the reason for executing this operation
2327
2328 @rtype: string
2329 @return: job id
2330
2331 """
2332 query = []
2333 _AppendForceIf(query, force)
2334 _AppendDryRunIf(query, dry_run)
2335 _AppendReason(query, reason)
2336
2337 body = {
2338 "nodes": nodes,
2339 }
2340
2341 return self._SendRequest(HTTP_PUT,
2342 ("/%s/groups/%s/assign-nodes" %
2343 (GANETI_RAPI_VERSION, group)), query, body)
2344
2363
2387
2410
2411 - def Query(self, what, fields, qfilter=None, reason=None):
2412 """Retrieves information about resources.
2413
2414 @type what: string
2415 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2416 @type fields: list of string
2417 @param fields: Requested fields
2418 @type qfilter: None or list
2419 @param qfilter: Query filter
2420 @type reason: string
2421 @param reason: the reason for executing this operation
2422
2423 @rtype: string
2424 @return: job id
2425
2426 """
2427 query = []
2428 _AppendReason(query, reason)
2429
2430 body = {
2431 "fields": fields,
2432 }
2433
2434 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2435
2436 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2437
2438 return self._SendRequest(HTTP_PUT,
2439 ("/%s/query/%s" %
2440 (GANETI_RAPI_VERSION, what)), query, body)
2441
2442 - def QueryFields(self, what, fields=None, reason=None):
2443 """Retrieves available fields for a resource.
2444
2445 @type what: string
2446 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2447 @type fields: list of string
2448 @param fields: Requested fields
2449 @type reason: string
2450 @param reason: the reason for executing this operation
2451
2452 @rtype: string
2453 @return: job id
2454
2455 """
2456 query = []
2457 _AppendReason(query, reason)
2458
2459 if fields is not None:
2460 _AppendIf(query, True, ("fields", ",".join(fields)))
2461
2462 return self._SendRequest(HTTP_GET,
2463 ("/%s/query/%s/fields" %
2464 (GANETI_RAPI_VERSION, what)), query, None)
2465