1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """Ganeti RAPI client.
32
33 @attention: To use the RAPI client, the application B{must} call
34 C{pycurl.global_init} during initialization and
35 C{pycurl.global_cleanup} before exiting the process. This is very
36 important in multi-threaded programs. See curl_global_init(3) and
37 curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
38 can be used.
39
40 """
41
42
43
44
45 import logging
46 import simplejson
47 import socket
48 import urllib
49 import threading
50 import pycurl
51 import time
52
53 try:
54 from cStringIO import StringIO
55 except ImportError:
56 from StringIO import StringIO
57
58
59 GANETI_RAPI_PORT = 5080
60 GANETI_RAPI_VERSION = 2
61
62 HTTP_DELETE = "DELETE"
63 HTTP_GET = "GET"
64 HTTP_PUT = "PUT"
65 HTTP_POST = "POST"
66 HTTP_OK = 200
67 HTTP_NOT_FOUND = 404
68 HTTP_APP_JSON = "application/json"
69
70 REPLACE_DISK_PRI = "replace_on_primary"
71 REPLACE_DISK_SECONDARY = "replace_on_secondary"
72 REPLACE_DISK_CHG = "replace_new_secondary"
73 REPLACE_DISK_AUTO = "replace_auto"
74
75 NODE_EVAC_PRI = "primary-only"
76 NODE_EVAC_SEC = "secondary-only"
77 NODE_EVAC_ALL = "all"
78
79 NODE_ROLE_DRAINED = "drained"
80 NODE_ROLE_MASTER_CANDIDATE = "master-candidate"
81 NODE_ROLE_MASTER = "master"
82 NODE_ROLE_OFFLINE = "offline"
83 NODE_ROLE_REGULAR = "regular"
84
85 JOB_STATUS_QUEUED = "queued"
86 JOB_STATUS_WAITING = "waiting"
87 JOB_STATUS_CANCELING = "canceling"
88 JOB_STATUS_RUNNING = "running"
89 JOB_STATUS_CANCELED = "canceled"
90 JOB_STATUS_SUCCESS = "success"
91 JOB_STATUS_ERROR = "error"
92 JOB_STATUS_PENDING = frozenset([
93 JOB_STATUS_QUEUED,
94 JOB_STATUS_WAITING,
95 JOB_STATUS_CANCELING,
96 ])
97 JOB_STATUS_FINALIZED = frozenset([
98 JOB_STATUS_CANCELED,
99 JOB_STATUS_SUCCESS,
100 JOB_STATUS_ERROR,
101 ])
102 JOB_STATUS_ALL = frozenset([
103 JOB_STATUS_RUNNING,
104 ]) | JOB_STATUS_PENDING | JOB_STATUS_FINALIZED
105
106
107 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
108
109
110 _REQ_DATA_VERSION_FIELD = "__version__"
111 _QPARAM_DRY_RUN = "dry-run"
112 _QPARAM_FORCE = "force"
113
114
115 INST_CREATE_REQV1 = "instance-create-reqv1"
116 INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
117 NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
118 NODE_EVAC_RES1 = "node-evac-res1"
119
120
121 _INST_CREATE_REQV1 = INST_CREATE_REQV1
122 _INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
123 _NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
124 _NODE_EVAC_RES1 = NODE_EVAC_RES1
125
126
127 ECODE_RESOLVER = "resolver_error"
128
129
130 ECODE_NORES = "insufficient_resources"
131
132
133 ECODE_TEMP_NORES = "temp_insufficient_resources"
134
135
136 ECODE_INVAL = "wrong_input"
137
138
139 ECODE_STATE = "wrong_state"
140
141
142 ECODE_NOENT = "unknown_entity"
143
144
145 ECODE_EXISTS = "already_exists"
146
147
148 ECODE_NOTUNIQUE = "resource_not_unique"
149
150
151 ECODE_FAULT = "internal_error"
152
153
154 ECODE_ENVIRON = "environment_error"
155
156
157 ECODE_ALL = frozenset([
158 ECODE_RESOLVER,
159 ECODE_NORES,
160 ECODE_TEMP_NORES,
161 ECODE_INVAL,
162 ECODE_STATE,
163 ECODE_NOENT,
164 ECODE_EXISTS,
165 ECODE_NOTUNIQUE,
166 ECODE_FAULT,
167 ECODE_ENVIRON,
168 ])
169
170
171 try:
172 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
173 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
174 except AttributeError:
175 _CURLE_SSL_CACERT = 60
176 _CURLE_SSL_CACERT_BADFILE = 77
177
178 _CURL_SSL_CERT_ERRORS = frozenset([
179 _CURLE_SSL_CACERT,
180 _CURLE_SSL_CACERT_BADFILE,
181 ])
182
183
184 -class Error(Exception):
185 """Base error class for this module.
186
187 """
188 pass
189
192 """Generic error raised from Ganeti API.
193
194 """
198
201 """Raised when a problem is found with the SSL certificate.
202
203 """
204 pass
205
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 @type iallocator: str or None
987 @param iallocator: instance allocator plugin to use
988 @rtype: string
989 @return: job id
990
991 """
992 body = {}
993 _SetItemIf(body, disks is not None, "disks", disks)
994 _SetItemIf(body, nodes is not None, "nodes", nodes)
995 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
996
997 query = []
998 _AppendReason(query, reason)
999
1000 return self._SendRequest(HTTP_POST,
1001 ("/%s/instances/%s/recreate-disks" %
1002 (GANETI_RAPI_VERSION, instance)), query, body)
1003
1004 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
1005 reason=None):
1006 """Grows a disk of an instance.
1007
1008 More details for parameters can be found in the RAPI documentation.
1009
1010 @type instance: string
1011 @param instance: Instance name
1012 @type disk: integer
1013 @param disk: Disk index
1014 @type amount: integer
1015 @param amount: Grow disk by this amount (MiB)
1016 @type wait_for_sync: bool
1017 @param wait_for_sync: Wait for disk to synchronize
1018 @type reason: string
1019 @param reason: the reason for executing this operation
1020 @rtype: string
1021 @return: job id
1022
1023 """
1024 body = {
1025 "amount": amount,
1026 }
1027
1028 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1029
1030 query = []
1031 _AppendReason(query, reason)
1032
1033 return self._SendRequest(HTTP_POST,
1034 ("/%s/instances/%s/disk/%s/grow" %
1035 (GANETI_RAPI_VERSION, instance, disk)),
1036 query, body)
1037
1055
1079
1102
1103 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1104 dry_run=False, reason=None, **kwargs):
1105 """Reboots an instance.
1106
1107 @type instance: str
1108 @param instance: instance to reboot
1109 @type reboot_type: str
1110 @param reboot_type: one of: hard, soft, full
1111 @type ignore_secondaries: bool
1112 @param ignore_secondaries: if True, ignores errors for the secondary node
1113 while re-assembling disks (in hard-reboot mode only)
1114 @type dry_run: bool
1115 @param dry_run: whether to perform a dry run
1116 @type reason: string
1117 @param reason: the reason for the reboot
1118 @rtype: string
1119 @return: job id
1120
1121 """
1122 query = []
1123 body = kwargs
1124
1125 _AppendDryRunIf(query, dry_run)
1126 _AppendIf(query, reboot_type, ("type", reboot_type))
1127 _AppendIf(query, ignore_secondaries is not None,
1128 ("ignore_secondaries", ignore_secondaries))
1129 _AppendReason(query, reason)
1130
1131 return self._SendRequest(HTTP_POST,
1132 ("/%s/instances/%s/reboot" %
1133 (GANETI_RAPI_VERSION, instance)), query, body)
1134
1135 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1136 reason=None, **kwargs):
1137 """Shuts down an instance.
1138
1139 @type instance: str
1140 @param instance: the instance to shut down
1141 @type dry_run: bool
1142 @param dry_run: whether to perform a dry run
1143 @type no_remember: bool
1144 @param no_remember: if true, will not record the state change
1145 @type reason: string
1146 @param reason: the reason for the shutdown
1147 @rtype: string
1148 @return: job id
1149
1150 """
1151 query = []
1152 body = kwargs
1153
1154 _AppendDryRunIf(query, dry_run)
1155 _AppendIf(query, no_remember, ("no_remember", 1))
1156 _AppendReason(query, reason)
1157
1158 return self._SendRequest(HTTP_PUT,
1159 ("/%s/instances/%s/shutdown" %
1160 (GANETI_RAPI_VERSION, instance)), query, body)
1161
1162 - def StartupInstance(self, instance, dry_run=False, no_remember=False,
1163 reason=None):
1164 """Starts up an instance.
1165
1166 @type instance: str
1167 @param instance: the instance to start up
1168 @type dry_run: bool
1169 @param dry_run: whether to perform a dry run
1170 @type no_remember: bool
1171 @param no_remember: if true, will not record the state change
1172 @type reason: string
1173 @param reason: the reason for the startup
1174 @rtype: string
1175 @return: job id
1176
1177 """
1178 query = []
1179 _AppendDryRunIf(query, dry_run)
1180 _AppendIf(query, no_remember, ("no_remember", 1))
1181 _AppendReason(query, reason)
1182
1183 return self._SendRequest(HTTP_PUT,
1184 ("/%s/instances/%s/startup" %
1185 (GANETI_RAPI_VERSION, instance)), query, None)
1186
1187 - def ReinstallInstance(self, instance, os=None, no_startup=False,
1188 osparams=None, reason=None):
1189 """Reinstalls an instance.
1190
1191 @type instance: str
1192 @param instance: The instance to reinstall
1193 @type os: str or None
1194 @param os: The operating system to reinstall. If None, the instance's
1195 current operating system will be installed again
1196 @type no_startup: bool
1197 @param no_startup: Whether to start the instance automatically
1198 @type reason: string
1199 @param reason: the reason for executing this operation
1200 @rtype: string
1201 @return: job id
1202
1203 """
1204 query = []
1205 _AppendReason(query, reason)
1206
1207 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1208 body = {
1209 "start": not no_startup,
1210 }
1211 _SetItemIf(body, os is not None, "os", os)
1212 _SetItemIf(body, osparams is not None, "osparams", osparams)
1213 return self._SendRequest(HTTP_POST,
1214 ("/%s/instances/%s/reinstall" %
1215 (GANETI_RAPI_VERSION, instance)), query, body)
1216
1217
1218 if osparams:
1219 raise GanetiApiError("Server does not support specifying OS parameters"
1220 " for instance reinstallation")
1221
1222 query = []
1223 _AppendIf(query, os, ("os", os))
1224 _AppendIf(query, no_startup, ("nostartup", 1))
1225
1226 return self._SendRequest(HTTP_POST,
1227 ("/%s/instances/%s/reinstall" %
1228 (GANETI_RAPI_VERSION, instance)), query, None)
1229
1233 """Replaces disks on an instance.
1234
1235 @type instance: str
1236 @param instance: instance whose disks to replace
1237 @type disks: list of ints
1238 @param disks: Indexes of disks to replace
1239 @type mode: str
1240 @param mode: replacement mode to use (defaults to replace_auto)
1241 @type remote_node: str or None
1242 @param remote_node: new secondary node to use (for use with
1243 replace_new_secondary mode)
1244 @type iallocator: str or None
1245 @param iallocator: instance allocator plugin to use (for use with
1246 replace_auto mode)
1247 @type reason: string
1248 @param reason: the reason for executing this operation
1249 @type early_release: bool
1250 @param early_release: whether to release locks as soon as possible
1251
1252 @rtype: string
1253 @return: job id
1254
1255 """
1256 query = [
1257 ("mode", mode),
1258 ]
1259
1260
1261
1262 if disks is not None:
1263 _AppendIf(query, True,
1264 ("disks", ",".join(str(idx) for idx in disks)))
1265
1266 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1267 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1268 _AppendReason(query, reason)
1269 _AppendIf(query, early_release is not None,
1270 ("early_release", early_release))
1271
1272 return self._SendRequest(HTTP_POST,
1273 ("/%s/instances/%s/replace-disks" %
1274 (GANETI_RAPI_VERSION, instance)), query, None)
1275
1277 """Prepares an instance for an export.
1278
1279 @type instance: string
1280 @param instance: Instance name
1281 @type mode: string
1282 @param mode: Export mode
1283 @type reason: string
1284 @param reason: the reason for executing this operation
1285 @rtype: string
1286 @return: Job ID
1287
1288 """
1289 query = [("mode", mode)]
1290 _AppendReason(query, reason)
1291 return self._SendRequest(HTTP_PUT,
1292 ("/%s/instances/%s/prepare-export" %
1293 (GANETI_RAPI_VERSION, instance)), query, None)
1294
1295 - def ExportInstance(self, instance, mode, destination, shutdown=None,
1296 remove_instance=None, x509_key_name=None,
1297 destination_x509_ca=None, compress=None, reason=None):
1298 """Exports an instance.
1299
1300 @type instance: string
1301 @param instance: Instance name
1302 @type mode: string
1303 @param mode: Export mode
1304 @type reason: string
1305 @param reason: the reason for executing this operation
1306 @rtype: string
1307 @return: Job ID
1308
1309 """
1310 body = {
1311 "destination": destination,
1312 "mode": mode,
1313 }
1314
1315 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1316 _SetItemIf(body, remove_instance is not None,
1317 "remove_instance", remove_instance)
1318 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1319 _SetItemIf(body, destination_x509_ca is not None,
1320 "destination_x509_ca", destination_x509_ca)
1321 _SetItemIf(body, compress is not None, "compress", compress)
1322
1323 query = []
1324 _AppendReason(query, reason)
1325
1326 return self._SendRequest(HTTP_PUT,
1327 ("/%s/instances/%s/export" %
1328 (GANETI_RAPI_VERSION, instance)), query, body)
1329
1330 - def MigrateInstance(self, instance, mode=None, cleanup=None,
1331 target_node=None, reason=None):
1332 """Migrates an instance.
1333
1334 @type instance: string
1335 @param instance: Instance name
1336 @type mode: string
1337 @param mode: Migration mode
1338 @type cleanup: bool
1339 @param cleanup: Whether to clean up a previously failed migration
1340 @type target_node: string
1341 @param target_node: Target Node for externally mirrored instances
1342 @type reason: string
1343 @param reason: the reason for executing this operation
1344 @rtype: string
1345 @return: job id
1346
1347 """
1348 body = {}
1349 _SetItemIf(body, mode is not None, "mode", mode)
1350 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1351 _SetItemIf(body, target_node is not None, "target_node", target_node)
1352
1353 query = []
1354 _AppendReason(query, reason)
1355
1356 return self._SendRequest(HTTP_PUT,
1357 ("/%s/instances/%s/migrate" %
1358 (GANETI_RAPI_VERSION, instance)), query, body)
1359
1360 - def FailoverInstance(self, instance, iallocator=None,
1361 ignore_consistency=None, target_node=None, reason=None):
1362 """Does a failover of an instance.
1363
1364 @type instance: string
1365 @param instance: Instance name
1366 @type iallocator: string
1367 @param iallocator: Iallocator for deciding the target node for
1368 shared-storage instances
1369 @type ignore_consistency: bool
1370 @param ignore_consistency: Whether to ignore disk consistency
1371 @type target_node: string
1372 @param target_node: Target node for shared-storage instances
1373 @type reason: string
1374 @param reason: the reason for executing this operation
1375 @rtype: string
1376 @return: job id
1377
1378 """
1379 body = {}
1380 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1381 _SetItemIf(body, ignore_consistency is not None,
1382 "ignore_consistency", ignore_consistency)
1383 _SetItemIf(body, target_node is not None, "target_node", target_node)
1384
1385 query = []
1386 _AppendReason(query, reason)
1387
1388 return self._SendRequest(HTTP_PUT,
1389 ("/%s/instances/%s/failover" %
1390 (GANETI_RAPI_VERSION, instance)), query, body)
1391
1392 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1393 reason=None):
1394 """Changes the name of an instance.
1395
1396 @type instance: string
1397 @param instance: Instance name
1398 @type new_name: string
1399 @param new_name: New instance name
1400 @type ip_check: bool
1401 @param ip_check: Whether to ensure instance's IP address is inactive
1402 @type name_check: bool
1403 @param name_check: Whether to ensure instance's name is resolvable
1404 @type reason: string
1405 @param reason: the reason for executing this operation
1406 @rtype: string
1407 @return: job id
1408
1409 """
1410 body = {
1411 "new_name": new_name,
1412 }
1413
1414 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1415 _SetItemIf(body, name_check is not None, "name_check", name_check)
1416
1417 query = []
1418 _AppendReason(query, reason)
1419
1420 return self._SendRequest(HTTP_PUT,
1421 ("/%s/instances/%s/rename" %
1422 (GANETI_RAPI_VERSION, instance)), query, body)
1423
1425 """Request information for connecting to instance's console.
1426
1427 @type instance: string
1428 @param instance: Instance name
1429 @type reason: string
1430 @param reason: the reason for executing this operation
1431 @rtype: dict
1432 @return: dictionary containing information about instance's console
1433
1434 """
1435 query = []
1436 _AppendReason(query, reason)
1437 return self._SendRequest(HTTP_GET,
1438 ("/%s/instances/%s/console" %
1439 (GANETI_RAPI_VERSION, instance)), query, None)
1440
1442 """Gets all jobs for the cluster.
1443
1444 @type bulk: bool
1445 @param bulk: Whether to return detailed information about jobs.
1446 @rtype: list of int
1447 @return: List of job ids for the cluster or list of dicts with detailed
1448 information about the jobs if bulk parameter was true.
1449
1450 """
1451 query = []
1452 _AppendIf(query, bulk, ("bulk", 1))
1453
1454 if bulk:
1455 return self._SendRequest(HTTP_GET,
1456 "/%s/jobs" % GANETI_RAPI_VERSION,
1457 query, None)
1458 else:
1459 return [int(j["id"])
1460 for j in self._SendRequest(HTTP_GET,
1461 "/%s/jobs" % GANETI_RAPI_VERSION,
1462 None, None)]
1463
1465 """Gets the status of a job.
1466
1467 @type job_id: string
1468 @param job_id: job id whose status to query
1469
1470 @rtype: dict
1471 @return: job status
1472
1473 """
1474 return self._SendRequest(HTTP_GET,
1475 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1476 None, None)
1477
1479 """Polls cluster for job status until completion.
1480
1481 Completion is defined as any of the following states listed in
1482 L{JOB_STATUS_FINALIZED}.
1483
1484 @type job_id: string
1485 @param job_id: job id to watch
1486 @type period: int
1487 @param period: how often to poll for status (optional, default 5s)
1488 @type retries: int
1489 @param retries: how many time to poll before giving up
1490 (optional, default -1 means unlimited)
1491
1492 @rtype: bool
1493 @return: C{True} if job succeeded or C{False} if failed/status timeout
1494 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1495 possible; L{WaitForJobChange} returns immediately after a job changed and
1496 does not use polling
1497
1498 """
1499 while retries != 0:
1500 job_result = self.GetJobStatus(job_id)
1501
1502 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1503 return True
1504 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1505 return False
1506
1507 if period:
1508 time.sleep(period)
1509
1510 if retries > 0:
1511 retries -= 1
1512
1513 return False
1514
1516 """Waits for job changes.
1517
1518 @type job_id: string
1519 @param job_id: Job ID for which to wait
1520 @return: C{None} if no changes have been detected and a dict with two keys,
1521 C{job_info} and C{log_entries} otherwise.
1522 @rtype: dict
1523
1524 """
1525 body = {
1526 "fields": fields,
1527 "previous_job_info": prev_job_info,
1528 "previous_log_serial": prev_log_serial,
1529 }
1530
1531 return self._SendRequest(HTTP_GET,
1532 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1533 None, body)
1534
1535 - def CancelJob(self, job_id, dry_run=False):
1536 """Cancels a job.
1537
1538 @type job_id: string
1539 @param job_id: id of the job to delete
1540 @type dry_run: bool
1541 @param dry_run: whether to perform a dry run
1542 @rtype: tuple
1543 @return: tuple containing the result, and a message (bool, string)
1544
1545 """
1546 query = []
1547 _AppendDryRunIf(query, dry_run)
1548
1549 return self._SendRequest(HTTP_DELETE,
1550 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1551 query, None)
1552
1553 - def GetNodes(self, bulk=False, reason=None):
1554 """Gets all nodes in the cluster.
1555
1556 @type bulk: bool
1557 @param bulk: whether to return all information about all instances
1558 @type reason: string
1559 @param reason: the reason for executing this operation
1560
1561 @rtype: list of dict or str
1562 @return: if bulk is true, info about nodes in the cluster,
1563 else list of nodes in the cluster
1564
1565 """
1566 query = []
1567 _AppendIf(query, bulk, ("bulk", 1))
1568 _AppendReason(query, reason)
1569
1570 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1571 query, None)
1572 if bulk:
1573 return nodes
1574 else:
1575 return [n["id"] for n in nodes]
1576
1577 - def GetNode(self, node, reason=None):
1578 """Gets information about a node.
1579
1580 @type node: str
1581 @param node: node whose info to return
1582 @type reason: string
1583 @param reason: the reason for executing this operation
1584
1585 @rtype: dict
1586 @return: info about the node
1587
1588 """
1589 query = []
1590 _AppendReason(query, reason)
1591
1592 return self._SendRequest(HTTP_GET,
1593 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1594 query, None)
1595
1596 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1597 dry_run=False, early_release=None,
1598 mode=None, accept_old=False, reason=None):
1599 """Evacuates instances from a Ganeti node.
1600
1601 @type node: str
1602 @param node: node to evacuate
1603 @type iallocator: str or None
1604 @param iallocator: instance allocator to use
1605 @type remote_node: str
1606 @param remote_node: node to evaucate to
1607 @type dry_run: bool
1608 @param dry_run: whether to perform a dry run
1609 @type early_release: bool
1610 @param early_release: whether to enable parallelization
1611 @type mode: string
1612 @param mode: Node evacuation mode
1613 @type accept_old: bool
1614 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1615 results
1616 @type reason: string
1617 @param reason: the reason for executing this operation
1618
1619 @rtype: string, or a list for pre-2.5 results
1620 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1621 list of (job ID, instance name, new secondary node); if dry_run was
1622 specified, then the actual move jobs were not submitted and the job IDs
1623 will be C{None}
1624
1625 @raises GanetiApiError: if an iallocator and remote_node are both
1626 specified
1627
1628 """
1629 if iallocator and remote_node:
1630 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1631
1632 query = []
1633 _AppendDryRunIf(query, dry_run)
1634 _AppendReason(query, reason)
1635
1636 if _NODE_EVAC_RES1 in self.GetFeatures():
1637
1638 body = {}
1639
1640 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1641 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1642 _SetItemIf(body, early_release is not None,
1643 "early_release", early_release)
1644 _SetItemIf(body, mode is not None, "mode", mode)
1645 else:
1646
1647 body = None
1648
1649 if not accept_old:
1650 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1651 " not accept old-style results (parameter"
1652 " accept_old)")
1653
1654
1655 if mode is not None and mode != NODE_EVAC_SEC:
1656 raise GanetiApiError("Server can only evacuate secondary instances")
1657
1658 _AppendIf(query, iallocator, ("iallocator", iallocator))
1659 _AppendIf(query, remote_node, ("remote_node", remote_node))
1660 _AppendIf(query, early_release, ("early_release", 1))
1661
1662 return self._SendRequest(HTTP_POST,
1663 ("/%s/nodes/%s/evacuate" %
1664 (GANETI_RAPI_VERSION, node)), query, body)
1665
1666 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1667 target_node=None, reason=None):
1668 """Migrates all primary instances from a node.
1669
1670 @type node: str
1671 @param node: node to migrate
1672 @type mode: string
1673 @param mode: if passed, it will overwrite the live migration type,
1674 otherwise the hypervisor default will be used
1675 @type dry_run: bool
1676 @param dry_run: whether to perform a dry run
1677 @type iallocator: string
1678 @param iallocator: instance allocator to use
1679 @type target_node: string
1680 @param target_node: Target node for shared-storage instances
1681 @type reason: string
1682 @param reason: the reason for executing this operation
1683
1684 @rtype: string
1685 @return: job id
1686
1687 """
1688 query = []
1689 _AppendDryRunIf(query, dry_run)
1690 _AppendReason(query, reason)
1691
1692 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1693 body = {}
1694
1695 _SetItemIf(body, mode is not None, "mode", mode)
1696 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1697 _SetItemIf(body, target_node is not None, "target_node", target_node)
1698
1699 assert len(query) <= 1
1700
1701 return self._SendRequest(HTTP_POST,
1702 ("/%s/nodes/%s/migrate" %
1703 (GANETI_RAPI_VERSION, node)), query, body)
1704 else:
1705
1706 if target_node is not None:
1707 raise GanetiApiError("Server does not support specifying target node"
1708 " for node migration")
1709
1710 _AppendIf(query, mode is not None, ("mode", mode))
1711
1712 return self._SendRequest(HTTP_POST,
1713 ("/%s/nodes/%s/migrate" %
1714 (GANETI_RAPI_VERSION, node)), query, None)
1715
1717 """Gets the current role for a node.
1718
1719 @type node: str
1720 @param node: node whose role to return
1721 @type reason: string
1722 @param reason: the reason for executing this operation
1723
1724 @rtype: str
1725 @return: the current role for a node
1726
1727 """
1728 query = []
1729 _AppendReason(query, reason)
1730
1731 return self._SendRequest(HTTP_GET,
1732 ("/%s/nodes/%s/role" %
1733 (GANETI_RAPI_VERSION, node)), query, None)
1734
1735 - def SetNodeRole(self, node, role, force=False, auto_promote=None,
1736 reason=None):
1737 """Sets the role for a node.
1738
1739 @type node: str
1740 @param node: the node whose role to set
1741 @type role: str
1742 @param role: the role to set for the node
1743 @type force: bool
1744 @param force: whether to force the role change
1745 @type auto_promote: bool
1746 @param auto_promote: Whether node(s) should be promoted to master candidate
1747 if necessary
1748 @type reason: string
1749 @param reason: the reason for executing this operation
1750
1751 @rtype: string
1752 @return: job id
1753
1754 """
1755 query = []
1756 _AppendForceIf(query, force)
1757 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1758 _AppendReason(query, reason)
1759
1760 return self._SendRequest(HTTP_PUT,
1761 ("/%s/nodes/%s/role" %
1762 (GANETI_RAPI_VERSION, node)), query, role)
1763
1765 """Powercycles a node.
1766
1767 @type node: string
1768 @param node: Node name
1769 @type force: bool
1770 @param force: Whether to force the operation
1771 @type reason: string
1772 @param reason: the reason for executing this operation
1773 @rtype: string
1774 @return: job id
1775
1776 """
1777 query = []
1778 _AppendForceIf(query, force)
1779 _AppendReason(query, reason)
1780
1781 return self._SendRequest(HTTP_POST,
1782 ("/%s/nodes/%s/powercycle" %
1783 (GANETI_RAPI_VERSION, node)), query, None)
1784
1785 - def ModifyNode(self, node, reason=None, **kwargs):
1786 """Modifies a node.
1787
1788 More details for parameters can be found in the RAPI documentation.
1789
1790 @type node: string
1791 @param node: Node name
1792 @type reason: string
1793 @param reason: the reason for executing this operation
1794 @rtype: string
1795 @return: job id
1796
1797 """
1798 query = []
1799 _AppendReason(query, reason)
1800
1801 return self._SendRequest(HTTP_POST,
1802 ("/%s/nodes/%s/modify" %
1803 (GANETI_RAPI_VERSION, node)), query, kwargs)
1804
1806 """Gets the storage units for a node.
1807
1808 @type node: str
1809 @param node: the node whose storage units to return
1810 @type storage_type: str
1811 @param storage_type: storage type whose units to return
1812 @type output_fields: str
1813 @param output_fields: storage type fields to return
1814 @type reason: string
1815 @param reason: the reason for executing this operation
1816
1817 @rtype: string
1818 @return: job id where results can be retrieved
1819
1820 """
1821 query = [
1822 ("storage_type", storage_type),
1823 ("output_fields", output_fields),
1824 ]
1825 _AppendReason(query, reason)
1826
1827 return self._SendRequest(HTTP_GET,
1828 ("/%s/nodes/%s/storage" %
1829 (GANETI_RAPI_VERSION, node)), query, None)
1830
1833 """Modifies parameters of storage units on the node.
1834
1835 @type node: str
1836 @param node: node whose storage units to modify
1837 @type storage_type: str
1838 @param storage_type: storage type whose units to modify
1839 @type name: str
1840 @param name: name of the storage unit
1841 @type allocatable: bool or None
1842 @param allocatable: Whether to set the "allocatable" flag on the storage
1843 unit (None=no modification, True=set, False=unset)
1844 @type reason: string
1845 @param reason: the reason for executing this operation
1846
1847 @rtype: string
1848 @return: job id
1849
1850 """
1851 query = [
1852 ("storage_type", storage_type),
1853 ("name", name),
1854 ]
1855
1856 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1857 _AppendReason(query, reason)
1858
1859 return self._SendRequest(HTTP_PUT,
1860 ("/%s/nodes/%s/storage/modify" %
1861 (GANETI_RAPI_VERSION, node)), query, None)
1862
1864 """Repairs a storage unit on the node.
1865
1866 @type node: str
1867 @param node: node whose storage units to repair
1868 @type storage_type: str
1869 @param storage_type: storage type to repair
1870 @type name: str
1871 @param name: name of the storage unit to repair
1872 @type reason: string
1873 @param reason: the reason for executing this operation
1874
1875 @rtype: string
1876 @return: job id
1877
1878 """
1879 query = [
1880 ("storage_type", storage_type),
1881 ("name", name),
1882 ]
1883 _AppendReason(query, reason)
1884
1885 return self._SendRequest(HTTP_PUT,
1886 ("/%s/nodes/%s/storage/repair" %
1887 (GANETI_RAPI_VERSION, node)), query, None)
1888
1907
1931
1955
1957 """Gets all networks in the cluster.
1958
1959 @type bulk: bool
1960 @param bulk: whether to return all information about the networks
1961
1962 @rtype: list of dict or str
1963 @return: if bulk is true, a list of dictionaries with info about all
1964 networks in the cluster, else a list of names of those networks
1965
1966 """
1967 query = []
1968 _AppendIf(query, bulk, ("bulk", 1))
1969 _AppendReason(query, reason)
1970
1971 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1972 query, None)
1973 if bulk:
1974 return networks
1975 else:
1976 return [n["name"] for n in networks]
1977
1979 """Gets information about a network.
1980
1981 @type network: str
1982 @param network: name of the network whose info to return
1983 @type reason: string
1984 @param reason: the reason for executing this operation
1985
1986 @rtype: dict
1987 @return: info about the network
1988
1989 """
1990 query = []
1991 _AppendReason(query, reason)
1992
1993 return self._SendRequest(HTTP_GET,
1994 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1995 query, None)
1996
1997 - def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1998 gateway6=None, mac_prefix=None,
1999 add_reserved_ips=None, tags=None, dry_run=False,
2000 reason=None):
2001 """Creates a new network.
2002
2003 @type network_name: str
2004 @param network_name: the name of network to create
2005 @type dry_run: bool
2006 @param dry_run: whether to peform a dry run
2007 @type reason: string
2008 @param reason: the reason for executing this operation
2009
2010 @rtype: string
2011 @return: job id
2012
2013 """
2014 query = []
2015 _AppendDryRunIf(query, dry_run)
2016 _AppendReason(query, reason)
2017
2018 if add_reserved_ips:
2019 add_reserved_ips = add_reserved_ips.split(",")
2020
2021 if tags:
2022 tags = tags.split(",")
2023
2024 body = {
2025 "network_name": network_name,
2026 "gateway": gateway,
2027 "network": network,
2028 "gateway6": gateway6,
2029 "network6": network6,
2030 "mac_prefix": mac_prefix,
2031 "add_reserved_ips": add_reserved_ips,
2032 "tags": tags,
2033 }
2034
2035 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2036 query, body)
2037
2038 - def ConnectNetwork(self, network_name, group_name, mode, link,
2039 vlan="", dry_run=False, reason=None):
2040 """Connects a Network to a NodeGroup with the given netparams
2041
2042 """
2043 body = {
2044 "group_name": group_name,
2045 "network_mode": mode,
2046 "network_link": link,
2047 "network_vlan": vlan,
2048 }
2049
2050 query = []
2051 _AppendDryRunIf(query, dry_run)
2052 _AppendReason(query, reason)
2053
2054 return self._SendRequest(HTTP_PUT,
2055 ("/%s/networks/%s/connect" %
2056 (GANETI_RAPI_VERSION, network_name)), query, body)
2057
2058 - def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2059 reason=None):
2074
2076 """Modifies a network.
2077
2078 More details for parameters can be found in the RAPI documentation.
2079
2080 @type network: string
2081 @param network: Network name
2082 @type reason: string
2083 @param reason: the reason for executing this operation
2084 @rtype: string
2085 @return: job id
2086
2087 """
2088 query = []
2089 _AppendReason(query, reason)
2090
2091 return self._SendRequest(HTTP_PUT,
2092 ("/%s/networks/%s/modify" %
2093 (GANETI_RAPI_VERSION, network)), None, kwargs)
2094
2096 """Deletes a network.
2097
2098 @type network: str
2099 @param network: the network to delete
2100 @type dry_run: bool
2101 @param dry_run: whether to peform a dry run
2102 @type reason: string
2103 @param reason: the reason for executing this operation
2104
2105 @rtype: string
2106 @return: job id
2107
2108 """
2109 query = []
2110 _AppendDryRunIf(query, dry_run)
2111 _AppendReason(query, reason)
2112
2113 return self._SendRequest(HTTP_DELETE,
2114 ("/%s/networks/%s" %
2115 (GANETI_RAPI_VERSION, network)), query, None)
2116
2135
2159
2182
2183 - def GetGroups(self, bulk=False, reason=None):
2184 """Gets all node groups in the cluster.
2185
2186 @type bulk: bool
2187 @param bulk: whether to return all information about the groups
2188 @type reason: string
2189 @param reason: the reason for executing this operation
2190
2191 @rtype: list of dict or str
2192 @return: if bulk is true, a list of dictionaries with info about all node
2193 groups in the cluster, else a list of names of those node groups
2194
2195 """
2196 query = []
2197 _AppendIf(query, bulk, ("bulk", 1))
2198 _AppendReason(query, reason)
2199
2200 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2201 query, None)
2202 if bulk:
2203 return groups
2204 else:
2205 return [g["name"] for g in groups]
2206
2207 - def GetGroup(self, group, reason=None):
2208 """Gets information about a node group.
2209
2210 @type group: str
2211 @param group: name of the node group whose info to return
2212 @type reason: string
2213 @param reason: the reason for executing this operation
2214
2215 @rtype: dict
2216 @return: info about the node group
2217
2218 """
2219 query = []
2220 _AppendReason(query, reason)
2221
2222 return self._SendRequest(HTTP_GET,
2223 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2224 query, None)
2225
2226 - def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2227 """Creates a new node group.
2228
2229 @type name: str
2230 @param name: the name of node group to create
2231 @type alloc_policy: str
2232 @param alloc_policy: the desired allocation policy for the group, if any
2233 @type dry_run: bool
2234 @param dry_run: whether to peform a dry run
2235 @type reason: string
2236 @param reason: the reason for executing this operation
2237
2238 @rtype: string
2239 @return: job id
2240
2241 """
2242 query = []
2243 _AppendDryRunIf(query, dry_run)
2244 _AppendReason(query, reason)
2245
2246 body = {
2247 "name": name,
2248 "alloc_policy": alloc_policy,
2249 }
2250
2251 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2252 query, body)
2253
2255 """Modifies a node group.
2256
2257 More details for parameters can be found in the RAPI documentation.
2258
2259 @type group: string
2260 @param group: Node group name
2261 @type reason: string
2262 @param reason: the reason for executing this operation
2263 @rtype: string
2264 @return: job id
2265
2266 """
2267 query = []
2268 _AppendReason(query, reason)
2269
2270 return self._SendRequest(HTTP_PUT,
2271 ("/%s/groups/%s/modify" %
2272 (GANETI_RAPI_VERSION, group)), query, kwargs)
2273
2274 - def DeleteGroup(self, group, dry_run=False, reason=None):
2275 """Deletes a node group.
2276
2277 @type group: str
2278 @param group: the node group to delete
2279 @type dry_run: bool
2280 @param dry_run: whether to peform a dry run
2281 @type reason: string
2282 @param reason: the reason for executing this operation
2283
2284 @rtype: string
2285 @return: job id
2286
2287 """
2288 query = []
2289 _AppendDryRunIf(query, dry_run)
2290 _AppendReason(query, reason)
2291
2292 return self._SendRequest(HTTP_DELETE,
2293 ("/%s/groups/%s" %
2294 (GANETI_RAPI_VERSION, group)), query, None)
2295
2297 """Changes the name of a node group.
2298
2299 @type group: string
2300 @param group: Node group name
2301 @type new_name: string
2302 @param new_name: New node group name
2303 @type reason: string
2304 @param reason: the reason for executing this operation
2305
2306 @rtype: string
2307 @return: job id
2308
2309 """
2310 body = {
2311 "new_name": new_name,
2312 }
2313
2314 query = []
2315 _AppendReason(query, reason)
2316
2317 return self._SendRequest(HTTP_PUT,
2318 ("/%s/groups/%s/rename" %
2319 (GANETI_RAPI_VERSION, group)), query, body)
2320
2321 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2322 reason=None):
2323 """Assigns nodes to a group.
2324
2325 @type group: string
2326 @param group: Node group name
2327 @type nodes: list of strings
2328 @param nodes: List of nodes to assign to the group
2329 @type reason: string
2330 @param reason: the reason for executing this operation
2331
2332 @rtype: string
2333 @return: job id
2334
2335 """
2336 query = []
2337 _AppendForceIf(query, force)
2338 _AppendDryRunIf(query, dry_run)
2339 _AppendReason(query, reason)
2340
2341 body = {
2342 "nodes": nodes,
2343 }
2344
2345 return self._SendRequest(HTTP_PUT,
2346 ("/%s/groups/%s/assign-nodes" %
2347 (GANETI_RAPI_VERSION, group)), query, body)
2348
2367
2391
2414
2415 - def Query(self, what, fields, qfilter=None, reason=None):
2416 """Retrieves information about resources.
2417
2418 @type what: string
2419 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2420 @type fields: list of string
2421 @param fields: Requested fields
2422 @type qfilter: None or list
2423 @param qfilter: Query filter
2424 @type reason: string
2425 @param reason: the reason for executing this operation
2426
2427 @rtype: string
2428 @return: job id
2429
2430 """
2431 query = []
2432 _AppendReason(query, reason)
2433
2434 body = {
2435 "fields": fields,
2436 }
2437
2438 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2439
2440 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2441
2442 return self._SendRequest(HTTP_PUT,
2443 ("/%s/query/%s" %
2444 (GANETI_RAPI_VERSION, what)), query, body)
2445
2446 - def QueryFields(self, what, fields=None, reason=None):
2447 """Retrieves available fields for a resource.
2448
2449 @type what: string
2450 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2451 @type fields: list of string
2452 @param fields: Requested fields
2453 @type reason: string
2454 @param reason: the reason for executing this operation
2455
2456 @rtype: string
2457 @return: job id
2458
2459 """
2460 query = []
2461 _AppendReason(query, reason)
2462
2463 if fields is not None:
2464 _AppendIf(query, True, ("fields", ",".join(fields)))
2465
2466 return self._SendRequest(HTTP_GET,
2467 ("/%s/query/%s/fields" %
2468 (GANETI_RAPI_VERSION, what)), query, None)
2469