Add tags in network objects
[ganeti-github.git] / lib / rapi / client.py
1 #
2 #
3
4 # Copyright (C) 2010, 2011, 2012 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Ganeti RAPI client.
23
24 @attention: To use the RAPI client, the application B{must} call
25 C{pycurl.global_init} during initialization and
26 C{pycurl.global_cleanup} before exiting the process. This is very
27 important in multi-threaded programs. See curl_global_init(3) and
28 curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
29 can be used.
30
31 """
32
33 # No Ganeti-specific modules should be imported. The RAPI client is supposed to
34 # be standalone.
35
36 import logging
37 import simplejson
38 import socket
39 import urllib
40 import threading
41 import pycurl
42 import time
43
44 try:
45 from cStringIO import StringIO
46 except ImportError:
47 from StringIO import StringIO
48
49
50 GANETI_RAPI_PORT = 5080
51 GANETI_RAPI_VERSION = 2
52
53 HTTP_DELETE = "DELETE"
54 HTTP_GET = "GET"
55 HTTP_PUT = "PUT"
56 HTTP_POST = "POST"
57 HTTP_OK = 200
58 HTTP_NOT_FOUND = 404
59 HTTP_APP_JSON = "application/json"
60
61 REPLACE_DISK_PRI = "replace_on_primary"
62 REPLACE_DISK_SECONDARY = "replace_on_secondary"
63 REPLACE_DISK_CHG = "replace_new_secondary"
64 REPLACE_DISK_AUTO = "replace_auto"
65
66 NODE_EVAC_PRI = "primary-only"
67 NODE_EVAC_SEC = "secondary-only"
68 NODE_EVAC_ALL = "all"
69
70 NODE_ROLE_DRAINED = "drained"
71 NODE_ROLE_MASTER_CANDIATE = "master-candidate"
72 NODE_ROLE_MASTER = "master"
73 NODE_ROLE_OFFLINE = "offline"
74 NODE_ROLE_REGULAR = "regular"
75
76 JOB_STATUS_QUEUED = "queued"
77 JOB_STATUS_WAITING = "waiting"
78 JOB_STATUS_CANCELING = "canceling"
79 JOB_STATUS_RUNNING = "running"
80 JOB_STATUS_CANCELED = "canceled"
81 JOB_STATUS_SUCCESS = "success"
82 JOB_STATUS_ERROR = "error"
83 JOB_STATUS_PENDING = frozenset([
84 JOB_STATUS_QUEUED,
85 JOB_STATUS_WAITING,
86 JOB_STATUS_CANCELING,
87 ])
88 JOB_STATUS_FINALIZED = frozenset([
89 JOB_STATUS_CANCELED,
90 JOB_STATUS_SUCCESS,
91 JOB_STATUS_ERROR,
92 ])
93 JOB_STATUS_ALL = frozenset([
94 JOB_STATUS_RUNNING,
95 ]) | JOB_STATUS_PENDING | JOB_STATUS_FINALIZED
96
97 # Legacy name
98 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
99
100 # Internal constants
101 _REQ_DATA_VERSION_FIELD = "__version__"
102 _QPARAM_DRY_RUN = "dry-run"
103 _QPARAM_FORCE = "force"
104
105 # Feature strings
106 INST_CREATE_REQV1 = "instance-create-reqv1"
107 INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
108 NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
109 NODE_EVAC_RES1 = "node-evac-res1"
110
111 # Old feature constant names in case they're references by users of this module
112 _INST_CREATE_REQV1 = INST_CREATE_REQV1
113 _INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
114 _NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
115 _NODE_EVAC_RES1 = NODE_EVAC_RES1
116
117 # Older pycURL versions don't have all error constants
118 try:
119 _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
120 _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
121 except AttributeError:
122 _CURLE_SSL_CACERT = 60
123 _CURLE_SSL_CACERT_BADFILE = 77
124
125 _CURL_SSL_CERT_ERRORS = frozenset([
126 _CURLE_SSL_CACERT,
127 _CURLE_SSL_CACERT_BADFILE,
128 ])
129
130
131 class Error(Exception):
132 """Base error class for this module.
133
134 """
135 pass
136
137
138 class GanetiApiError(Error):
139 """Generic error raised from Ganeti API.
140
141 """
142 def __init__(self, msg, code=None):
143 Error.__init__(self, msg)
144 self.code = code
145
146
147 class CertificateError(GanetiApiError):
148 """Raised when a problem is found with the SSL certificate.
149
150 """
151 pass
152
153
154 def _AppendIf(container, condition, value):
155 """Appends to a list if a condition evaluates to truth.
156
157 """
158 if condition:
159 container.append(value)
160
161 return condition
162
163
164 def _AppendDryRunIf(container, condition):
165 """Appends a "dry-run" parameter if a condition evaluates to truth.
166
167 """
168 return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
169
170
171 def _AppendForceIf(container, condition):
172 """Appends a "force" parameter if a condition evaluates to truth.
173
174 """
175 return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
176
177
178 def _SetItemIf(container, condition, item, value):
179 """Sets an item if a condition evaluates to truth.
180
181 """
182 if condition:
183 container[item] = value
184
185 return condition
186
187
188 def UsesRapiClient(fn):
189 """Decorator for code using RAPI client to initialize pycURL.
190
191 """
192 def wrapper(*args, **kwargs):
193 # curl_global_init(3) and curl_global_cleanup(3) must be called with only
194 # one thread running. This check is just a safety measure -- it doesn't
195 # cover all cases.
196 assert threading.activeCount() == 1, \
197 "Found active threads when initializing pycURL"
198
199 pycurl.global_init(pycurl.GLOBAL_ALL)
200 try:
201 return fn(*args, **kwargs)
202 finally:
203 pycurl.global_cleanup()
204
205 return wrapper
206
207
208 def GenericCurlConfig(verbose=False, use_signal=False,
209 use_curl_cabundle=False, cafile=None, capath=None,
210 proxy=None, verify_hostname=False,
211 connect_timeout=None, timeout=None,
212 _pycurl_version_fn=pycurl.version_info):
213 """Curl configuration function generator.
214
215 @type verbose: bool
216 @param verbose: Whether to set cURL to verbose mode
217 @type use_signal: bool
218 @param use_signal: Whether to allow cURL to use signals
219 @type use_curl_cabundle: bool
220 @param use_curl_cabundle: Whether to use cURL's default CA bundle
221 @type cafile: string
222 @param cafile: In which file we can find the certificates
223 @type capath: string
224 @param capath: In which directory we can find the certificates
225 @type proxy: string
226 @param proxy: Proxy to use, None for default behaviour and empty string for
227 disabling proxies (see curl_easy_setopt(3))
228 @type verify_hostname: bool
229 @param verify_hostname: Whether to verify the remote peer certificate's
230 commonName
231 @type connect_timeout: number
232 @param connect_timeout: Timeout for establishing connection in seconds
233 @type timeout: number
234 @param timeout: Timeout for complete transfer in seconds (see
235 curl_easy_setopt(3)).
236
237 """
238 if use_curl_cabundle and (cafile or capath):
239 raise Error("Can not use default CA bundle when CA file or path is set")
240
241 def _ConfigCurl(curl, logger):
242 """Configures a cURL object
243
244 @type curl: pycurl.Curl
245 @param curl: cURL object
246
247 """
248 logger.debug("Using cURL version %s", pycurl.version)
249
250 # pycurl.version_info returns a tuple with information about the used
251 # version of libcurl. Item 5 is the SSL library linked to it.
252 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
253 # 0, '1.2.3.3', ...)
254 sslver = _pycurl_version_fn()[5]
255 if not sslver:
256 raise Error("No SSL support in cURL")
257
258 lcsslver = sslver.lower()
259 if lcsslver.startswith("openssl/"):
260 pass
261 elif lcsslver.startswith("nss/"):
262 # TODO: investigate compatibility beyond a simple test
263 pass
264 elif lcsslver.startswith("gnutls/"):
265 if capath:
266 raise Error("cURL linked against GnuTLS has no support for a"
267 " CA path (%s)" % (pycurl.version, ))
268 else:
269 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
270 sslver)
271
272 curl.setopt(pycurl.VERBOSE, verbose)
273 curl.setopt(pycurl.NOSIGNAL, not use_signal)
274
275 # Whether to verify remote peer's CN
276 if verify_hostname:
277 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
278 # certificate must indicate that the server is the server to which you
279 # meant to connect, or the connection fails. [...] When the value is 1,
280 # the certificate must contain a Common Name field, but it doesn't matter
281 # what name it says. [...]"
282 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
283 else:
284 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
285
286 if cafile or capath or use_curl_cabundle:
287 # Require certificates to be checked
288 curl.setopt(pycurl.SSL_VERIFYPEER, True)
289 if cafile:
290 curl.setopt(pycurl.CAINFO, str(cafile))
291 if capath:
292 curl.setopt(pycurl.CAPATH, str(capath))
293 # Not changing anything for using default CA bundle
294 else:
295 # Disable SSL certificate verification
296 curl.setopt(pycurl.SSL_VERIFYPEER, False)
297
298 if proxy is not None:
299 curl.setopt(pycurl.PROXY, str(proxy))
300
301 # Timeouts
302 if connect_timeout is not None:
303 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
304 if timeout is not None:
305 curl.setopt(pycurl.TIMEOUT, timeout)
306
307 return _ConfigCurl
308
309
310 class GanetiRapiClient(object): # pylint: disable=R0904
311 """Ganeti RAPI client.
312
313 """
314 USER_AGENT = "Ganeti RAPI Client"
315 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
316
317 def __init__(self, host, port=GANETI_RAPI_PORT,
318 username=None, password=None, logger=logging,
319 curl_config_fn=None, curl_factory=None):
320 """Initializes this class.
321
322 @type host: string
323 @param host: the ganeti cluster master to interact with
324 @type port: int
325 @param port: the port on which the RAPI is running (default is 5080)
326 @type username: string
327 @param username: the username to connect with
328 @type password: string
329 @param password: the password to connect with
330 @type curl_config_fn: callable
331 @param curl_config_fn: Function to configure C{pycurl.Curl} object
332 @param logger: Logging object
333
334 """
335 self._username = username
336 self._password = password
337 self._logger = logger
338 self._curl_config_fn = curl_config_fn
339 self._curl_factory = curl_factory
340
341 try:
342 socket.inet_pton(socket.AF_INET6, host)
343 address = "[%s]:%s" % (host, port)
344 except socket.error:
345 address = "%s:%s" % (host, port)
346
347 self._base_url = "https://%s" % address
348
349 if username is not None:
350 if password is None:
351 raise Error("Password not specified")
352 elif password:
353 raise Error("Specified password without username")
354
355 def _CreateCurl(self):
356 """Creates a cURL object.
357
358 """
359 # Create pycURL object if no factory is provided
360 if self._curl_factory:
361 curl = self._curl_factory()
362 else:
363 curl = pycurl.Curl()
364
365 # Default cURL settings
366 curl.setopt(pycurl.VERBOSE, False)
367 curl.setopt(pycurl.FOLLOWLOCATION, False)
368 curl.setopt(pycurl.MAXREDIRS, 5)
369 curl.setopt(pycurl.NOSIGNAL, True)
370 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
371 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
372 curl.setopt(pycurl.SSL_VERIFYPEER, False)
373 curl.setopt(pycurl.HTTPHEADER, [
374 "Accept: %s" % HTTP_APP_JSON,
375 "Content-type: %s" % HTTP_APP_JSON,
376 ])
377
378 assert ((self._username is None and self._password is None) ^
379 (self._username is not None and self._password is not None))
380
381 if self._username:
382 # Setup authentication
383 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
384 curl.setopt(pycurl.USERPWD,
385 str("%s:%s" % (self._username, self._password)))
386
387 # Call external configuration function
388 if self._curl_config_fn:
389 self._curl_config_fn(curl, self._logger)
390
391 return curl
392
393 @staticmethod
394 def _EncodeQuery(query):
395 """Encode query values for RAPI URL.
396
397 @type query: list of two-tuples
398 @param query: Query arguments
399 @rtype: list
400 @return: Query list with encoded values
401
402 """
403 result = []
404
405 for name, value in query:
406 if value is None:
407 result.append((name, ""))
408
409 elif isinstance(value, bool):
410 # Boolean values must be encoded as 0 or 1
411 result.append((name, int(value)))
412
413 elif isinstance(value, (list, tuple, dict)):
414 raise ValueError("Invalid query data type %r" % type(value).__name__)
415
416 else:
417 result.append((name, value))
418
419 return result
420
421 def _SendRequest(self, method, path, query, content):
422 """Sends an HTTP request.
423
424 This constructs a full URL, encodes and decodes HTTP bodies, and
425 handles invalid responses in a pythonic way.
426
427 @type method: string
428 @param method: HTTP method to use
429 @type path: string
430 @param path: HTTP URL path
431 @type query: list of two-tuples
432 @param query: query arguments to pass to urllib.urlencode
433 @type content: str or None
434 @param content: HTTP body content
435
436 @rtype: str
437 @return: JSON-Decoded response
438
439 @raises CertificateError: If an invalid SSL certificate is found
440 @raises GanetiApiError: If an invalid response is returned
441
442 """
443 assert path.startswith("/")
444
445 curl = self._CreateCurl()
446
447 if content is not None:
448 encoded_content = self._json_encoder.encode(content)
449 else:
450 encoded_content = ""
451
452 # Build URL
453 urlparts = [self._base_url, path]
454 if query:
455 urlparts.append("?")
456 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
457
458 url = "".join(urlparts)
459
460 self._logger.debug("Sending request %s %s (content=%r)",
461 method, url, encoded_content)
462
463 # Buffer for response
464 encoded_resp_body = StringIO()
465
466 # Configure cURL
467 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
468 curl.setopt(pycurl.URL, str(url))
469 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
470 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
471
472 try:
473 # Send request and wait for response
474 try:
475 curl.perform()
476 except pycurl.error, err:
477 if err.args[0] in _CURL_SSL_CERT_ERRORS:
478 raise CertificateError("SSL certificate error %s" % err,
479 code=err.args[0])
480
481 raise GanetiApiError(str(err), code=err.args[0])
482 finally:
483 # Reset settings to not keep references to large objects in memory
484 # between requests
485 curl.setopt(pycurl.POSTFIELDS, "")
486 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
487
488 # Get HTTP response code
489 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
490
491 # Was anything written to the response buffer?
492 if encoded_resp_body.tell():
493 response_content = simplejson.loads(encoded_resp_body.getvalue())
494 else:
495 response_content = None
496
497 if http_code != HTTP_OK:
498 if isinstance(response_content, dict):
499 msg = ("%s %s: %s" %
500 (response_content["code"],
501 response_content["message"],
502 response_content["explain"]))
503 else:
504 msg = str(response_content)
505
506 raise GanetiApiError(msg, code=http_code)
507
508 return response_content
509
510 def GetVersion(self):
511 """Gets the Remote API version running on the cluster.
512
513 @rtype: int
514 @return: Ganeti Remote API version
515
516 """
517 return self._SendRequest(HTTP_GET, "/version", None, None)
518
519 def GetFeatures(self):
520 """Gets the list of optional features supported by RAPI server.
521
522 @rtype: list
523 @return: List of optional features
524
525 """
526 try:
527 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
528 None, None)
529 except GanetiApiError, err:
530 # Older RAPI servers don't support this resource
531 if err.code == HTTP_NOT_FOUND:
532 return []
533
534 raise
535
536 def GetOperatingSystems(self):
537 """Gets the Operating Systems running in the Ganeti cluster.
538
539 @rtype: list of str
540 @return: operating systems
541
542 """
543 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
544 None, None)
545
546 def GetInfo(self):
547 """Gets info about the cluster.
548
549 @rtype: dict
550 @return: information about the cluster
551
552 """
553 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
554 None, None)
555
556 def RedistributeConfig(self):
557 """Tells the cluster to redistribute its configuration files.
558
559 @rtype: string
560 @return: job id
561
562 """
563 return self._SendRequest(HTTP_PUT,
564 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
565 None, None)
566
567 def ModifyCluster(self, **kwargs):
568 """Modifies cluster parameters.
569
570 More details for parameters can be found in the RAPI documentation.
571
572 @rtype: string
573 @return: job id
574
575 """
576 body = kwargs
577
578 return self._SendRequest(HTTP_PUT,
579 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
580
581 def GetClusterTags(self):
582 """Gets the cluster tags.
583
584 @rtype: list of str
585 @return: cluster tags
586
587 """
588 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
589 None, None)
590
591 def AddClusterTags(self, tags, dry_run=False):
592 """Adds tags to the cluster.
593
594 @type tags: list of str
595 @param tags: tags to add to the cluster
596 @type dry_run: bool
597 @param dry_run: whether to perform a dry run
598
599 @rtype: string
600 @return: job id
601
602 """
603 query = [("tag", t) for t in tags]
604 _AppendDryRunIf(query, dry_run)
605
606 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
607 query, None)
608
609 def DeleteClusterTags(self, tags, dry_run=False):
610 """Deletes tags from the cluster.
611
612 @type tags: list of str
613 @param tags: tags to delete
614 @type dry_run: bool
615 @param dry_run: whether to perform a dry run
616 @rtype: string
617 @return: job id
618
619 """
620 query = [("tag", t) for t in tags]
621 _AppendDryRunIf(query, dry_run)
622
623 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
624 query, None)
625
626 def GetInstances(self, bulk=False):
627 """Gets information about instances on the cluster.
628
629 @type bulk: bool
630 @param bulk: whether to return all information about all instances
631
632 @rtype: list of dict or list of str
633 @return: if bulk is True, info about the instances, else a list of instances
634
635 """
636 query = []
637 _AppendIf(query, bulk, ("bulk", 1))
638
639 instances = self._SendRequest(HTTP_GET,
640 "/%s/instances" % GANETI_RAPI_VERSION,
641 query, None)
642 if bulk:
643 return instances
644 else:
645 return [i["id"] for i in instances]
646
647 def GetInstance(self, instance):
648 """Gets information about an instance.
649
650 @type instance: str
651 @param instance: instance whose info to return
652
653 @rtype: dict
654 @return: info about the instance
655
656 """
657 return self._SendRequest(HTTP_GET,
658 ("/%s/instances/%s" %
659 (GANETI_RAPI_VERSION, instance)), None, None)
660
661 def GetInstanceInfo(self, instance, static=None):
662 """Gets information about an instance.
663
664 @type instance: string
665 @param instance: Instance name
666 @rtype: string
667 @return: Job ID
668
669 """
670 if static is not None:
671 query = [("static", static)]
672 else:
673 query = None
674
675 return self._SendRequest(HTTP_GET,
676 ("/%s/instances/%s/info" %
677 (GANETI_RAPI_VERSION, instance)), query, None)
678
679 @staticmethod
680 def _UpdateWithKwargs(base, **kwargs):
681 """Updates the base with params from kwargs.
682
683 @param base: The base dict, filled with required fields
684
685 @note: This is an inplace update of base
686
687 """
688 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
689 if conflicts:
690 raise GanetiApiError("Required fields can not be specified as"
691 " keywords: %s" % ", ".join(conflicts))
692
693 base.update((key, value) for key, value in kwargs.iteritems()
694 if key != "dry_run")
695
696 def InstanceAllocation(self, mode, name, disk_template, disks, nics,
697 **kwargs):
698 """Generates an instance allocation as used by multiallocate.
699
700 More details for parameters can be found in the RAPI documentation.
701 It is the same as used by CreateInstance.
702
703 @type mode: string
704 @param mode: Instance creation mode
705 @type name: string
706 @param name: Hostname of the instance to create
707 @type disk_template: string
708 @param disk_template: Disk template for instance (e.g. plain, diskless,
709 file, or drbd)
710 @type disks: list of dicts
711 @param disks: List of disk definitions
712 @type nics: list of dicts
713 @param nics: List of NIC definitions
714
715 @return: A dict with the generated entry
716
717 """
718 # All required fields for request data version 1
719 alloc = {
720 "mode": mode,
721 "name": name,
722 "disk_template": disk_template,
723 "disks": disks,
724 "nics": nics,
725 }
726
727 self._UpdateWithKwargs(alloc, **kwargs)
728
729 return alloc
730
731 def InstancesMultiAlloc(self, instances, **kwargs):
732 """Tries to allocate multiple instances.
733
734 More details for parameters can be found in the RAPI documentation.
735
736 @param instances: A list of L{InstanceAllocation} results
737
738 """
739 query = []
740 body = {
741 "instances": instances,
742 }
743 self._UpdateWithKwargs(body, **kwargs)
744
745 _AppendDryRunIf(query, kwargs.get("dry_run"))
746
747 return self._SendRequest(HTTP_POST,
748 "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
749 query, body)
750
751 def CreateInstance(self, mode, name, disk_template, disks, nics,
752 **kwargs):
753 """Creates a new instance.
754
755 More details for parameters can be found in the RAPI documentation.
756
757 @type mode: string
758 @param mode: Instance creation mode
759 @type name: string
760 @param name: Hostname of the instance to create
761 @type disk_template: string
762 @param disk_template: Disk template for instance (e.g. plain, diskless,
763 file, or drbd)
764 @type disks: list of dicts
765 @param disks: List of disk definitions
766 @type nics: list of dicts
767 @param nics: List of NIC definitions
768 @type dry_run: bool
769 @keyword dry_run: whether to perform a dry run
770
771 @rtype: string
772 @return: job id
773
774 """
775 query = []
776
777 _AppendDryRunIf(query, kwargs.get("dry_run"))
778
779 if _INST_CREATE_REQV1 in self.GetFeatures():
780 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
781 **kwargs)
782 body[_REQ_DATA_VERSION_FIELD] = 1
783 else:
784 raise GanetiApiError("Server does not support new-style (version 1)"
785 " instance creation requests")
786
787 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
788 query, body)
789
790 def DeleteInstance(self, instance, dry_run=False):
791 """Deletes an instance.
792
793 @type instance: str
794 @param instance: the instance to delete
795
796 @rtype: string
797 @return: job id
798
799 """
800 query = []
801 _AppendDryRunIf(query, dry_run)
802
803 return self._SendRequest(HTTP_DELETE,
804 ("/%s/instances/%s" %
805 (GANETI_RAPI_VERSION, instance)), query, None)
806
807 def ModifyInstance(self, instance, **kwargs):
808 """Modifies an instance.
809
810 More details for parameters can be found in the RAPI documentation.
811
812 @type instance: string
813 @param instance: Instance name
814 @rtype: string
815 @return: job id
816
817 """
818 body = kwargs
819
820 return self._SendRequest(HTTP_PUT,
821 ("/%s/instances/%s/modify" %
822 (GANETI_RAPI_VERSION, instance)), None, body)
823
824 def ActivateInstanceDisks(self, instance, ignore_size=None):
825 """Activates an instance's disks.
826
827 @type instance: string
828 @param instance: Instance name
829 @type ignore_size: bool
830 @param ignore_size: Whether to ignore recorded size
831 @rtype: string
832 @return: job id
833
834 """
835 query = []
836 _AppendIf(query, ignore_size, ("ignore_size", 1))
837
838 return self._SendRequest(HTTP_PUT,
839 ("/%s/instances/%s/activate-disks" %
840 (GANETI_RAPI_VERSION, instance)), query, None)
841
842 def DeactivateInstanceDisks(self, instance):
843 """Deactivates an instance's disks.
844
845 @type instance: string
846 @param instance: Instance name
847 @rtype: string
848 @return: job id
849
850 """
851 return self._SendRequest(HTTP_PUT,
852 ("/%s/instances/%s/deactivate-disks" %
853 (GANETI_RAPI_VERSION, instance)), None, None)
854
855 def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
856 """Recreate an instance's disks.
857
858 @type instance: string
859 @param instance: Instance name
860 @type disks: list of int
861 @param disks: List of disk indexes
862 @type nodes: list of string
863 @param nodes: New instance nodes, if relocation is desired
864 @rtype: string
865 @return: job id
866
867 """
868 body = {}
869 _SetItemIf(body, disks is not None, "disks", disks)
870 _SetItemIf(body, nodes is not None, "nodes", nodes)
871
872 return self._SendRequest(HTTP_POST,
873 ("/%s/instances/%s/recreate-disks" %
874 (GANETI_RAPI_VERSION, instance)), None, body)
875
876 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
877 """Grows a disk of an instance.
878
879 More details for parameters can be found in the RAPI documentation.
880
881 @type instance: string
882 @param instance: Instance name
883 @type disk: integer
884 @param disk: Disk index
885 @type amount: integer
886 @param amount: Grow disk by this amount (MiB)
887 @type wait_for_sync: bool
888 @param wait_for_sync: Wait for disk to synchronize
889 @rtype: string
890 @return: job id
891
892 """
893 body = {
894 "amount": amount,
895 }
896
897 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
898
899 return self._SendRequest(HTTP_POST,
900 ("/%s/instances/%s/disk/%s/grow" %
901 (GANETI_RAPI_VERSION, instance, disk)),
902 None, body)
903
904 def GetInstanceTags(self, instance):
905 """Gets tags for an instance.
906
907 @type instance: str
908 @param instance: instance whose tags to return
909
910 @rtype: list of str
911 @return: tags for the instance
912
913 """
914 return self._SendRequest(HTTP_GET,
915 ("/%s/instances/%s/tags" %
916 (GANETI_RAPI_VERSION, instance)), None, None)
917
918 def AddInstanceTags(self, instance, tags, dry_run=False):
919 """Adds tags to an instance.
920
921 @type instance: str
922 @param instance: instance to add tags to
923 @type tags: list of str
924 @param tags: tags to add to the instance
925 @type dry_run: bool
926 @param dry_run: whether to perform a dry run
927
928 @rtype: string
929 @return: job id
930
931 """
932 query = [("tag", t) for t in tags]
933 _AppendDryRunIf(query, dry_run)
934
935 return self._SendRequest(HTTP_PUT,
936 ("/%s/instances/%s/tags" %
937 (GANETI_RAPI_VERSION, instance)), query, None)
938
939 def DeleteInstanceTags(self, instance, tags, dry_run=False):
940 """Deletes tags from an instance.
941
942 @type instance: str
943 @param instance: instance to delete tags from
944 @type tags: list of str
945 @param tags: tags to delete
946 @type dry_run: bool
947 @param dry_run: whether to perform a dry run
948 @rtype: string
949 @return: job id
950
951 """
952 query = [("tag", t) for t in tags]
953 _AppendDryRunIf(query, dry_run)
954
955 return self._SendRequest(HTTP_DELETE,
956 ("/%s/instances/%s/tags" %
957 (GANETI_RAPI_VERSION, instance)), query, None)
958
959 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
960 dry_run=False):
961 """Reboots an instance.
962
963 @type instance: str
964 @param instance: instance to rebot
965 @type reboot_type: str
966 @param reboot_type: one of: hard, soft, full
967 @type ignore_secondaries: bool
968 @param ignore_secondaries: if True, ignores errors for the secondary node
969 while re-assembling disks (in hard-reboot mode only)
970 @type dry_run: bool
971 @param dry_run: whether to perform a dry run
972 @rtype: string
973 @return: job id
974
975 """
976 query = []
977 _AppendDryRunIf(query, dry_run)
978 _AppendIf(query, reboot_type, ("type", reboot_type))
979 _AppendIf(query, ignore_secondaries is not None,
980 ("ignore_secondaries", ignore_secondaries))
981
982 return self._SendRequest(HTTP_POST,
983 ("/%s/instances/%s/reboot" %
984 (GANETI_RAPI_VERSION, instance)), query, None)
985
986 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
987 **kwargs):
988 """Shuts down an instance.
989
990 @type instance: str
991 @param instance: the instance to shut down
992 @type dry_run: bool
993 @param dry_run: whether to perform a dry run
994 @type no_remember: bool
995 @param no_remember: if true, will not record the state change
996 @rtype: string
997 @return: job id
998
999 """
1000 query = []
1001 body = kwargs
1002
1003 _AppendDryRunIf(query, dry_run)
1004 _AppendIf(query, no_remember, ("no-remember", 1))
1005
1006 return self._SendRequest(HTTP_PUT,
1007 ("/%s/instances/%s/shutdown" %
1008 (GANETI_RAPI_VERSION, instance)), query, body)
1009
1010 def StartupInstance(self, instance, dry_run=False, no_remember=False):
1011 """Starts up an instance.
1012
1013 @type instance: str
1014 @param instance: the instance to start up
1015 @type dry_run: bool
1016 @param dry_run: whether to perform a dry run
1017 @type no_remember: bool
1018 @param no_remember: if true, will not record the state change
1019 @rtype: string
1020 @return: job id
1021
1022 """
1023 query = []
1024 _AppendDryRunIf(query, dry_run)
1025 _AppendIf(query, no_remember, ("no-remember", 1))
1026
1027 return self._SendRequest(HTTP_PUT,
1028 ("/%s/instances/%s/startup" %
1029 (GANETI_RAPI_VERSION, instance)), query, None)
1030
1031 def ReinstallInstance(self, instance, os=None, no_startup=False,
1032 osparams=None):
1033 """Reinstalls an instance.
1034
1035 @type instance: str
1036 @param instance: The instance to reinstall
1037 @type os: str or None
1038 @param os: The operating system to reinstall. If None, the instance's
1039 current operating system will be installed again
1040 @type no_startup: bool
1041 @param no_startup: Whether to start the instance automatically
1042 @rtype: string
1043 @return: job id
1044
1045 """
1046 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1047 body = {
1048 "start": not no_startup,
1049 }
1050 _SetItemIf(body, os is not None, "os", os)
1051 _SetItemIf(body, osparams is not None, "osparams", osparams)
1052 return self._SendRequest(HTTP_POST,
1053 ("/%s/instances/%s/reinstall" %
1054 (GANETI_RAPI_VERSION, instance)), None, body)
1055
1056 # Use old request format
1057 if osparams:
1058 raise GanetiApiError("Server does not support specifying OS parameters"
1059 " for instance reinstallation")
1060
1061 query = []
1062 _AppendIf(query, os, ("os", os))
1063 _AppendIf(query, no_startup, ("nostartup", 1))
1064
1065 return self._SendRequest(HTTP_POST,
1066 ("/%s/instances/%s/reinstall" %
1067 (GANETI_RAPI_VERSION, instance)), query, None)
1068
1069 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1070 remote_node=None, iallocator=None):
1071 """Replaces disks on an instance.
1072
1073 @type instance: str
1074 @param instance: instance whose disks to replace
1075 @type disks: list of ints
1076 @param disks: Indexes of disks to replace
1077 @type mode: str
1078 @param mode: replacement mode to use (defaults to replace_auto)
1079 @type remote_node: str or None
1080 @param remote_node: new secondary node to use (for use with
1081 replace_new_secondary mode)
1082 @type iallocator: str or None
1083 @param iallocator: instance allocator plugin to use (for use with
1084 replace_auto mode)
1085
1086 @rtype: string
1087 @return: job id
1088
1089 """
1090 query = [
1091 ("mode", mode),
1092 ]
1093
1094 # TODO: Convert to body parameters
1095
1096 if disks is not None:
1097 _AppendIf(query, True,
1098 ("disks", ",".join(str(idx) for idx in disks)))
1099
1100 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1101 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1102
1103 return self._SendRequest(HTTP_POST,
1104 ("/%s/instances/%s/replace-disks" %
1105 (GANETI_RAPI_VERSION, instance)), query, None)
1106
1107 def PrepareExport(self, instance, mode):
1108 """Prepares an instance for an export.
1109
1110 @type instance: string
1111 @param instance: Instance name
1112 @type mode: string
1113 @param mode: Export mode
1114 @rtype: string
1115 @return: Job ID
1116
1117 """
1118 query = [("mode", mode)]
1119 return self._SendRequest(HTTP_PUT,
1120 ("/%s/instances/%s/prepare-export" %
1121 (GANETI_RAPI_VERSION, instance)), query, None)
1122
1123 def ExportInstance(self, instance, mode, destination, shutdown=None,
1124 remove_instance=None,
1125 x509_key_name=None, destination_x509_ca=None):
1126 """Exports an instance.
1127
1128 @type instance: string
1129 @param instance: Instance name
1130 @type mode: string
1131 @param mode: Export mode
1132 @rtype: string
1133 @return: Job ID
1134
1135 """
1136 body = {
1137 "destination": destination,
1138 "mode": mode,
1139 }
1140
1141 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1142 _SetItemIf(body, remove_instance is not None,
1143 "remove_instance", remove_instance)
1144 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1145 _SetItemIf(body, destination_x509_ca is not None,
1146 "destination_x509_ca", destination_x509_ca)
1147
1148 return self._SendRequest(HTTP_PUT,
1149 ("/%s/instances/%s/export" %
1150 (GANETI_RAPI_VERSION, instance)), None, body)
1151
1152 def MigrateInstance(self, instance, mode=None, cleanup=None):
1153 """Migrates an instance.
1154
1155 @type instance: string
1156 @param instance: Instance name
1157 @type mode: string
1158 @param mode: Migration mode
1159 @type cleanup: bool
1160 @param cleanup: Whether to clean up a previously failed migration
1161 @rtype: string
1162 @return: job id
1163
1164 """
1165 body = {}
1166 _SetItemIf(body, mode is not None, "mode", mode)
1167 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1168
1169 return self._SendRequest(HTTP_PUT,
1170 ("/%s/instances/%s/migrate" %
1171 (GANETI_RAPI_VERSION, instance)), None, body)
1172
1173 def FailoverInstance(self, instance, iallocator=None,
1174 ignore_consistency=None, target_node=None):
1175 """Does a failover of an instance.
1176
1177 @type instance: string
1178 @param instance: Instance name
1179 @type iallocator: string
1180 @param iallocator: Iallocator for deciding the target node for
1181 shared-storage instances
1182 @type ignore_consistency: bool
1183 @param ignore_consistency: Whether to ignore disk consistency
1184 @type target_node: string
1185 @param target_node: Target node for shared-storage instances
1186 @rtype: string
1187 @return: job id
1188
1189 """
1190 body = {}
1191 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1192 _SetItemIf(body, ignore_consistency is not None,
1193 "ignore_consistency", ignore_consistency)
1194 _SetItemIf(body, target_node is not None, "target_node", target_node)
1195
1196 return self._SendRequest(HTTP_PUT,
1197 ("/%s/instances/%s/failover" %
1198 (GANETI_RAPI_VERSION, instance)), None, body)
1199
1200 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1201 """Changes the name of an instance.
1202
1203 @type instance: string
1204 @param instance: Instance name
1205 @type new_name: string
1206 @param new_name: New instance name
1207 @type ip_check: bool
1208 @param ip_check: Whether to ensure instance's IP address is inactive
1209 @type name_check: bool
1210 @param name_check: Whether to ensure instance's name is resolvable
1211 @rtype: string
1212 @return: job id
1213
1214 """
1215 body = {
1216 "new_name": new_name,
1217 }
1218
1219 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1220 _SetItemIf(body, name_check is not None, "name_check", name_check)
1221
1222 return self._SendRequest(HTTP_PUT,
1223 ("/%s/instances/%s/rename" %
1224 (GANETI_RAPI_VERSION, instance)), None, body)
1225
1226 def GetInstanceConsole(self, instance):
1227 """Request information for connecting to instance's console.
1228
1229 @type instance: string
1230 @param instance: Instance name
1231 @rtype: dict
1232 @return: dictionary containing information about instance's console
1233
1234 """
1235 return self._SendRequest(HTTP_GET,
1236 ("/%s/instances/%s/console" %
1237 (GANETI_RAPI_VERSION, instance)), None, None)
1238
1239 def GetJobs(self):
1240 """Gets all jobs for the cluster.
1241
1242 @rtype: list of int
1243 @return: job ids for the cluster
1244
1245 """
1246 return [int(j["id"])
1247 for j in self._SendRequest(HTTP_GET,
1248 "/%s/jobs" % GANETI_RAPI_VERSION,
1249 None, None)]
1250
1251 def GetJobStatus(self, job_id):
1252 """Gets the status of a job.
1253
1254 @type job_id: string
1255 @param job_id: job id whose status to query
1256
1257 @rtype: dict
1258 @return: job status
1259
1260 """
1261 return self._SendRequest(HTTP_GET,
1262 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1263 None, None)
1264
1265 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1266 """Polls cluster for job status until completion.
1267
1268 Completion is defined as any of the following states listed in
1269 L{JOB_STATUS_FINALIZED}.
1270
1271 @type job_id: string
1272 @param job_id: job id to watch
1273 @type period: int
1274 @param period: how often to poll for status (optional, default 5s)
1275 @type retries: int
1276 @param retries: how many time to poll before giving up
1277 (optional, default -1 means unlimited)
1278
1279 @rtype: bool
1280 @return: C{True} if job succeeded or C{False} if failed/status timeout
1281 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1282 possible; L{WaitForJobChange} returns immediately after a job changed and
1283 does not use polling
1284
1285 """
1286 while retries != 0:
1287 job_result = self.GetJobStatus(job_id)
1288
1289 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1290 return True
1291 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1292 return False
1293
1294 if period:
1295 time.sleep(period)
1296
1297 if retries > 0:
1298 retries -= 1
1299
1300 return False
1301
1302 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1303 """Waits for job changes.
1304
1305 @type job_id: string
1306 @param job_id: Job ID for which to wait
1307 @return: C{None} if no changes have been detected and a dict with two keys,
1308 C{job_info} and C{log_entries} otherwise.
1309 @rtype: dict
1310
1311 """
1312 body = {
1313 "fields": fields,
1314 "previous_job_info": prev_job_info,
1315 "previous_log_serial": prev_log_serial,
1316 }
1317
1318 return self._SendRequest(HTTP_GET,
1319 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1320 None, body)
1321
1322 def CancelJob(self, job_id, dry_run=False):
1323 """Cancels a job.
1324
1325 @type job_id: string
1326 @param job_id: id of the job to delete
1327 @type dry_run: bool
1328 @param dry_run: whether to perform a dry run
1329 @rtype: tuple
1330 @return: tuple containing the result, and a message (bool, string)
1331
1332 """
1333 query = []
1334 _AppendDryRunIf(query, dry_run)
1335
1336 return self._SendRequest(HTTP_DELETE,
1337 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1338 query, None)
1339
1340 def GetNodes(self, bulk=False):
1341 """Gets all nodes in the cluster.
1342
1343 @type bulk: bool
1344 @param bulk: whether to return all information about all instances
1345
1346 @rtype: list of dict or str
1347 @return: if bulk is true, info about nodes in the cluster,
1348 else list of nodes in the cluster
1349
1350 """
1351 query = []
1352 _AppendIf(query, bulk, ("bulk", 1))
1353
1354 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1355 query, None)
1356 if bulk:
1357 return nodes
1358 else:
1359 return [n["id"] for n in nodes]
1360
1361 def GetNode(self, node):
1362 """Gets information about a node.
1363
1364 @type node: str
1365 @param node: node whose info to return
1366
1367 @rtype: dict
1368 @return: info about the node
1369
1370 """
1371 return self._SendRequest(HTTP_GET,
1372 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1373 None, None)
1374
1375 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1376 dry_run=False, early_release=None,
1377 mode=None, accept_old=False):
1378 """Evacuates instances from a Ganeti node.
1379
1380 @type node: str
1381 @param node: node to evacuate
1382 @type iallocator: str or None
1383 @param iallocator: instance allocator to use
1384 @type remote_node: str
1385 @param remote_node: node to evaucate to
1386 @type dry_run: bool
1387 @param dry_run: whether to perform a dry run
1388 @type early_release: bool
1389 @param early_release: whether to enable parallelization
1390 @type mode: string
1391 @param mode: Node evacuation mode
1392 @type accept_old: bool
1393 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1394 results
1395
1396 @rtype: string, or a list for pre-2.5 results
1397 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1398 list of (job ID, instance name, new secondary node); if dry_run was
1399 specified, then the actual move jobs were not submitted and the job IDs
1400 will be C{None}
1401
1402 @raises GanetiApiError: if an iallocator and remote_node are both
1403 specified
1404
1405 """
1406 if iallocator and remote_node:
1407 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1408
1409 query = []
1410 _AppendDryRunIf(query, dry_run)
1411
1412 if _NODE_EVAC_RES1 in self.GetFeatures():
1413 # Server supports body parameters
1414 body = {}
1415
1416 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1417 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1418 _SetItemIf(body, early_release is not None,
1419 "early_release", early_release)
1420 _SetItemIf(body, mode is not None, "mode", mode)
1421 else:
1422 # Pre-2.5 request format
1423 body = None
1424
1425 if not accept_old:
1426 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1427 " not accept old-style results (parameter"
1428 " accept_old)")
1429
1430 # Pre-2.5 servers can only evacuate secondaries
1431 if mode is not None and mode != NODE_EVAC_SEC:
1432 raise GanetiApiError("Server can only evacuate secondary instances")
1433
1434 _AppendIf(query, iallocator, ("iallocator", iallocator))
1435 _AppendIf(query, remote_node, ("remote_node", remote_node))
1436 _AppendIf(query, early_release, ("early_release", 1))
1437
1438 return self._SendRequest(HTTP_POST,
1439 ("/%s/nodes/%s/evacuate" %
1440 (GANETI_RAPI_VERSION, node)), query, body)
1441
1442 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1443 target_node=None):
1444 """Migrates all primary instances from a node.
1445
1446 @type node: str
1447 @param node: node to migrate
1448 @type mode: string
1449 @param mode: if passed, it will overwrite the live migration type,
1450 otherwise the hypervisor default will be used
1451 @type dry_run: bool
1452 @param dry_run: whether to perform a dry run
1453 @type iallocator: string
1454 @param iallocator: instance allocator to use
1455 @type target_node: string
1456 @param target_node: Target node for shared-storage instances
1457
1458 @rtype: string
1459 @return: job id
1460
1461 """
1462 query = []
1463 _AppendDryRunIf(query, dry_run)
1464
1465 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1466 body = {}
1467
1468 _SetItemIf(body, mode is not None, "mode", mode)
1469 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1470 _SetItemIf(body, target_node is not None, "target_node", target_node)
1471
1472 assert len(query) <= 1
1473
1474 return self._SendRequest(HTTP_POST,
1475 ("/%s/nodes/%s/migrate" %
1476 (GANETI_RAPI_VERSION, node)), query, body)
1477 else:
1478 # Use old request format
1479 if target_node is not None:
1480 raise GanetiApiError("Server does not support specifying target node"
1481 " for node migration")
1482
1483 _AppendIf(query, mode is not None, ("mode", mode))
1484
1485 return self._SendRequest(HTTP_POST,
1486 ("/%s/nodes/%s/migrate" %
1487 (GANETI_RAPI_VERSION, node)), query, None)
1488
1489 def GetNodeRole(self, node):
1490 """Gets the current role for a node.
1491
1492 @type node: str
1493 @param node: node whose role to return
1494
1495 @rtype: str
1496 @return: the current role for a node
1497
1498 """
1499 return self._SendRequest(HTTP_GET,
1500 ("/%s/nodes/%s/role" %
1501 (GANETI_RAPI_VERSION, node)), None, None)
1502
1503 def SetNodeRole(self, node, role, force=False, auto_promote=None):
1504 """Sets the role for a node.
1505
1506 @type node: str
1507 @param node: the node whose role to set
1508 @type role: str
1509 @param role: the role to set for the node
1510 @type force: bool
1511 @param force: whether to force the role change
1512 @type auto_promote: bool
1513 @param auto_promote: Whether node(s) should be promoted to master candidate
1514 if necessary
1515
1516 @rtype: string
1517 @return: job id
1518
1519 """
1520 query = []
1521 _AppendForceIf(query, force)
1522 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1523
1524 return self._SendRequest(HTTP_PUT,
1525 ("/%s/nodes/%s/role" %
1526 (GANETI_RAPI_VERSION, node)), query, role)
1527
1528 def PowercycleNode(self, node, force=False):
1529 """Powercycles a node.
1530
1531 @type node: string
1532 @param node: Node name
1533 @type force: bool
1534 @param force: Whether to force the operation
1535 @rtype: string
1536 @return: job id
1537
1538 """
1539 query = []
1540 _AppendForceIf(query, force)
1541
1542 return self._SendRequest(HTTP_POST,
1543 ("/%s/nodes/%s/powercycle" %
1544 (GANETI_RAPI_VERSION, node)), query, None)
1545
1546 def ModifyNode(self, node, **kwargs):
1547 """Modifies a node.
1548
1549 More details for parameters can be found in the RAPI documentation.
1550
1551 @type node: string
1552 @param node: Node name
1553 @rtype: string
1554 @return: job id
1555
1556 """
1557 return self._SendRequest(HTTP_POST,
1558 ("/%s/nodes/%s/modify" %
1559 (GANETI_RAPI_VERSION, node)), None, kwargs)
1560
1561 def GetNodeStorageUnits(self, node, storage_type, output_fields):
1562 """Gets the storage units for a node.
1563
1564 @type node: str
1565 @param node: the node whose storage units to return
1566 @type storage_type: str
1567 @param storage_type: storage type whose units to return
1568 @type output_fields: str
1569 @param output_fields: storage type fields to return
1570
1571 @rtype: string
1572 @return: job id where results can be retrieved
1573
1574 """
1575 query = [
1576 ("storage_type", storage_type),
1577 ("output_fields", output_fields),
1578 ]
1579
1580 return self._SendRequest(HTTP_GET,
1581 ("/%s/nodes/%s/storage" %
1582 (GANETI_RAPI_VERSION, node)), query, None)
1583
1584 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1585 """Modifies parameters of storage units on the node.
1586
1587 @type node: str
1588 @param node: node whose storage units to modify
1589 @type storage_type: str
1590 @param storage_type: storage type whose units to modify
1591 @type name: str
1592 @param name: name of the storage unit
1593 @type allocatable: bool or None
1594 @param allocatable: Whether to set the "allocatable" flag on the storage
1595 unit (None=no modification, True=set, False=unset)
1596
1597 @rtype: string
1598 @return: job id
1599
1600 """
1601 query = [
1602 ("storage_type", storage_type),
1603 ("name", name),
1604 ]
1605
1606 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1607
1608 return self._SendRequest(HTTP_PUT,
1609 ("/%s/nodes/%s/storage/modify" %
1610 (GANETI_RAPI_VERSION, node)), query, None)
1611
1612 def RepairNodeStorageUnits(self, node, storage_type, name):
1613 """Repairs a storage unit on the node.
1614
1615 @type node: str
1616 @param node: node whose storage units to repair
1617 @type storage_type: str
1618 @param storage_type: storage type to repair
1619 @type name: str
1620 @param name: name of the storage unit to repair
1621
1622 @rtype: string
1623 @return: job id
1624
1625 """
1626 query = [
1627 ("storage_type", storage_type),
1628 ("name", name),
1629 ]
1630
1631 return self._SendRequest(HTTP_PUT,
1632 ("/%s/nodes/%s/storage/repair" %
1633 (GANETI_RAPI_VERSION, node)), query, None)
1634
1635 def GetNodeTags(self, node):
1636 """Gets the tags for a node.
1637
1638 @type node: str
1639 @param node: node whose tags to return
1640
1641 @rtype: list of str
1642 @return: tags for the node
1643
1644 """
1645 return self._SendRequest(HTTP_GET,
1646 ("/%s/nodes/%s/tags" %
1647 (GANETI_RAPI_VERSION, node)), None, None)
1648
1649 def AddNodeTags(self, node, tags, dry_run=False):
1650 """Adds tags to a node.
1651
1652 @type node: str
1653 @param node: node to add tags to
1654 @type tags: list of str
1655 @param tags: tags to add to the node
1656 @type dry_run: bool
1657 @param dry_run: whether to perform a dry run
1658
1659 @rtype: string
1660 @return: job id
1661
1662 """
1663 query = [("tag", t) for t in tags]
1664 _AppendDryRunIf(query, dry_run)
1665
1666 return self._SendRequest(HTTP_PUT,
1667 ("/%s/nodes/%s/tags" %
1668 (GANETI_RAPI_VERSION, node)), query, tags)
1669
1670 def DeleteNodeTags(self, node, tags, dry_run=False):
1671 """Delete tags from a node.
1672
1673 @type node: str
1674 @param node: node to remove tags from
1675 @type tags: list of str
1676 @param tags: tags to remove from the node
1677 @type dry_run: bool
1678 @param dry_run: whether to perform a dry run
1679
1680 @rtype: string
1681 @return: job id
1682
1683 """
1684 query = [("tag", t) for t in tags]
1685 _AppendDryRunIf(query, dry_run)
1686
1687 return self._SendRequest(HTTP_DELETE,
1688 ("/%s/nodes/%s/tags" %
1689 (GANETI_RAPI_VERSION, node)), query, None)
1690
1691 def GetNetworks(self, bulk=False):
1692 """Gets all networks in the cluster.
1693
1694 @type bulk: bool
1695 @param bulk: whether to return all information about the networks
1696
1697 @rtype: list of dict or str
1698 @return: if bulk is true, a list of dictionaries with info about all
1699 networks in the cluster, else a list of names of those networks
1700
1701 """
1702 query = []
1703 _AppendIf(query, bulk, ("bulk", 1))
1704
1705 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1706 query, None)
1707 if bulk:
1708 return networks
1709 else:
1710 return [n["name"] for n in networks]
1711
1712 def GetNetwork(self, network):
1713 """Gets information about a network.
1714
1715 @type group: str
1716 @param group: name of the network whose info to return
1717
1718 @rtype: dict
1719 @return: info about the network
1720
1721 """
1722 return self._SendRequest(HTTP_GET,
1723 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1724 None, None)
1725
1726 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1727 gateway6=None, mac_prefix=None, network_type=None,
1728 add_reserved_ips=None, tags=None, dry_run=False):
1729 """Creates a new network.
1730
1731 @type name: str
1732 @param name: the name of network to create
1733 @type dry_run: bool
1734 @param dry_run: whether to peform a dry run
1735
1736 @rtype: string
1737 @return: job id
1738
1739 """
1740 query = []
1741 _AppendDryRunIf(query, dry_run)
1742
1743 if add_reserved_ips:
1744 add_reserved_ips = add_reserved_ips.split(',')
1745
1746 if tags:
1747 tags = tags.split(',')
1748
1749 body = {
1750 "network_name": network_name,
1751 "gateway": gateway,
1752 "network": network,
1753 "gateway6": gateway6,
1754 "network6": network6,
1755 "mac_prefix": mac_prefix,
1756 "network_type": network_type,
1757 "add_reserved_ips": add_reserved_ips,
1758 "tags": tags,
1759 }
1760
1761 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1762 query, body)
1763
1764 def ConnectNetwork(self, network_name, group_name, mode, link):
1765 """Connects a Network to a NodeGroup with the given netparams
1766
1767 """
1768 body = {
1769 "group_name": group_name,
1770 "network_mode": mode,
1771 "network_link": link
1772 }
1773
1774 return self._SendRequest(HTTP_PUT,
1775 ("/%s/networks/%s/connect" %
1776 (GANETI_RAPI_VERSION, network_name)), None, body)
1777
1778 def DisconnectNetwork(self, network_name, group_name):
1779 """Connects a Network to a NodeGroup with the given netparams
1780
1781 """
1782 body = {
1783 "group_name": group_name
1784 }
1785 return self._SendRequest(HTTP_PUT,
1786 ("/%s/networks/%s/disconnect" %
1787 (GANETI_RAPI_VERSION, network_name)), None, body)
1788
1789
1790 def DeleteNetwork(self, network, dry_run=False):
1791 """Deletes a network.
1792
1793 @type group: str
1794 @param group: the network to delete
1795 @type dry_run: bool
1796 @param dry_run: whether to peform a dry run
1797
1798 @rtype: string
1799 @return: job id
1800
1801 """
1802 query = []
1803 _AppendDryRunIf(query, dry_run)
1804
1805 return self._SendRequest(HTTP_DELETE,
1806 ("/%s/networks/%s" %
1807 (GANETI_RAPI_VERSION, network)), query, None)
1808
1809 def GetGroups(self, bulk=False):
1810 """Gets all node groups in the cluster.
1811
1812 @type bulk: bool
1813 @param bulk: whether to return all information about the groups
1814
1815 @rtype: list of dict or str
1816 @return: if bulk is true, a list of dictionaries with info about all node
1817 groups in the cluster, else a list of names of those node groups
1818
1819 """
1820 query = []
1821 _AppendIf(query, bulk, ("bulk", 1))
1822
1823 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1824 query, None)
1825 if bulk:
1826 return groups
1827 else:
1828 return [g["name"] for g in groups]
1829
1830 def GetGroup(self, group):
1831 """Gets information about a node group.
1832
1833 @type group: str
1834 @param group: name of the node group whose info to return
1835
1836 @rtype: dict
1837 @return: info about the node group
1838
1839 """
1840 return self._SendRequest(HTTP_GET,
1841 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1842 None, None)
1843
1844 def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1845 """Creates a new node group.
1846
1847 @type name: str
1848 @param name: the name of node group to create
1849 @type alloc_policy: str
1850 @param alloc_policy: the desired allocation policy for the group, if any
1851 @type dry_run: bool
1852 @param dry_run: whether to peform a dry run
1853
1854 @rtype: string
1855 @return: job id
1856
1857 """
1858 query = []
1859 _AppendDryRunIf(query, dry_run)
1860
1861 body = {
1862 "name": name,
1863 "alloc_policy": alloc_policy
1864 }
1865
1866 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1867 query, body)
1868
1869 def ModifyGroup(self, group, **kwargs):
1870 """Modifies a node group.
1871
1872 More details for parameters can be found in the RAPI documentation.
1873
1874 @type group: string
1875 @param group: Node group name
1876 @rtype: string
1877 @return: job id
1878
1879 """
1880 return self._SendRequest(HTTP_PUT,
1881 ("/%s/groups/%s/modify" %
1882 (GANETI_RAPI_VERSION, group)), None, kwargs)
1883
1884 def DeleteGroup(self, group, dry_run=False):
1885 """Deletes a node group.
1886
1887 @type group: str
1888 @param group: the node group to delete
1889 @type dry_run: bool
1890 @param dry_run: whether to peform a dry run
1891
1892 @rtype: string
1893 @return: job id
1894
1895 """
1896 query = []
1897 _AppendDryRunIf(query, dry_run)
1898
1899 return self._SendRequest(HTTP_DELETE,
1900 ("/%s/groups/%s" %
1901 (GANETI_RAPI_VERSION, group)), query, None)
1902
1903 def RenameGroup(self, group, new_name):
1904 """Changes the name of a node group.
1905
1906 @type group: string
1907 @param group: Node group name
1908 @type new_name: string
1909 @param new_name: New node group name
1910
1911 @rtype: string
1912 @return: job id
1913
1914 """
1915 body = {
1916 "new_name": new_name,
1917 }
1918
1919 return self._SendRequest(HTTP_PUT,
1920 ("/%s/groups/%s/rename" %
1921 (GANETI_RAPI_VERSION, group)), None, body)
1922
1923 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1924 """Assigns nodes to a group.
1925
1926 @type group: string
1927 @param group: Node group name
1928 @type nodes: list of strings
1929 @param nodes: List of nodes to assign to the group
1930
1931 @rtype: string
1932 @return: job id
1933
1934 """
1935 query = []
1936 _AppendForceIf(query, force)
1937 _AppendDryRunIf(query, dry_run)
1938
1939 body = {
1940 "nodes": nodes,
1941 }
1942
1943 return self._SendRequest(HTTP_PUT,
1944 ("/%s/groups/%s/assign-nodes" %
1945 (GANETI_RAPI_VERSION, group)), query, body)
1946
1947 def GetGroupTags(self, group):
1948 """Gets tags for a node group.
1949
1950 @type group: string
1951 @param group: Node group whose tags to return
1952
1953 @rtype: list of strings
1954 @return: tags for the group
1955
1956 """
1957 return self._SendRequest(HTTP_GET,
1958 ("/%s/groups/%s/tags" %
1959 (GANETI_RAPI_VERSION, group)), None, None)
1960
1961 def AddGroupTags(self, group, tags, dry_run=False):
1962 """Adds tags to a node group.
1963
1964 @type group: str
1965 @param group: group to add tags to
1966 @type tags: list of string
1967 @param tags: tags to add to the group
1968 @type dry_run: bool
1969 @param dry_run: whether to perform a dry run
1970
1971 @rtype: string
1972 @return: job id
1973
1974 """
1975 query = [("tag", t) for t in tags]
1976 _AppendDryRunIf(query, dry_run)
1977
1978 return self._SendRequest(HTTP_PUT,
1979 ("/%s/groups/%s/tags" %
1980 (GANETI_RAPI_VERSION, group)), query, None)
1981
1982 def DeleteGroupTags(self, group, tags, dry_run=False):
1983 """Deletes tags from a node group.
1984
1985 @type group: str
1986 @param group: group to delete tags from
1987 @type tags: list of string
1988 @param tags: tags to delete
1989 @type dry_run: bool
1990 @param dry_run: whether to perform a dry run
1991 @rtype: string
1992 @return: job id
1993
1994 """
1995 query = [("tag", t) for t in tags]
1996 _AppendDryRunIf(query, dry_run)
1997
1998 return self._SendRequest(HTTP_DELETE,
1999 ("/%s/groups/%s/tags" %
2000 (GANETI_RAPI_VERSION, group)), query, None)
2001
2002 def Query(self, what, fields, qfilter=None):
2003 """Retrieves information about resources.
2004
2005 @type what: string
2006 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2007 @type fields: list of string
2008 @param fields: Requested fields
2009 @type qfilter: None or list
2010 @param qfilter: Query filter
2011
2012 @rtype: string
2013 @return: job id
2014
2015 """
2016 body = {
2017 "fields": fields,
2018 }
2019
2020 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2021 # TODO: remove "filter" after 2.7
2022 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2023
2024 return self._SendRequest(HTTP_PUT,
2025 ("/%s/query/%s" %
2026 (GANETI_RAPI_VERSION, what)), None, body)
2027
2028 def QueryFields(self, what, fields=None):
2029 """Retrieves available fields for a resource.
2030
2031 @type what: string
2032 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2033 @type fields: list of string
2034 @param fields: Requested fields
2035
2036 @rtype: string
2037 @return: job id
2038
2039 """
2040 query = []
2041
2042 if fields is not None:
2043 _AppendIf(query, True, ("fields", ",".join(fields)))
2044
2045 return self._SendRequest(HTTP_GET,
2046 ("/%s/query/%s/fields" %
2047 (GANETI_RAPI_VERSION, what)), query, None)