1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Ganeti RAPI client."""
23
24
25
26
27 import sys
28 import httplib
29 import urllib2
30 import logging
31 import simplejson
32 import socket
33 import urllib
34 import OpenSSL
35 import distutils.version
36
37
38 GANETI_RAPI_PORT = 5080
39 GANETI_RAPI_VERSION = 2
40
41 HTTP_DELETE = "DELETE"
42 HTTP_GET = "GET"
43 HTTP_PUT = "PUT"
44 HTTP_POST = "POST"
45 HTTP_OK = 200
46 HTTP_NOT_FOUND = 404
47 HTTP_APP_JSON = "application/json"
48
49 REPLACE_DISK_PRI = "replace_on_primary"
50 REPLACE_DISK_SECONDARY = "replace_on_secondary"
51 REPLACE_DISK_CHG = "replace_new_secondary"
52 REPLACE_DISK_AUTO = "replace_auto"
53
54 NODE_ROLE_DRAINED = "drained"
55 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
56 NODE_ROLE_MASTER = "master"
57 NODE_ROLE_OFFLINE = "offline"
58 NODE_ROLE_REGULAR = "regular"
59
60
61 _REQ_DATA_VERSION_FIELD = "__version__"
62 _INST_CREATE_REQV1 = "instance-create-reqv1"
63 _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
64 _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
65 _INST_CREATE_V0_PARAMS = frozenset([
66 "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
67 "hypervisor", "file_storage_dir", "file_driver", "dry_run",
68 ])
69 _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
70
71
72 -class Error(Exception):
73 """Base error class for this module.
74
75 """
76 pass
77
80 """Raised when a problem is found with the SSL certificate.
81
82 """
83 pass
84
87 """Generic error raised from Ganeti API.
88
89 """
93
109
112 """Certificate verificator for SSL context.
113
114 Configures SSL context to verify server's certificate.
115
116 """
117 _CAPATH_MINVERSION = "0.9"
118 _DEFVFYPATHS_MINVERSION = "0.9"
119
120 _PYOPENSSL_VERSION = OpenSSL.__version__
121 _PARSED_PYOPENSSL_VERSION = distutils.version.LooseVersion(_PYOPENSSL_VERSION)
122
123 _SUPPORT_CAPATH = (_PARSED_PYOPENSSL_VERSION >= _CAPATH_MINVERSION)
124 _SUPPORT_DEFVFYPATHS = (_PARSED_PYOPENSSL_VERSION >= _DEFVFYPATHS_MINVERSION)
125
126 - def __init__(self, cafile=None, capath=None, use_default_verify_paths=False):
127 """Initializes this class.
128
129 @type cafile: string
130 @param cafile: In which file we can find the certificates
131 @type capath: string
132 @param capath: In which directory we can find the certificates
133 @type use_default_verify_paths: bool
134 @param use_default_verify_paths: Whether the platform provided CA
135 certificates are to be used for
136 verification purposes
137
138 """
139 self._cafile = cafile
140 self._capath = capath
141 self._use_default_verify_paths = use_default_verify_paths
142
143 if self._capath is not None and not self._SUPPORT_CAPATH:
144 raise Error(("PyOpenSSL %s has no support for a CA directory,"
145 " version %s or above is required") %
146 (self._PYOPENSSL_VERSION, self._CAPATH_MINVERSION))
147
148 if self._use_default_verify_paths and not self._SUPPORT_DEFVFYPATHS:
149 raise Error(("PyOpenSSL %s has no support for using default verification"
150 " paths, version %s or above is required") %
151 (self._PYOPENSSL_VERSION, self._DEFVFYPATHS_MINVERSION))
152
153 @staticmethod
155 """Callback for SSL certificate verification.
156
157 @param logger: Logging object
158
159 """
160 if ok:
161 log_fn = logger.debug
162 else:
163 log_fn = logger.error
164
165 log_fn("Verifying SSL certificate at depth %s, subject '%s', issuer '%s'",
166 errdepth, FormatX509Name(cert.get_subject()),
167 FormatX509Name(cert.get_issuer()))
168
169 if not ok:
170 try:
171
172
173 fn = OpenSSL.crypto.X509_verify_cert_error_string
174 except AttributeError:
175 errmsg = ""
176 else:
177 errmsg = ":%s" % fn(errnum)
178
179 logger.error("verify error:num=%s%s", errnum, errmsg)
180
181 return ok
182
184 """Configures an SSL context to verify certificates.
185
186 @type ctx: OpenSSL.SSL.Context
187 @param ctx: SSL context
188
189 """
190 if self._use_default_verify_paths:
191 ctx.set_default_verify_paths()
192
193 if self._cafile or self._capath:
194 if self._SUPPORT_CAPATH:
195 ctx.load_verify_locations(self._cafile, self._capath)
196 else:
197 ctx.load_verify_locations(self._cafile)
198
199 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER,
200 lambda conn, cert, errnum, errdepth, ok: \
201 self._VerifySslCertCb(logger, conn, cert,
202 errnum, errdepth, ok))
203
206 """HTTPS Connection handler that verifies the SSL certificate.
207
208 """
209
210
211 _SUPPORT_FAKESOCKET = (sys.hexversion < 0x2060000)
212
214 """Initializes this class.
215
216 """
217 httplib.HTTPSConnection.__init__(self, *args, **kwargs)
218 self._logger = None
219 self._config_ssl_verification = None
220
221 - def Setup(self, logger, config_ssl_verification):
222 """Sets the SSL verification config function.
223
224 @param logger: Logging object
225 @type config_ssl_verification: callable
226
227 """
228 assert self._logger is None
229 assert self._config_ssl_verification is None
230
231 self._logger = logger
232 self._config_ssl_verification = config_ssl_verification
233
235 """Connect to the server specified when the object was created.
236
237 This ensures that SSL certificates are verified.
238
239 """
240 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
241
242 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
243 ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
244
245 if self._config_ssl_verification:
246 self._config_ssl_verification(ctx, self._logger)
247
248 ssl = OpenSSL.SSL.Connection(ctx, sock)
249 ssl.connect((self.host, self.port))
250
251 if self._SUPPORT_FAKESOCKET:
252 self.sock = httplib.FakeSocket(sock, ssl)
253 else:
254 self.sock = _SslSocketWrapper(ssl)
255
259 """Initializes this class.
260
261 """
262 self._sock = sock
263
265 """Forward everything to underlying socket.
266
267 """
268 return getattr(self._sock, name)
269
271 """Fake makefile method.
272
273 makefile() on normal file descriptors uses dup2(2), which doesn't work with
274 SSL sockets and therefore is not implemented by pyOpenSSL. This fake method
275 works with the httplib module, but might not work for other modules.
276
277 """
278
279 return socket._fileobject(self._sock, mode, bufsize)
280
283 - def __init__(self, logger, config_ssl_verification):
284 """Initializes this class.
285
286 @param logger: Logging object
287 @type config_ssl_verification: callable
288 @param config_ssl_verification: Function to configure SSL context for
289 certificate verification
290
291 """
292 urllib2.HTTPSHandler.__init__(self)
293 self._logger = logger
294 self._config_ssl_verification = config_ssl_verification
295
297 """Wrapper around L{_HTTPSConnectionOpenSSL} to add SSL verification.
298
299 This wrapper is necessary provide a compatible API to urllib2.
300
301 """
302 conn = _HTTPSConnectionOpenSSL(*args, **kwargs)
303 conn.Setup(self._logger, self._config_ssl_verification)
304 return conn
305
307 """Creates HTTPS connection.
308
309 Called by urllib2.
310
311 """
312 return self.do_open(self._CreateHttpsConnection, req)
313
316 - def __init__(self, method, url, headers, data):
317 """Initializes this class.
318
319 """
320 urllib2.Request.__init__(self, url, data=data, headers=headers)
321 self._method = method
322
324 """Returns the HTTP request method.
325
326 """
327 return self._method
328
331 """Ganeti RAPI client.
332
333 """
334 USER_AGENT = "Ganeti RAPI Client"
335 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
336
337 - def __init__(self, host, port=GANETI_RAPI_PORT,
338 username=None, password=None,
339 config_ssl_verification=None, ignore_proxy=False,
340 logger=logging):
341 """Constructor.
342
343 @type host: string
344 @param host: the ganeti cluster master to interact with
345 @type port: int
346 @param port: the port on which the RAPI is running (default is 5080)
347 @type username: string
348 @param username: the username to connect with
349 @type password: string
350 @param password: the password to connect with
351 @type config_ssl_verification: callable
352 @param config_ssl_verification: Function to configure SSL context for
353 certificate verification
354 @type ignore_proxy: bool
355 @param ignore_proxy: Whether to ignore proxy settings
356 @param logger: Logging object
357
358 """
359 self._host = host
360 self._port = port
361 self._logger = logger
362
363 self._base_url = "https://%s:%s" % (host, port)
364
365 handlers = [_HTTPSHandler(self._logger, config_ssl_verification)]
366
367 self._httpauthhandler = None
368 if username is not None:
369 pwmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
370 pwmgr.add_password(None, self._base_url, username, password)
371 self._httpauthhandler = urllib2.HTTPBasicAuthHandler(pwmgr)
372 handlers.append(self._httpauthhandler)
373 elif password:
374 raise Error("Specified password without username")
375
376 if ignore_proxy:
377 handlers.append(urllib2.ProxyHandler({}))
378
379 self._http = urllib2.build_opener(*handlers)
380
381 self._headers = {
382 "Accept": HTTP_APP_JSON,
383 "Content-type": HTTP_APP_JSON,
384 "User-Agent": self.USER_AGENT,
385 }
386
387 @staticmethod
389 """Encode query values for RAPI URL.
390
391 @type query: list of two-tuples
392 @param query: Query arguments
393 @rtype: list
394 @return: Query list with encoded values
395
396 """
397 result = []
398
399 for name, value in query:
400 if value is None:
401 result.append((name, ""))
402
403 elif isinstance(value, bool):
404
405 result.append((name, int(value)))
406
407 elif isinstance(value, (list, tuple, dict)):
408 raise ValueError("Invalid query data type %r" % type(value).__name__)
409
410 else:
411 result.append((name, value))
412
413 return result
414
416 """Sends an HTTP request.
417
418 This constructs a full URL, encodes and decodes HTTP bodies, and
419 handles invalid responses in a pythonic way.
420
421 @type method: string
422 @param method: HTTP method to use
423 @type path: string
424 @param path: HTTP URL path
425 @type query: list of two-tuples
426 @param query: query arguments to pass to urllib.urlencode
427 @type content: str or None
428 @param content: HTTP body content
429
430 @rtype: str
431 @return: JSON-Decoded response
432
433 @raises CertificateError: If an invalid SSL certificate is found
434 @raises GanetiApiError: If an invalid response is returned
435
436 """
437 assert path.startswith("/")
438
439 if content:
440 encoded_content = self._json_encoder.encode(content)
441 else:
442 encoded_content = None
443
444
445 urlparts = [self._base_url, path]
446 if query:
447 urlparts.append("?")
448 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
449
450 url = "".join(urlparts)
451
452 self._logger.debug("Sending request %s %s to %s:%s"
453 " (headers=%r, content=%r)",
454 method, url, self._host, self._port, self._headers,
455 encoded_content)
456
457 req = _RapiRequest(method, url, self._headers, encoded_content)
458
459 try:
460 resp = self._http.open(req)
461 encoded_response_content = resp.read()
462
463
464
465 if self._httpauthhandler is not None:
466 self._httpauthhandler.retried = 0
467 except (OpenSSL.SSL.Error, OpenSSL.crypto.Error), err:
468 raise CertificateError("SSL issue: %s (%r)" % (err, err))
469 except urllib2.HTTPError, err:
470 raise GanetiApiError(str(err), code=err.code)
471 except urllib2.URLError, err:
472 raise GanetiApiError(str(err))
473
474 if encoded_response_content:
475 response_content = simplejson.loads(encoded_response_content)
476 else:
477 response_content = None
478
479
480 if resp.code != HTTP_OK:
481 if isinstance(response_content, dict):
482 msg = ("%s %s: %s" %
483 (response_content["code"],
484 response_content["message"],
485 response_content["explain"]))
486 else:
487 msg = str(response_content)
488
489 raise GanetiApiError(msg, code=resp.code)
490
491 return response_content
492
494 """Gets the Remote API version running on the cluster.
495
496 @rtype: int
497 @return: Ganeti Remote API version
498
499 """
500 return self._SendRequest(HTTP_GET, "/version", None, None)
501
518
520 """Gets the Operating Systems running in the Ganeti cluster.
521
522 @rtype: list of str
523 @return: operating systems
524
525 """
526 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
527 None, None)
528
530 """Gets info about the cluster.
531
532 @rtype: dict
533 @return: information about the cluster
534
535 """
536 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
537 None, None)
538
548
567
583
585 """Gets information about instances on the cluster.
586
587 @type bulk: bool
588 @param bulk: whether to return all information about all instances
589
590 @rtype: list of dict or list of str
591 @return: if bulk is True, info about the instances, else a list of instances
592
593 """
594 query = []
595 if bulk:
596 query.append(("bulk", 1))
597
598 instances = self._SendRequest(HTTP_GET,
599 "/%s/instances" % GANETI_RAPI_VERSION,
600 query, None)
601 if bulk:
602 return instances
603 else:
604 return [i["id"] for i in instances]
605
607 """Gets information about an instance.
608
609 @type instance: str
610 @param instance: instance whose info to return
611
612 @rtype: dict
613 @return: info about the instance
614
615 """
616 return self._SendRequest(HTTP_GET,
617 ("/%s/instances/%s" %
618 (GANETI_RAPI_VERSION, instance)), None, None)
619
621 """Gets information about an instance.
622
623 @type instance: string
624 @param instance: Instance name
625 @rtype: string
626 @return: Job ID
627
628 """
629 if static is not None:
630 query = [("static", static)]
631 else:
632 query = None
633
634 return self._SendRequest(HTTP_GET,
635 ("/%s/instances/%s/info" %
636 (GANETI_RAPI_VERSION, instance)), query, None)
637
638 - def CreateInstance(self, mode, name, disk_template, disks, nics,
639 **kwargs):
640 """Creates a new instance.
641
642 More details for parameters can be found in the RAPI documentation.
643
644 @type mode: string
645 @param mode: Instance creation mode
646 @type name: string
647 @param name: Hostname of the instance to create
648 @type disk_template: string
649 @param disk_template: Disk template for instance (e.g. plain, diskless,
650 file, or drbd)
651 @type disks: list of dicts
652 @param disks: List of disk definitions
653 @type nics: list of dicts
654 @param nics: List of NIC definitions
655 @type dry_run: bool
656 @keyword dry_run: whether to perform a dry run
657
658 @rtype: int
659 @return: job id
660
661 """
662 query = []
663
664 if kwargs.get("dry_run"):
665 query.append(("dry-run", 1))
666
667 if _INST_CREATE_REQV1 in self.GetFeatures():
668
669 body = {
670 _REQ_DATA_VERSION_FIELD: 1,
671 "mode": mode,
672 "name": name,
673 "disk_template": disk_template,
674 "disks": disks,
675 "nics": nics,
676 }
677
678 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
679 if conflicts:
680 raise GanetiApiError("Required fields can not be specified as"
681 " keywords: %s" % ", ".join(conflicts))
682
683 body.update((key, value) for key, value in kwargs.iteritems()
684 if key != "dry_run")
685 else:
686
687
688
689
690
691
692
693
694
695 for idx, disk in enumerate(disks):
696 unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS
697 if unsupported:
698 raise GanetiApiError("Server supports request version 0 only, but"
699 " disk %s specifies the unsupported parameters"
700 " %s, allowed are %s" %
701 (idx, unsupported,
702 list(_INST_CREATE_V0_DISK_PARAMS)))
703
704 assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and
705 "size" in _INST_CREATE_V0_DISK_PARAMS)
706 disk_sizes = [disk["size"] for disk in disks]
707
708
709 if not nics:
710 raise GanetiApiError("Server supports request version 0 only, but"
711 " no NIC specified")
712 elif len(nics) > 1:
713 raise GanetiApiError("Server supports request version 0 only, but"
714 " more than one NIC specified")
715
716 assert len(nics) == 1
717
718 unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS
719 if unsupported:
720 raise GanetiApiError("Server supports request version 0 only, but"
721 " NIC 0 specifies the unsupported parameters %s,"
722 " allowed are %s" %
723 (unsupported, list(_INST_NIC_PARAMS)))
724
725
726 unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS -
727 _INST_CREATE_V0_DPARAMS)
728 if unsupported:
729 allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS)
730 raise GanetiApiError("Server supports request version 0 only, but"
731 " the following unsupported parameters are"
732 " specified: %s, allowed are %s" %
733 (unsupported, list(allowed)))
734
735
736 body = {
737 _REQ_DATA_VERSION_FIELD: 0,
738 "name": name,
739 "disk_template": disk_template,
740 "disks": disk_sizes,
741 }
742
743
744 assert len(nics) == 1
745 assert not (set(body.keys()) & set(nics[0].keys()))
746 body.update(nics[0])
747
748
749 assert not (set(body.keys()) & set(kwargs.keys()))
750 body.update(dict((key, value) for key, value in kwargs.items()
751 if key in _INST_CREATE_V0_PARAMS))
752
753
754 for i in (value for key, value in kwargs.items()
755 if key in _INST_CREATE_V0_DPARAMS):
756 assert not (set(body.keys()) & set(i.keys()))
757 body.update(i)
758
759 assert not (set(kwargs.keys()) -
760 (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS))
761 assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS)
762
763 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
764 query, body)
765
767 """Deletes an instance.
768
769 @type instance: str
770 @param instance: the instance to delete
771
772 @rtype: int
773 @return: job id
774
775 """
776 query = []
777 if dry_run:
778 query.append(("dry-run", 1))
779
780 return self._SendRequest(HTTP_DELETE,
781 ("/%s/instances/%s" %
782 (GANETI_RAPI_VERSION, instance)), query, None)
783
797
819
838
839 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
840 dry_run=False):
841 """Reboots an instance.
842
843 @type instance: str
844 @param instance: instance to rebot
845 @type reboot_type: str
846 @param reboot_type: one of: hard, soft, full
847 @type ignore_secondaries: bool
848 @param ignore_secondaries: if True, ignores errors for the secondary node
849 while re-assembling disks (in hard-reboot mode only)
850 @type dry_run: bool
851 @param dry_run: whether to perform a dry run
852
853 """
854 query = []
855 if reboot_type:
856 query.append(("type", reboot_type))
857 if ignore_secondaries is not None:
858 query.append(("ignore_secondaries", ignore_secondaries))
859 if dry_run:
860 query.append(("dry-run", 1))
861
862 return self._SendRequest(HTTP_POST,
863 ("/%s/instances/%s/reboot" %
864 (GANETI_RAPI_VERSION, instance)), query, None)
865
867 """Shuts down an instance.
868
869 @type instance: str
870 @param instance: the instance to shut down
871 @type dry_run: bool
872 @param dry_run: whether to perform a dry run
873
874 """
875 query = []
876 if dry_run:
877 query.append(("dry-run", 1))
878
879 return self._SendRequest(HTTP_PUT,
880 ("/%s/instances/%s/shutdown" %
881 (GANETI_RAPI_VERSION, instance)), query, None)
882
884 """Starts up an instance.
885
886 @type instance: str
887 @param instance: the instance to start up
888 @type dry_run: bool
889 @param dry_run: whether to perform a dry run
890
891 """
892 query = []
893 if dry_run:
894 query.append(("dry-run", 1))
895
896 return self._SendRequest(HTTP_PUT,
897 ("/%s/instances/%s/startup" %
898 (GANETI_RAPI_VERSION, instance)), query, None)
899
901 """Reinstalls an instance.
902
903 @type instance: str
904 @param instance: the instance to reinstall
905 @type os: str
906 @param os: the os to reinstall
907 @type no_startup: bool
908 @param no_startup: whether to start the instance automatically
909
910 """
911 query = [("os", os)]
912 if no_startup:
913 query.append(("nostartup", 1))
914 return self._SendRequest(HTTP_POST,
915 ("/%s/instances/%s/reinstall" %
916 (GANETI_RAPI_VERSION, instance)), query, None)
917
920 """Replaces disks on an instance.
921
922 @type instance: str
923 @param instance: instance whose disks to replace
924 @type disks: list of ints
925 @param disks: Indexes of disks to replace
926 @type mode: str
927 @param mode: replacement mode to use (defaults to replace_auto)
928 @type remote_node: str or None
929 @param remote_node: new secondary node to use (for use with
930 replace_new_secondary mode)
931 @type iallocator: str or None
932 @param iallocator: instance allocator plugin to use (for use with
933 replace_auto mode)
934 @type dry_run: bool
935 @param dry_run: whether to perform a dry run
936
937 @rtype: int
938 @return: job id
939
940 """
941 query = [
942 ("mode", mode),
943 ]
944
945 if disks:
946 query.append(("disks", ",".join(str(idx) for idx in disks)))
947
948 if remote_node:
949 query.append(("remote_node", remote_node))
950
951 if iallocator:
952 query.append(("iallocator", iallocator))
953
954 if dry_run:
955 query.append(("dry-run", 1))
956
957 return self._SendRequest(HTTP_POST,
958 ("/%s/instances/%s/replace-disks" %
959 (GANETI_RAPI_VERSION, instance)), query, None)
960
962 """Gets all jobs for the cluster.
963
964 @rtype: list of int
965 @return: job ids for the cluster
966
967 """
968 return [int(j["id"])
969 for j in self._SendRequest(HTTP_GET,
970 "/%s/jobs" % GANETI_RAPI_VERSION,
971 None, None)]
972
974 """Gets the status of a job.
975
976 @type job_id: int
977 @param job_id: job id whose status to query
978
979 @rtype: dict
980 @return: job status
981
982 """
983 return self._SendRequest(HTTP_GET,
984 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
985 None, None)
986
988 """Waits for job changes.
989
990 @type job_id: int
991 @param job_id: Job ID for which to wait
992
993 """
994 body = {
995 "fields": fields,
996 "previous_job_info": prev_job_info,
997 "previous_log_serial": prev_log_serial,
998 }
999
1000 return self._SendRequest(HTTP_GET,
1001 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1002 None, body)
1003
1004 - def CancelJob(self, job_id, dry_run=False):
1005 """Cancels a job.
1006
1007 @type job_id: int
1008 @param job_id: id of the job to delete
1009 @type dry_run: bool
1010 @param dry_run: whether to perform a dry run
1011
1012 """
1013 query = []
1014 if dry_run:
1015 query.append(("dry-run", 1))
1016
1017 return self._SendRequest(HTTP_DELETE,
1018 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1019 query, None)
1020
1022 """Gets all nodes in the cluster.
1023
1024 @type bulk: bool
1025 @param bulk: whether to return all information about all instances
1026
1027 @rtype: list of dict or str
1028 @return: if bulk is true, info about nodes in the cluster,
1029 else list of nodes in the cluster
1030
1031 """
1032 query = []
1033 if bulk:
1034 query.append(("bulk", 1))
1035
1036 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1037 query, None)
1038 if bulk:
1039 return nodes
1040 else:
1041 return [n["id"] for n in nodes]
1042
1044 """Gets information about a node.
1045
1046 @type node: str
1047 @param node: node whose info to return
1048
1049 @rtype: dict
1050 @return: info about the node
1051
1052 """
1053 return self._SendRequest(HTTP_GET,
1054 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1055 None, None)
1056
1057 - def EvacuateNode(self, node, iallocator=None, remote_node=None,
1058 dry_run=False):
1059 """Evacuates instances from a Ganeti node.
1060
1061 @type node: str
1062 @param node: node to evacuate
1063 @type iallocator: str or None
1064 @param iallocator: instance allocator to use
1065 @type remote_node: str
1066 @param remote_node: node to evaucate to
1067 @type dry_run: bool
1068 @param dry_run: whether to perform a dry run
1069
1070 @rtype: int
1071 @return: job id
1072
1073 @raises GanetiApiError: if an iallocator and remote_node are both specified
1074
1075 """
1076 if iallocator and remote_node:
1077 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1078
1079 query = []
1080 if iallocator:
1081 query.append(("iallocator", iallocator))
1082 if remote_node:
1083 query.append(("remote_node", remote_node))
1084 if dry_run:
1085 query.append(("dry-run", 1))
1086
1087 return self._SendRequest(HTTP_POST,
1088 ("/%s/nodes/%s/evacuate" %
1089 (GANETI_RAPI_VERSION, node)), query, None)
1090
1091 - def MigrateNode(self, node, live=True, dry_run=False):
1092 """Migrates all primary instances from a node.
1093
1094 @type node: str
1095 @param node: node to migrate
1096 @type live: bool
1097 @param live: whether to use live migration
1098 @type dry_run: bool
1099 @param dry_run: whether to perform a dry run
1100
1101 @rtype: int
1102 @return: job id
1103
1104 """
1105 query = []
1106 if live:
1107 query.append(("live", 1))
1108 if dry_run:
1109 query.append(("dry-run", 1))
1110
1111 return self._SendRequest(HTTP_POST,
1112 ("/%s/nodes/%s/migrate" %
1113 (GANETI_RAPI_VERSION, node)), query, None)
1114
1116 """Gets the current role for a node.
1117
1118 @type node: str
1119 @param node: node whose role to return
1120
1121 @rtype: str
1122 @return: the current role for a node
1123
1124 """
1125 return self._SendRequest(HTTP_GET,
1126 ("/%s/nodes/%s/role" %
1127 (GANETI_RAPI_VERSION, node)), None, None)
1128
1130 """Sets the role for a node.
1131
1132 @type node: str
1133 @param node: the node whose role to set
1134 @type role: str
1135 @param role: the role to set for the node
1136 @type force: bool
1137 @param force: whether to force the role change
1138
1139 @rtype: int
1140 @return: job id
1141
1142 """
1143 query = [
1144 ("force", force),
1145 ]
1146
1147 return self._SendRequest(HTTP_PUT,
1148 ("/%s/nodes/%s/role" %
1149 (GANETI_RAPI_VERSION, node)), query, role)
1150
1152 """Gets the storage units for a node.
1153
1154 @type node: str
1155 @param node: the node whose storage units to return
1156 @type storage_type: str
1157 @param storage_type: storage type whose units to return
1158 @type output_fields: str
1159 @param output_fields: storage type fields to return
1160
1161 @rtype: int
1162 @return: job id where results can be retrieved
1163
1164 """
1165 query = [
1166 ("storage_type", storage_type),
1167 ("output_fields", output_fields),
1168 ]
1169
1170 return self._SendRequest(HTTP_GET,
1171 ("/%s/nodes/%s/storage" %
1172 (GANETI_RAPI_VERSION, node)), query, None)
1173
1175 """Modifies parameters of storage units on the node.
1176
1177 @type node: str
1178 @param node: node whose storage units to modify
1179 @type storage_type: str
1180 @param storage_type: storage type whose units to modify
1181 @type name: str
1182 @param name: name of the storage unit
1183 @type allocatable: bool or None
1184 @param allocatable: Whether to set the "allocatable" flag on the storage
1185 unit (None=no modification, True=set, False=unset)
1186
1187 @rtype: int
1188 @return: job id
1189
1190 """
1191 query = [
1192 ("storage_type", storage_type),
1193 ("name", name),
1194 ]
1195
1196 if allocatable is not None:
1197 query.append(("allocatable", allocatable))
1198
1199 return self._SendRequest(HTTP_PUT,
1200 ("/%s/nodes/%s/storage/modify" %
1201 (GANETI_RAPI_VERSION, node)), query, None)
1202
1204 """Repairs a storage unit on the node.
1205
1206 @type node: str
1207 @param node: node whose storage units to repair
1208 @type storage_type: str
1209 @param storage_type: storage type to repair
1210 @type name: str
1211 @param name: name of the storage unit to repair
1212
1213 @rtype: int
1214 @return: job id
1215
1216 """
1217 query = [
1218 ("storage_type", storage_type),
1219 ("name", name),
1220 ]
1221
1222 return self._SendRequest(HTTP_PUT,
1223 ("/%s/nodes/%s/storage/repair" %
1224 (GANETI_RAPI_VERSION, node)), query, None)
1225
1239
1261
1283