Also accept just "BoringSSL"
[ganeti-github.git] / lib / rapi / client.py
1 #
2 #
3
4 # Copyright (C) 2010, 2011, 2012 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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 # No Ganeti-specific modules should be imported. The RAPI client is supposed to
43 # be standalone.
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 # Legacy name
107 JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
108
109 # Internal constants
110 _REQ_DATA_VERSION_FIELD = "__version__"
111 _QPARAM_DRY_RUN = "dry-run"
112 _QPARAM_FORCE = "force"
113
114 # Feature strings
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 # Old feature constant names in case they're references by users of this module
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 #: Resolver errors
127 ECODE_RESOLVER = "resolver_error"
128
129 #: Not enough resources (iallocator failure, disk space, memory, etc.)
130 ECODE_NORES = "insufficient_resources"
131
132 #: Temporarily out of resources; operation can be tried again
133 ECODE_TEMP_NORES = "temp_insufficient_resources"
134
135 #: Wrong arguments (at syntax level)
136 ECODE_INVAL = "wrong_input"
137
138 #: Wrong entity state
139 ECODE_STATE = "wrong_state"
140
141 #: Entity not found
142 ECODE_NOENT = "unknown_entity"
143
144 #: Entity already exists
145 ECODE_EXISTS = "already_exists"
146
147 #: Resource not unique (e.g. MAC or IP duplication)
148 ECODE_NOTUNIQUE = "resource_not_unique"
149
150 #: Internal cluster error
151 ECODE_FAULT = "internal_error"
152
153 #: Environment error (e.g. node disk error)
154 ECODE_ENVIRON = "environment_error"
155
156 #: List of all failure types
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 # Older pycURL versions don't have all error constants
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
190
191 class GanetiApiError(Error):
192 """Generic error raised from Ganeti API.
193
194 """
195 def __init__(self, msg, code=None):
196 Error.__init__(self, msg)
197 self.code = code
198
199
200 class CertificateError(GanetiApiError):
201 """Raised when a problem is found with the SSL certificate.
202
203 """
204 pass
205
206
207 def EpochNano():
208 """Return the current timestamp expressed as number of nanoseconds since the
209 unix epoch
210
211 @return: nanoseconds since the Unix epoch
212
213 """
214 return int(time.time() * 1000000000)
215
216
217 def _AppendIf(container, condition, value):
218 """Appends to a list if a condition evaluates to truth.
219
220 """
221 if condition:
222 container.append(value)
223
224 return condition
225
226
227 def _AppendDryRunIf(container, condition):
228 """Appends a "dry-run" parameter if a condition evaluates to truth.
229
230 """
231 return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
232
233
234 def _AppendForceIf(container, condition):
235 """Appends a "force" parameter if a condition evaluates to truth.
236
237 """
238 return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
239
240
241 def _AppendReason(container, reason):
242 """Appends an element to the reason trail.
243
244 If the user provided a reason, it is added to the reason trail.
245
246 """
247 return _AppendIf(container, reason, ("reason", reason))
248
249
250 def _SetItemIf(container, condition, item, value):
251 """Sets an item if a condition evaluates to truth.
252
253 """
254 if condition:
255 container[item] = value
256
257 return condition
258
259
260 def UsesRapiClient(fn):
261 """Decorator for code using RAPI client to initialize pycURL.
262
263 """
264 def wrapper(*args, **kwargs):
265 # curl_global_init(3) and curl_global_cleanup(3) must be called with only
266 # one thread running. This check is just a safety measure -- it doesn't
267 # cover all cases.
268 assert threading.activeCount() == 1, \
269 "Found active threads when initializing pycURL"
270
271 pycurl.global_init(pycurl.GLOBAL_ALL)
272 try:
273 return fn(*args, **kwargs)
274 finally:
275 pycurl.global_cleanup()
276
277 return wrapper
278
279
280 def GenericCurlConfig(verbose=False, use_signal=False,
281 use_curl_cabundle=False, cafile=None, capath=None,
282 proxy=None, verify_hostname=False,
283 connect_timeout=None, timeout=None,
284 _pycurl_version_fn=pycurl.version_info):
285 """Curl configuration function generator.
286
287 @type verbose: bool
288 @param verbose: Whether to set cURL to verbose mode
289 @type use_signal: bool
290 @param use_signal: Whether to allow cURL to use signals
291 @type use_curl_cabundle: bool
292 @param use_curl_cabundle: Whether to use cURL's default CA bundle
293 @type cafile: string
294 @param cafile: In which file we can find the certificates
295 @type capath: string
296 @param capath: In which directory we can find the certificates
297 @type proxy: string
298 @param proxy: Proxy to use, None for default behaviour and empty string for
299 disabling proxies (see curl_easy_setopt(3))
300 @type verify_hostname: bool
301 @param verify_hostname: Whether to verify the remote peer certificate's
302 commonName
303 @type connect_timeout: number
304 @param connect_timeout: Timeout for establishing connection in seconds
305 @type timeout: number
306 @param timeout: Timeout for complete transfer in seconds (see
307 curl_easy_setopt(3)).
308
309 """
310 if use_curl_cabundle and (cafile or capath):
311 raise Error("Can not use default CA bundle when CA file or path is set")
312
313 def _ConfigCurl(curl, logger):
314 """Configures a cURL object
315
316 @type curl: pycurl.Curl
317 @param curl: cURL object
318
319 """
320 logger.debug("Using cURL version %s", pycurl.version)
321
322 # pycurl.version_info returns a tuple with information about the used
323 # version of libcurl. Item 5 is the SSL library linked to it.
324 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
325 # 0, '1.2.3.3', ...)
326 sslver = _pycurl_version_fn()[5]
327 if not sslver:
328 raise Error("No SSL support in cURL")
329
330 lcsslver = sslver.lower()
331 if lcsslver.startswith("openssl/"):
332 pass
333 elif lcsslver.startswith("nss/"):
334 # TODO: investigate compatibility beyond a simple test
335 pass
336 elif lcsslver.startswith("gnutls/"):
337 if capath:
338 raise Error("cURL linked against GnuTLS has no support for a"
339 " CA path (%s)" % (pycurl.version, ))
340 elif lcsslver.startswith("boringssl"):
341 pass
342 else:
343 raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
344 sslver)
345
346 curl.setopt(pycurl.VERBOSE, verbose)
347 curl.setopt(pycurl.NOSIGNAL, not use_signal)
348
349 # Whether to verify remote peer's CN
350 if verify_hostname:
351 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
352 # certificate must indicate that the server is the server to which you
353 # meant to connect, or the connection fails. [...] When the value is 1,
354 # the certificate must contain a Common Name field, but it doesn't matter
355 # what name it says. [...]"
356 curl.setopt(pycurl.SSL_VERIFYHOST, 2)
357 else:
358 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
359
360 if cafile or capath or use_curl_cabundle:
361 # Require certificates to be checked
362 curl.setopt(pycurl.SSL_VERIFYPEER, True)
363 if cafile:
364 curl.setopt(pycurl.CAINFO, str(cafile))
365 if capath:
366 curl.setopt(pycurl.CAPATH, str(capath))
367 # Not changing anything for using default CA bundle
368 else:
369 # Disable SSL certificate verification
370 curl.setopt(pycurl.SSL_VERIFYPEER, False)
371
372 if proxy is not None:
373 curl.setopt(pycurl.PROXY, str(proxy))
374
375 # Timeouts
376 if connect_timeout is not None:
377 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
378 if timeout is not None:
379 curl.setopt(pycurl.TIMEOUT, timeout)
380
381 return _ConfigCurl
382
383
384 class GanetiRapiClient(object): # pylint: disable=R0904
385 """Ganeti RAPI client.
386
387 """
388 USER_AGENT = "Ganeti RAPI Client"
389 _json_encoder = simplejson.JSONEncoder(sort_keys=True)
390
391 def __init__(self, host, port=GANETI_RAPI_PORT,
392 username=None, password=None, logger=logging,
393 curl_config_fn=None, curl_factory=None):
394 """Initializes this class.
395
396 @type host: string
397 @param host: the ganeti cluster master to interact with
398 @type port: int
399 @param port: the port on which the RAPI is running (default is 5080)
400 @type username: string
401 @param username: the username to connect with
402 @type password: string
403 @param password: the password to connect with
404 @type curl_config_fn: callable
405 @param curl_config_fn: Function to configure C{pycurl.Curl} object
406 @param logger: Logging object
407
408 """
409 self._username = username
410 self._password = password
411 self._logger = logger
412 self._curl_config_fn = curl_config_fn
413 self._curl_factory = curl_factory
414
415 try:
416 socket.inet_pton(socket.AF_INET6, host)
417 address = "[%s]:%s" % (host, port)
418 except socket.error:
419 address = "%s:%s" % (host, port)
420
421 self._base_url = "https://%s" % address
422
423 if username is not None:
424 if password is None:
425 raise Error("Password not specified")
426 elif password:
427 raise Error("Specified password without username")
428
429 def _CreateCurl(self):
430 """Creates a cURL object.
431
432 """
433 # Create pycURL object if no factory is provided
434 if self._curl_factory:
435 curl = self._curl_factory()
436 else:
437 curl = pycurl.Curl()
438
439 # Default cURL settings
440 curl.setopt(pycurl.VERBOSE, False)
441 curl.setopt(pycurl.FOLLOWLOCATION, False)
442 curl.setopt(pycurl.MAXREDIRS, 5)
443 curl.setopt(pycurl.NOSIGNAL, True)
444 curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
445 curl.setopt(pycurl.SSL_VERIFYHOST, 0)
446 curl.setopt(pycurl.SSL_VERIFYPEER, False)
447 curl.setopt(pycurl.HTTPHEADER, [
448 "Accept: %s" % HTTP_APP_JSON,
449 "Content-type: %s" % HTTP_APP_JSON,
450 ])
451
452 assert ((self._username is None and self._password is None) ^
453 (self._username is not None and self._password is not None))
454
455 if self._username:
456 # Setup authentication
457 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
458 curl.setopt(pycurl.USERPWD,
459 str("%s:%s" % (self._username, self._password)))
460
461 # Call external configuration function
462 if self._curl_config_fn:
463 self._curl_config_fn(curl, self._logger)
464
465 return curl
466
467 @staticmethod
468 def _EncodeQuery(query):
469 """Encode query values for RAPI URL.
470
471 @type query: list of two-tuples
472 @param query: Query arguments
473 @rtype: list
474 @return: Query list with encoded values
475
476 """
477 result = []
478
479 for name, value in query:
480 if value is None:
481 result.append((name, ""))
482
483 elif isinstance(value, bool):
484 # Boolean values must be encoded as 0 or 1
485 result.append((name, int(value)))
486
487 elif isinstance(value, (list, tuple, dict)):
488 raise ValueError("Invalid query data type %r" % type(value).__name__)
489
490 else:
491 result.append((name, value))
492
493 return result
494
495 def _SendRequest(self, method, path, query, content):
496 """Sends an HTTP request.
497
498 This constructs a full URL, encodes and decodes HTTP bodies, and
499 handles invalid responses in a pythonic way.
500
501 @type method: string
502 @param method: HTTP method to use
503 @type path: string
504 @param path: HTTP URL path
505 @type query: list of two-tuples
506 @param query: query arguments to pass to urllib.urlencode
507 @type content: str or None
508 @param content: HTTP body content
509
510 @rtype: str
511 @return: JSON-Decoded response
512
513 @raises CertificateError: If an invalid SSL certificate is found
514 @raises GanetiApiError: If an invalid response is returned
515
516 """
517 assert path.startswith("/")
518
519 curl = self._CreateCurl()
520
521 if content is not None:
522 encoded_content = self._json_encoder.encode(content)
523 else:
524 encoded_content = ""
525
526 # Build URL
527 urlparts = [self._base_url, path]
528 if query:
529 urlparts.append("?")
530 urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
531
532 url = "".join(urlparts)
533
534 self._logger.debug("Sending request %s %s (content=%r)",
535 method, url, encoded_content)
536
537 # Buffer for response
538 encoded_resp_body = StringIO()
539
540 # Configure cURL
541 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
542 curl.setopt(pycurl.URL, str(url))
543 curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
544 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
545
546 try:
547 # Send request and wait for response
548 try:
549 curl.perform()
550 except pycurl.error, err:
551 if err.args[0] in _CURL_SSL_CERT_ERRORS:
552 raise CertificateError("SSL certificate error %s" % err,
553 code=err.args[0])
554
555 raise GanetiApiError(str(err), code=err.args[0])
556 finally:
557 # Reset settings to not keep references to large objects in memory
558 # between requests
559 curl.setopt(pycurl.POSTFIELDS, "")
560 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
561
562 # Get HTTP response code
563 http_code = curl.getinfo(pycurl.RESPONSE_CODE)
564
565 # Was anything written to the response buffer?
566 if encoded_resp_body.tell():
567 response_content = simplejson.loads(encoded_resp_body.getvalue())
568 else:
569 response_content = None
570
571 if http_code != HTTP_OK:
572 if isinstance(response_content, dict):
573 msg = ("%s %s: %s" %
574 (response_content["code"],
575 response_content["message"],
576 response_content["explain"]))
577 else:
578 msg = str(response_content)
579
580 raise GanetiApiError(msg, code=http_code)
581
582 return response_content
583
584 def GetVersion(self):
585 """Gets the Remote API version running on the cluster.
586
587 @rtype: int
588 @return: Ganeti Remote API version
589
590 """
591 return self._SendRequest(HTTP_GET, "/version", None, None)
592
593 def GetFeatures(self):
594 """Gets the list of optional features supported by RAPI server.
595
596 @rtype: list
597 @return: List of optional features
598
599 """
600 try:
601 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
602 None, None)
603 except GanetiApiError, err:
604 # Older RAPI servers don't support this resource
605 if err.code == HTTP_NOT_FOUND:
606 return []
607
608 raise
609
610 def GetOperatingSystems(self, reason=None):
611 """Gets the Operating Systems running in the Ganeti cluster.
612
613 @rtype: list of str
614 @return: operating systems
615 @type reason: string
616 @param reason: the reason for executing this operation
617
618 """
619 query = []
620 _AppendReason(query, reason)
621 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
622 query, None)
623
624 def GetInfo(self, reason=None):
625 """Gets info about the cluster.
626
627 @type reason: string
628 @param reason: the reason for executing this operation
629 @rtype: dict
630 @return: information about the cluster
631
632 """
633 query = []
634 _AppendReason(query, reason)
635 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
636 query, None)
637
638 def RedistributeConfig(self, reason=None):
639 """Tells the cluster to redistribute its configuration files.
640
641 @type reason: string
642 @param reason: the reason for executing this operation
643 @rtype: string
644 @return: job id
645
646 """
647 query = []
648 _AppendReason(query, reason)
649 return self._SendRequest(HTTP_PUT,
650 "/%s/redistribute-config" % GANETI_RAPI_VERSION,
651 query, None)
652
653 def ModifyCluster(self, reason=None, **kwargs):
654 """Modifies cluster parameters.
655
656 More details for parameters can be found in the RAPI documentation.
657
658 @type reason: string
659 @param reason: the reason for executing this operation
660 @rtype: string
661 @return: job id
662
663 """
664 query = []
665 _AppendReason(query, reason)
666
667 body = kwargs
668
669 return self._SendRequest(HTTP_PUT,
670 "/%s/modify" % GANETI_RAPI_VERSION, query, body)
671
672 def GetClusterTags(self, reason=None):
673 """Gets the cluster tags.
674
675 @type reason: string
676 @param reason: the reason for executing this operation
677 @rtype: list of str
678 @return: cluster tags
679
680 """
681 query = []
682 _AppendReason(query, reason)
683 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
684 query, None)
685
686 def AddClusterTags(self, tags, dry_run=False, reason=None):
687 """Adds tags to the cluster.
688
689 @type tags: list of str
690 @param tags: tags to add to the cluster
691 @type dry_run: bool
692 @param dry_run: whether to perform a dry run
693 @type reason: string
694 @param reason: the reason for executing this operation
695
696 @rtype: string
697 @return: job id
698
699 """
700 query = [("tag", t) for t in tags]
701 _AppendDryRunIf(query, dry_run)
702 _AppendReason(query, reason)
703
704 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
705 query, None)
706
707 def DeleteClusterTags(self, tags, dry_run=False, reason=None):
708 """Deletes tags from the cluster.
709
710 @type tags: list of str
711 @param tags: tags to delete
712 @type dry_run: bool
713 @param dry_run: whether to perform a dry run
714 @type reason: string
715 @param reason: the reason for executing this operation
716 @rtype: string
717 @return: job id
718
719 """
720 query = [("tag", t) for t in tags]
721 _AppendDryRunIf(query, dry_run)
722 _AppendReason(query, reason)
723
724 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
725 query, None)
726
727 def GetInstances(self, bulk=False, reason=None):
728 """Gets information about instances on the cluster.
729
730 @type bulk: bool
731 @param bulk: whether to return all information about all instances
732 @type reason: string
733 @param reason: the reason for executing this operation
734
735 @rtype: list of dict or list of str
736 @return: if bulk is True, info about the instances, else a list of instances
737
738 """
739 query = []
740 _AppendIf(query, bulk, ("bulk", 1))
741 _AppendReason(query, reason)
742
743 instances = self._SendRequest(HTTP_GET,
744 "/%s/instances" % GANETI_RAPI_VERSION,
745 query, None)
746 if bulk:
747 return instances
748 else:
749 return [i["id"] for i in instances]
750
751 def GetInstance(self, instance, reason=None):
752 """Gets information about an instance.
753
754 @type instance: str
755 @param instance: instance whose info to return
756 @type reason: string
757 @param reason: the reason for executing this operation
758
759 @rtype: dict
760 @return: info about the instance
761
762 """
763 query = []
764 _AppendReason(query, reason)
765
766 return self._SendRequest(HTTP_GET,
767 ("/%s/instances/%s" %
768 (GANETI_RAPI_VERSION, instance)), query, None)
769
770 def GetInstanceInfo(self, instance, static=None, reason=None):
771 """Gets information about an instance.
772
773 @type instance: string
774 @param instance: Instance name
775 @type reason: string
776 @param reason: the reason for executing this operation
777 @rtype: string
778 @return: Job ID
779
780 """
781 query = []
782 if static is not None:
783 query.append(("static", static))
784 _AppendReason(query, reason)
785
786 return self._SendRequest(HTTP_GET,
787 ("/%s/instances/%s/info" %
788 (GANETI_RAPI_VERSION, instance)), query, None)
789
790 @staticmethod
791 def _UpdateWithKwargs(base, **kwargs):
792 """Updates the base with params from kwargs.
793
794 @param base: The base dict, filled with required fields
795
796 @note: This is an inplace update of base
797
798 """
799 conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
800 if conflicts:
801 raise GanetiApiError("Required fields can not be specified as"
802 " keywords: %s" % ", ".join(conflicts))
803
804 base.update((key, value) for key, value in kwargs.iteritems()
805 if key != "dry_run")
806
807 def InstanceAllocation(self, mode, name, disk_template, disks, nics,
808 **kwargs):
809 """Generates an instance allocation as used by multiallocate.
810
811 More details for parameters can be found in the RAPI documentation.
812 It is the same as used by CreateInstance.
813
814 @type mode: string
815 @param mode: Instance creation mode
816 @type name: string
817 @param name: Hostname of the instance to create
818 @type disk_template: string
819 @param disk_template: Disk template for instance (e.g. plain, diskless,
820 file, or drbd)
821 @type disks: list of dicts
822 @param disks: List of disk definitions
823 @type nics: list of dicts
824 @param nics: List of NIC definitions
825
826 @return: A dict with the generated entry
827
828 """
829 # All required fields for request data version 1
830 alloc = {
831 "mode": mode,
832 "name": name,
833 "disk_template": disk_template,
834 "disks": disks,
835 "nics": nics,
836 }
837
838 self._UpdateWithKwargs(alloc, **kwargs)
839
840 return alloc
841
842 def InstancesMultiAlloc(self, instances, reason=None, **kwargs):
843 """Tries to allocate multiple instances.
844
845 More details for parameters can be found in the RAPI documentation.
846
847 @param instances: A list of L{InstanceAllocation} results
848
849 """
850 query = []
851 body = {
852 "instances": instances,
853 }
854 self._UpdateWithKwargs(body, **kwargs)
855
856 _AppendDryRunIf(query, kwargs.get("dry_run"))
857 _AppendReason(query, reason)
858
859 return self._SendRequest(HTTP_POST,
860 "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
861 query, body)
862
863 def CreateInstance(self, mode, name, disk_template, disks, nics,
864 reason=None, **kwargs):
865 """Creates a new instance.
866
867 More details for parameters can be found in the RAPI documentation.
868
869 @type mode: string
870 @param mode: Instance creation mode
871 @type name: string
872 @param name: Hostname of the instance to create
873 @type disk_template: string
874 @param disk_template: Disk template for instance (e.g. plain, diskless,
875 file, or drbd)
876 @type disks: list of dicts
877 @param disks: List of disk definitions
878 @type nics: list of dicts
879 @param nics: List of NIC definitions
880 @type dry_run: bool
881 @keyword dry_run: whether to perform a dry run
882 @type reason: string
883 @param reason: the reason for executing this operation
884
885 @rtype: string
886 @return: job id
887
888 """
889 query = []
890
891 _AppendDryRunIf(query, kwargs.get("dry_run"))
892 _AppendReason(query, reason)
893
894 if _INST_CREATE_REQV1 in self.GetFeatures():
895 body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
896 **kwargs)
897 body[_REQ_DATA_VERSION_FIELD] = 1
898 else:
899 raise GanetiApiError("Server does not support new-style (version 1)"
900 " instance creation requests")
901
902 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
903 query, body)
904
905 def DeleteInstance(self, instance, dry_run=False, reason=None, **kwargs):
906 """Deletes an instance.
907
908 @type instance: str
909 @param instance: the instance to delete
910 @type reason: string
911 @param reason: the reason for executing this operation
912
913 @rtype: string
914 @return: job id
915
916 """
917 query = []
918 body = kwargs
919
920 _AppendDryRunIf(query, dry_run)
921 _AppendReason(query, reason)
922
923 return self._SendRequest(HTTP_DELETE,
924 ("/%s/instances/%s" %
925 (GANETI_RAPI_VERSION, instance)), query, body)
926
927 def ModifyInstance(self, instance, reason=None, **kwargs):
928 """Modifies an instance.
929
930 More details for parameters can be found in the RAPI documentation.
931
932 @type instance: string
933 @param instance: Instance name
934 @type reason: string
935 @param reason: the reason for executing this operation
936 @rtype: string
937 @return: job id
938
939 """
940 body = kwargs
941 query = []
942 _AppendReason(query, reason)
943
944 return self._SendRequest(HTTP_PUT,
945 ("/%s/instances/%s/modify" %
946 (GANETI_RAPI_VERSION, instance)), query, body)
947
948 def ActivateInstanceDisks(self, instance, ignore_size=None, reason=None):
949 """Activates an instance's disks.
950
951 @type instance: string
952 @param instance: Instance name
953 @type ignore_size: bool
954 @param ignore_size: Whether to ignore recorded size
955 @type reason: string
956 @param reason: the reason for executing this operation
957 @rtype: string
958 @return: job id
959
960 """
961 query = []
962 _AppendIf(query, ignore_size, ("ignore_size", 1))
963 _AppendReason(query, reason)
964
965 return self._SendRequest(HTTP_PUT,
966 ("/%s/instances/%s/activate-disks" %
967 (GANETI_RAPI_VERSION, instance)), query, None)
968
969 def DeactivateInstanceDisks(self, instance, reason=None):
970 """Deactivates an instance's disks.
971
972 @type instance: string
973 @param instance: Instance name
974 @type reason: string
975 @param reason: the reason for executing this operation
976 @rtype: string
977 @return: job id
978
979 """
980 query = []
981 _AppendReason(query, reason)
982 return self._SendRequest(HTTP_PUT,
983 ("/%s/instances/%s/deactivate-disks" %
984 (GANETI_RAPI_VERSION, instance)), query, None)
985
986 def RecreateInstanceDisks(self, instance, disks=None, nodes=None,
987 reason=None, iallocator=None):
988 """Recreate an instance's disks.
989
990 @type instance: string
991 @param instance: Instance name
992 @type disks: list of int
993 @param disks: List of disk indexes
994 @type nodes: list of string
995 @param nodes: New instance nodes, if relocation is desired
996 @type reason: string
997 @param reason: the reason for executing this operation
998 @type iallocator: str or None
999 @param iallocator: instance allocator plugin to use
1000 @rtype: string
1001 @return: job id
1002
1003 """
1004 body = {}
1005 _SetItemIf(body, disks is not None, "disks", disks)
1006 _SetItemIf(body, nodes is not None, "nodes", nodes)
1007 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1008
1009 query = []
1010 _AppendReason(query, reason)
1011
1012 return self._SendRequest(HTTP_POST,
1013 ("/%s/instances/%s/recreate-disks" %
1014 (GANETI_RAPI_VERSION, instance)), query, body)
1015
1016 def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
1017 reason=None):
1018 """Grows a disk of an instance.
1019
1020 More details for parameters can be found in the RAPI documentation.
1021
1022 @type instance: string
1023 @param instance: Instance name
1024 @type disk: integer
1025 @param disk: Disk index
1026 @type amount: integer
1027 @param amount: Grow disk by this amount (MiB)
1028 @type wait_for_sync: bool
1029 @param wait_for_sync: Wait for disk to synchronize
1030 @type reason: string
1031 @param reason: the reason for executing this operation
1032 @rtype: string
1033 @return: job id
1034
1035 """
1036 body = {
1037 "amount": amount,
1038 }
1039
1040 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1041
1042 query = []
1043 _AppendReason(query, reason)
1044
1045 return self._SendRequest(HTTP_POST,
1046 ("/%s/instances/%s/disk/%s/grow" %
1047 (GANETI_RAPI_VERSION, instance, disk)),
1048 query, body)
1049
1050 def GetInstanceTags(self, instance, reason=None):
1051 """Gets tags for an instance.
1052
1053 @type instance: str
1054 @param instance: instance whose tags to return
1055 @type reason: string
1056 @param reason: the reason for executing this operation
1057
1058 @rtype: list of str
1059 @return: tags for the instance
1060
1061 """
1062 query = []
1063 _AppendReason(query, reason)
1064 return self._SendRequest(HTTP_GET,
1065 ("/%s/instances/%s/tags" %
1066 (GANETI_RAPI_VERSION, instance)), query, None)
1067
1068 def AddInstanceTags(self, instance, tags, dry_run=False, reason=None):
1069 """Adds tags to an instance.
1070
1071 @type instance: str
1072 @param instance: instance to add tags to
1073 @type tags: list of str
1074 @param tags: tags to add to the instance
1075 @type dry_run: bool
1076 @param dry_run: whether to perform a dry run
1077 @type reason: string
1078 @param reason: the reason for executing this operation
1079
1080 @rtype: string
1081 @return: job id
1082
1083 """
1084 query = [("tag", t) for t in tags]
1085 _AppendDryRunIf(query, dry_run)
1086 _AppendReason(query, reason)
1087
1088 return self._SendRequest(HTTP_PUT,
1089 ("/%s/instances/%s/tags" %
1090 (GANETI_RAPI_VERSION, instance)), query, None)
1091
1092 def DeleteInstanceTags(self, instance, tags, dry_run=False, reason=None):
1093 """Deletes tags from an instance.
1094
1095 @type instance: str
1096 @param instance: instance to delete tags from
1097 @type tags: list of str
1098 @param tags: tags to delete
1099 @type dry_run: bool
1100 @param dry_run: whether to perform a dry run
1101 @type reason: string
1102 @param reason: the reason for executing this operation
1103 @rtype: string
1104 @return: job id
1105
1106 """
1107 query = [("tag", t) for t in tags]
1108 _AppendDryRunIf(query, dry_run)
1109 _AppendReason(query, reason)
1110
1111 return self._SendRequest(HTTP_DELETE,
1112 ("/%s/instances/%s/tags" %
1113 (GANETI_RAPI_VERSION, instance)), query, None)
1114
1115 def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1116 dry_run=False, reason=None, **kwargs):
1117 """Reboots an instance.
1118
1119 @type instance: str
1120 @param instance: instance to reboot
1121 @type reboot_type: str
1122 @param reboot_type: one of: hard, soft, full
1123 @type ignore_secondaries: bool
1124 @param ignore_secondaries: if True, ignores errors for the secondary node
1125 while re-assembling disks (in hard-reboot mode only)
1126 @type dry_run: bool
1127 @param dry_run: whether to perform a dry run
1128 @type reason: string
1129 @param reason: the reason for the reboot
1130 @rtype: string
1131 @return: job id
1132
1133 """
1134 query = []
1135 body = kwargs
1136
1137 _AppendDryRunIf(query, dry_run)
1138 _AppendIf(query, reboot_type, ("type", reboot_type))
1139 _AppendIf(query, ignore_secondaries is not None,
1140 ("ignore_secondaries", ignore_secondaries))
1141 _AppendReason(query, reason)
1142
1143 return self._SendRequest(HTTP_POST,
1144 ("/%s/instances/%s/reboot" %
1145 (GANETI_RAPI_VERSION, instance)), query, body)
1146
1147 def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1148 reason=None, **kwargs):
1149 """Shuts down an instance.
1150
1151 @type instance: str
1152 @param instance: the instance to shut down
1153 @type dry_run: bool
1154 @param dry_run: whether to perform a dry run
1155 @type no_remember: bool
1156 @param no_remember: if true, will not record the state change
1157 @type reason: string
1158 @param reason: the reason for the shutdown
1159 @rtype: string
1160 @return: job id
1161
1162 """
1163 query = []
1164 body = kwargs
1165
1166 _AppendDryRunIf(query, dry_run)
1167 _AppendIf(query, no_remember, ("no_remember", 1))
1168 _AppendReason(query, reason)
1169
1170 return self._SendRequest(HTTP_PUT,
1171 ("/%s/instances/%s/shutdown" %
1172 (GANETI_RAPI_VERSION, instance)), query, body)
1173
1174 def StartupInstance(self, instance, dry_run=False, no_remember=False,
1175 reason=None):
1176 """Starts up an instance.
1177
1178 @type instance: str
1179 @param instance: the instance to start up
1180 @type dry_run: bool
1181 @param dry_run: whether to perform a dry run
1182 @type no_remember: bool
1183 @param no_remember: if true, will not record the state change
1184 @type reason: string
1185 @param reason: the reason for the startup
1186 @rtype: string
1187 @return: job id
1188
1189 """
1190 query = []
1191 _AppendDryRunIf(query, dry_run)
1192 _AppendIf(query, no_remember, ("no_remember", 1))
1193 _AppendReason(query, reason)
1194
1195 return self._SendRequest(HTTP_PUT,
1196 ("/%s/instances/%s/startup" %
1197 (GANETI_RAPI_VERSION, instance)), query, None)
1198
1199 def ReinstallInstance(self, instance, os=None, no_startup=False,
1200 osparams=None, reason=None):
1201 """Reinstalls an instance.
1202
1203 @type instance: str
1204 @param instance: The instance to reinstall
1205 @type os: str or None
1206 @param os: The operating system to reinstall. If None, the instance's
1207 current operating system will be installed again
1208 @type no_startup: bool
1209 @param no_startup: Whether to start the instance automatically
1210 @type reason: string
1211 @param reason: the reason for executing this operation
1212 @rtype: string
1213 @return: job id
1214
1215 """
1216 query = []
1217 _AppendReason(query, reason)
1218
1219 if _INST_REINSTALL_REQV1 in self.GetFeatures():
1220 body = {
1221 "start": not no_startup,
1222 }
1223 _SetItemIf(body, os is not None, "os", os)
1224 _SetItemIf(body, osparams is not None, "osparams", osparams)
1225 return self._SendRequest(HTTP_POST,
1226 ("/%s/instances/%s/reinstall" %
1227 (GANETI_RAPI_VERSION, instance)), query, body)
1228
1229 # Use old request format
1230 if osparams:
1231 raise GanetiApiError("Server does not support specifying OS parameters"
1232 " for instance reinstallation")
1233
1234 query = []
1235 _AppendIf(query, os, ("os", os))
1236 _AppendIf(query, no_startup, ("nostartup", 1))
1237
1238 return self._SendRequest(HTTP_POST,
1239 ("/%s/instances/%s/reinstall" %
1240 (GANETI_RAPI_VERSION, instance)), query, None)
1241
1242 def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1243 remote_node=None, iallocator=None, reason=None,
1244 early_release=None):
1245 """Replaces disks on an instance.
1246
1247 @type instance: str
1248 @param instance: instance whose disks to replace
1249 @type disks: list of ints
1250 @param disks: Indexes of disks to replace
1251 @type mode: str
1252 @param mode: replacement mode to use (defaults to replace_auto)
1253 @type remote_node: str or None
1254 @param remote_node: new secondary node to use (for use with
1255 replace_new_secondary mode)
1256 @type iallocator: str or None
1257 @param iallocator: instance allocator plugin to use (for use with
1258 replace_auto mode)
1259 @type reason: string
1260 @param reason: the reason for executing this operation
1261 @type early_release: bool
1262 @param early_release: whether to release locks as soon as possible
1263
1264 @rtype: string
1265 @return: job id
1266
1267 """
1268 query = [
1269 ("mode", mode),
1270 ]
1271
1272 # TODO: Convert to body parameters
1273
1274 if disks is not None:
1275 _AppendIf(query, True,
1276 ("disks", ",".join(str(idx) for idx in disks)))
1277
1278 _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1279 _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1280 _AppendReason(query, reason)
1281 _AppendIf(query, early_release is not None,
1282 ("early_release", early_release))
1283
1284 return self._SendRequest(HTTP_POST,
1285 ("/%s/instances/%s/replace-disks" %
1286 (GANETI_RAPI_VERSION, instance)), query, None)
1287
1288 def PrepareExport(self, instance, mode, reason=None):
1289 """Prepares an instance for an export.
1290
1291 @type instance: string
1292 @param instance: Instance name
1293 @type mode: string
1294 @param mode: Export mode
1295 @type reason: string
1296 @param reason: the reason for executing this operation
1297 @rtype: string
1298 @return: Job ID
1299
1300 """
1301 query = [("mode", mode)]
1302 _AppendReason(query, reason)
1303 return self._SendRequest(HTTP_PUT,
1304 ("/%s/instances/%s/prepare-export" %
1305 (GANETI_RAPI_VERSION, instance)), query, None)
1306
1307 def ExportInstance(self, instance, mode, destination, shutdown=None,
1308 remove_instance=None, x509_key_name=None,
1309 destination_x509_ca=None, compress=None, reason=None):
1310 """Exports an instance.
1311
1312 @type instance: string
1313 @param instance: Instance name
1314 @type mode: string
1315 @param mode: Export mode
1316 @type reason: string
1317 @param reason: the reason for executing this operation
1318 @rtype: string
1319 @return: Job ID
1320
1321 """
1322 body = {
1323 "destination": destination,
1324 "mode": mode,
1325 }
1326
1327 _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1328 _SetItemIf(body, remove_instance is not None,
1329 "remove_instance", remove_instance)
1330 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1331 _SetItemIf(body, destination_x509_ca is not None,
1332 "destination_x509_ca", destination_x509_ca)
1333 _SetItemIf(body, compress is not None, "compress", compress)
1334
1335 query = []
1336 _AppendReason(query, reason)
1337
1338 return self._SendRequest(HTTP_PUT,
1339 ("/%s/instances/%s/export" %
1340 (GANETI_RAPI_VERSION, instance)), query, body)
1341
1342 def MigrateInstance(self, instance, mode=None, cleanup=None,
1343 target_node=None, reason=None):
1344 """Migrates an instance.
1345
1346 @type instance: string
1347 @param instance: Instance name
1348 @type mode: string
1349 @param mode: Migration mode
1350 @type cleanup: bool
1351 @param cleanup: Whether to clean up a previously failed migration
1352 @type target_node: string
1353 @param target_node: Target Node for externally mirrored instances
1354 @type reason: string
1355 @param reason: the reason for executing this operation
1356 @rtype: string
1357 @return: job id
1358
1359 """
1360 body = {}
1361 _SetItemIf(body, mode is not None, "mode", mode)
1362 _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1363 _SetItemIf(body, target_node is not None, "target_node", target_node)
1364
1365 query = []
1366 _AppendReason(query, reason)
1367
1368 return self._SendRequest(HTTP_PUT,
1369 ("/%s/instances/%s/migrate" %
1370 (GANETI_RAPI_VERSION, instance)), query, body)
1371
1372 def FailoverInstance(self, instance, iallocator=None,
1373 ignore_consistency=None, target_node=None, reason=None):
1374 """Does a failover of an instance.
1375
1376 @type instance: string
1377 @param instance: Instance name
1378 @type iallocator: string
1379 @param iallocator: Iallocator for deciding the target node for
1380 shared-storage instances
1381 @type ignore_consistency: bool
1382 @param ignore_consistency: Whether to ignore disk consistency
1383 @type target_node: string
1384 @param target_node: Target node for shared-storage instances
1385 @type reason: string
1386 @param reason: the reason for executing this operation
1387 @rtype: string
1388 @return: job id
1389
1390 """
1391 body = {}
1392 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1393 _SetItemIf(body, ignore_consistency is not None,
1394 "ignore_consistency", ignore_consistency)
1395 _SetItemIf(body, target_node is not None, "target_node", target_node)
1396
1397 query = []
1398 _AppendReason(query, reason)
1399
1400 return self._SendRequest(HTTP_PUT,
1401 ("/%s/instances/%s/failover" %
1402 (GANETI_RAPI_VERSION, instance)), query, body)
1403
1404 def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1405 reason=None):
1406 """Changes the name of an instance.
1407
1408 @type instance: string
1409 @param instance: Instance name
1410 @type new_name: string
1411 @param new_name: New instance name
1412 @type ip_check: bool
1413 @param ip_check: Whether to ensure instance's IP address is inactive
1414 @type name_check: bool
1415 @param name_check: Whether to ensure instance's name is resolvable
1416 @type reason: string
1417 @param reason: the reason for executing this operation
1418 @rtype: string
1419 @return: job id
1420
1421 """
1422 body = {
1423 "new_name": new_name,
1424 }
1425
1426 _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1427 _SetItemIf(body, name_check is not None, "name_check", name_check)
1428
1429 query = []
1430 _AppendReason(query, reason)
1431
1432 return self._SendRequest(HTTP_PUT,
1433 ("/%s/instances/%s/rename" %
1434 (GANETI_RAPI_VERSION, instance)), query, body)
1435
1436 def GetInstanceConsole(self, instance, reason=None):
1437 """Request information for connecting to instance's console.
1438
1439 @type instance: string
1440 @param instance: Instance name
1441 @type reason: string
1442 @param reason: the reason for executing this operation
1443 @rtype: dict
1444 @return: dictionary containing information about instance's console
1445
1446 """
1447 query = []
1448 _AppendReason(query, reason)
1449 return self._SendRequest(HTTP_GET,
1450 ("/%s/instances/%s/console" %
1451 (GANETI_RAPI_VERSION, instance)), query, None)
1452
1453 def GetJobs(self, bulk=False):
1454 """Gets all jobs for the cluster.
1455
1456 @type bulk: bool
1457 @param bulk: Whether to return detailed information about jobs.
1458 @rtype: list of int
1459 @return: List of job ids for the cluster or list of dicts with detailed
1460 information about the jobs if bulk parameter was true.
1461
1462 """
1463 query = []
1464 _AppendIf(query, bulk, ("bulk", 1))
1465
1466 if bulk:
1467 return self._SendRequest(HTTP_GET,
1468 "/%s/jobs" % GANETI_RAPI_VERSION,
1469 query, None)
1470 else:
1471 return [int(j["id"])
1472 for j in self._SendRequest(HTTP_GET,
1473 "/%s/jobs" % GANETI_RAPI_VERSION,
1474 None, None)]
1475
1476 def GetJobStatus(self, job_id):
1477 """Gets the status of a job.
1478
1479 @type job_id: string
1480 @param job_id: job id whose status to query
1481
1482 @rtype: dict
1483 @return: job status
1484
1485 """
1486 return self._SendRequest(HTTP_GET,
1487 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1488 None, None)
1489
1490 def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1491 """Polls cluster for job status until completion.
1492
1493 Completion is defined as any of the following states listed in
1494 L{JOB_STATUS_FINALIZED}.
1495
1496 @type job_id: string
1497 @param job_id: job id to watch
1498 @type period: int
1499 @param period: how often to poll for status (optional, default 5s)
1500 @type retries: int
1501 @param retries: how many time to poll before giving up
1502 (optional, default -1 means unlimited)
1503
1504 @rtype: bool
1505 @return: C{True} if job succeeded or C{False} if failed/status timeout
1506 @deprecated: It is recommended to use L{WaitForJobChange} wherever
1507 possible; L{WaitForJobChange} returns immediately after a job changed and
1508 does not use polling
1509
1510 """
1511 while retries != 0:
1512 job_result = self.GetJobStatus(job_id)
1513
1514 if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1515 return True
1516 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1517 return False
1518
1519 if period:
1520 time.sleep(period)
1521
1522 if retries > 0:
1523 retries -= 1
1524
1525 return False
1526
1527 def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1528 """Waits for job changes.
1529
1530 @type job_id: string
1531 @param job_id: Job ID for which to wait
1532 @return: C{None} if no changes have been detected and a dict with two keys,
1533 C{job_info} and C{log_entries} otherwise.
1534 @rtype: dict
1535
1536 """
1537 body = {
1538 "fields": fields,
1539 "previous_job_info": prev_job_info,
1540 "previous_log_serial": prev_log_serial,
1541 }
1542
1543 return self._SendRequest(HTTP_GET,
1544 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1545 None, body)
1546
1547 def CancelJob(self, job_id, dry_run=False):
1548 """Cancels a job.
1549
1550 @type job_id: string
1551 @param job_id: id of the job to delete
1552 @type dry_run: bool
1553 @param dry_run: whether to perform a dry run
1554 @rtype: tuple
1555 @return: tuple containing the result, and a message (bool, string)
1556
1557 """
1558 query = []
1559 _AppendDryRunIf(query, dry_run)
1560
1561 return self._SendRequest(HTTP_DELETE,
1562 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1563 query, None)
1564
1565 def GetNodes(self, bulk=False, reason=None):
1566 """Gets all nodes in the cluster.
1567
1568 @type bulk: bool
1569 @param bulk: whether to return all information about all instances
1570 @type reason: string
1571 @param reason: the reason for executing this operation
1572
1573 @rtype: list of dict or str
1574 @return: if bulk is true, info about nodes in the cluster,
1575 else list of nodes in the cluster
1576
1577 """
1578 query = []
1579 _AppendIf(query, bulk, ("bulk", 1))
1580 _AppendReason(query, reason)
1581
1582 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1583 query, None)
1584 if bulk:
1585 return nodes
1586 else:
1587 return [n["id"] for n in nodes]
1588
1589 def GetNode(self, node, reason=None):
1590 """Gets information about a node.
1591
1592 @type node: str
1593 @param node: node whose info to return
1594 @type reason: string
1595 @param reason: the reason for executing this operation
1596
1597 @rtype: dict
1598 @return: info about the node
1599
1600 """
1601 query = []
1602 _AppendReason(query, reason)
1603
1604 return self._SendRequest(HTTP_GET,
1605 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1606 query, None)
1607
1608 def EvacuateNode(self, node, iallocator=None, remote_node=None,
1609 dry_run=False, early_release=None,
1610 mode=None, accept_old=False, reason=None):
1611 """Evacuates instances from a Ganeti node.
1612
1613 @type node: str
1614 @param node: node to evacuate
1615 @type iallocator: str or None
1616 @param iallocator: instance allocator to use
1617 @type remote_node: str
1618 @param remote_node: node to evaucate to
1619 @type dry_run: bool
1620 @param dry_run: whether to perform a dry run
1621 @type early_release: bool
1622 @param early_release: whether to enable parallelization
1623 @type mode: string
1624 @param mode: Node evacuation mode
1625 @type accept_old: bool
1626 @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1627 results
1628 @type reason: string
1629 @param reason: the reason for executing this operation
1630
1631 @rtype: string, or a list for pre-2.5 results
1632 @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1633 list of (job ID, instance name, new secondary node); if dry_run was
1634 specified, then the actual move jobs were not submitted and the job IDs
1635 will be C{None}
1636
1637 @raises GanetiApiError: if an iallocator and remote_node are both
1638 specified
1639
1640 """
1641 if iallocator and remote_node:
1642 raise GanetiApiError("Only one of iallocator or remote_node can be used")
1643
1644 query = []
1645 _AppendDryRunIf(query, dry_run)
1646 _AppendReason(query, reason)
1647
1648 if _NODE_EVAC_RES1 in self.GetFeatures():
1649 # Server supports body parameters
1650 body = {}
1651
1652 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1653 _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1654 _SetItemIf(body, early_release is not None,
1655 "early_release", early_release)
1656 _SetItemIf(body, mode is not None, "mode", mode)
1657 else:
1658 # Pre-2.5 request format
1659 body = None
1660
1661 if not accept_old:
1662 raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1663 " not accept old-style results (parameter"
1664 " accept_old)")
1665
1666 # Pre-2.5 servers can only evacuate secondaries
1667 if mode is not None and mode != NODE_EVAC_SEC:
1668 raise GanetiApiError("Server can only evacuate secondary instances")
1669
1670 _AppendIf(query, iallocator, ("iallocator", iallocator))
1671 _AppendIf(query, remote_node, ("remote_node", remote_node))
1672 _AppendIf(query, early_release, ("early_release", 1))
1673
1674 return self._SendRequest(HTTP_POST,
1675 ("/%s/nodes/%s/evacuate" %
1676 (GANETI_RAPI_VERSION, node)), query, body)
1677
1678 def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1679 target_node=None, reason=None):
1680 """Migrates all primary instances from a node.
1681
1682 @type node: str
1683 @param node: node to migrate
1684 @type mode: string
1685 @param mode: if passed, it will overwrite the live migration type,
1686 otherwise the hypervisor default will be used
1687 @type dry_run: bool
1688 @param dry_run: whether to perform a dry run
1689 @type iallocator: string
1690 @param iallocator: instance allocator to use
1691 @type target_node: string
1692 @param target_node: Target node for shared-storage instances
1693 @type reason: string
1694 @param reason: the reason for executing this operation
1695
1696 @rtype: string
1697 @return: job id
1698
1699 """
1700 query = []
1701 _AppendDryRunIf(query, dry_run)
1702 _AppendReason(query, reason)
1703
1704 if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1705 body = {}
1706
1707 _SetItemIf(body, mode is not None, "mode", mode)
1708 _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1709 _SetItemIf(body, target_node is not None, "target_node", target_node)
1710
1711 assert len(query) <= 1
1712
1713 return self._SendRequest(HTTP_POST,
1714 ("/%s/nodes/%s/migrate" %
1715 (GANETI_RAPI_VERSION, node)), query, body)
1716 else:
1717 # Use old request format
1718 if target_node is not None:
1719 raise GanetiApiError("Server does not support specifying target node"
1720 " for node migration")
1721
1722 _AppendIf(query, mode is not None, ("mode", mode))
1723
1724 return self._SendRequest(HTTP_POST,
1725 ("/%s/nodes/%s/migrate" %
1726 (GANETI_RAPI_VERSION, node)), query, None)
1727
1728 def GetNodeRole(self, node, reason=None):
1729 """Gets the current role for a node.
1730
1731 @type node: str
1732 @param node: node whose role to return
1733 @type reason: string
1734 @param reason: the reason for executing this operation
1735
1736 @rtype: str
1737 @return: the current role for a node
1738
1739 """
1740 query = []
1741 _AppendReason(query, reason)
1742
1743 return self._SendRequest(HTTP_GET,
1744 ("/%s/nodes/%s/role" %
1745 (GANETI_RAPI_VERSION, node)), query, None)
1746
1747 def SetNodeRole(self, node, role, force=False, auto_promote=None,
1748 reason=None):
1749 """Sets the role for a node.
1750
1751 @type node: str
1752 @param node: the node whose role to set
1753 @type role: str
1754 @param role: the role to set for the node
1755 @type force: bool
1756 @param force: whether to force the role change
1757 @type auto_promote: bool
1758 @param auto_promote: Whether node(s) should be promoted to master candidate
1759 if necessary
1760 @type reason: string
1761 @param reason: the reason for executing this operation
1762
1763 @rtype: string
1764 @return: job id
1765
1766 """
1767 query = []
1768 _AppendForceIf(query, force)
1769 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1770 _AppendReason(query, reason)
1771
1772 return self._SendRequest(HTTP_PUT,
1773 ("/%s/nodes/%s/role" %
1774 (GANETI_RAPI_VERSION, node)), query, role)
1775
1776 def PowercycleNode(self, node, force=False, reason=None):
1777 """Powercycles a node.
1778
1779 @type node: string
1780 @param node: Node name
1781 @type force: bool
1782 @param force: Whether to force the operation
1783 @type reason: string
1784 @param reason: the reason for executing this operation
1785 @rtype: string
1786 @return: job id
1787
1788 """
1789 query = []
1790 _AppendForceIf(query, force)
1791 _AppendReason(query, reason)
1792
1793 return self._SendRequest(HTTP_POST,
1794 ("/%s/nodes/%s/powercycle" %
1795 (GANETI_RAPI_VERSION, node)), query, None)
1796
1797 def ModifyNode(self, node, reason=None, **kwargs):
1798 """Modifies a node.
1799
1800 More details for parameters can be found in the RAPI documentation.
1801
1802 @type node: string
1803 @param node: Node name
1804 @type reason: string
1805 @param reason: the reason for executing this operation
1806 @rtype: string
1807 @return: job id
1808
1809 """
1810 query = []
1811 _AppendReason(query, reason)
1812
1813 return self._SendRequest(HTTP_POST,
1814 ("/%s/nodes/%s/modify" %
1815 (GANETI_RAPI_VERSION, node)), query, kwargs)
1816
1817 def GetNodeStorageUnits(self, node, storage_type, output_fields, reason=None):
1818 """Gets the storage units for a node.
1819
1820 @type node: str
1821 @param node: the node whose storage units to return
1822 @type storage_type: str
1823 @param storage_type: storage type whose units to return
1824 @type output_fields: str
1825 @param output_fields: storage type fields to return
1826 @type reason: string
1827 @param reason: the reason for executing this operation
1828
1829 @rtype: string
1830 @return: job id where results can be retrieved
1831
1832 """
1833 query = [
1834 ("storage_type", storage_type),
1835 ("output_fields", output_fields),
1836 ]
1837 _AppendReason(query, reason)
1838
1839 return self._SendRequest(HTTP_GET,
1840 ("/%s/nodes/%s/storage" %
1841 (GANETI_RAPI_VERSION, node)), query, None)
1842
1843 def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None,
1844 reason=None):
1845 """Modifies parameters of storage units on the node.
1846
1847 @type node: str
1848 @param node: node whose storage units to modify
1849 @type storage_type: str
1850 @param storage_type: storage type whose units to modify
1851 @type name: str
1852 @param name: name of the storage unit
1853 @type allocatable: bool or None
1854 @param allocatable: Whether to set the "allocatable" flag on the storage
1855 unit (None=no modification, True=set, False=unset)
1856 @type reason: string
1857 @param reason: the reason for executing this operation
1858
1859 @rtype: string
1860 @return: job id
1861
1862 """
1863 query = [
1864 ("storage_type", storage_type),
1865 ("name", name),
1866 ]
1867
1868 _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1869 _AppendReason(query, reason)
1870
1871 return self._SendRequest(HTTP_PUT,
1872 ("/%s/nodes/%s/storage/modify" %
1873 (GANETI_RAPI_VERSION, node)), query, None)
1874
1875 def RepairNodeStorageUnits(self, node, storage_type, name, reason=None):
1876 """Repairs a storage unit on the node.
1877
1878 @type node: str
1879 @param node: node whose storage units to repair
1880 @type storage_type: str
1881 @param storage_type: storage type to repair
1882 @type name: str
1883 @param name: name of the storage unit to repair
1884 @type reason: string
1885 @param reason: the reason for executing this operation
1886
1887 @rtype: string
1888 @return: job id
1889
1890 """
1891 query = [
1892 ("storage_type", storage_type),
1893 ("name", name),
1894 ]
1895 _AppendReason(query, reason)
1896
1897 return self._SendRequest(HTTP_PUT,
1898 ("/%s/nodes/%s/storage/repair" %
1899 (GANETI_RAPI_VERSION, node)), query, None)
1900
1901 def GetNodeTags(self, node, reason=None):
1902 """Gets the tags for a node.
1903
1904 @type node: str
1905 @param node: node whose tags to return
1906 @type reason: string
1907 @param reason: the reason for executing this operation
1908
1909 @rtype: list of str
1910 @return: tags for the node
1911
1912 """
1913 query = []
1914 _AppendReason(query, reason)
1915
1916 return self._SendRequest(HTTP_GET,
1917 ("/%s/nodes/%s/tags" %
1918 (GANETI_RAPI_VERSION, node)), query, None)
1919
1920 def AddNodeTags(self, node, tags, dry_run=False, reason=None):
1921 """Adds tags to a node.
1922
1923 @type node: str
1924 @param node: node to add tags to
1925 @type tags: list of str
1926 @param tags: tags to add to the node
1927 @type dry_run: bool
1928 @param dry_run: whether to perform a dry run
1929 @type reason: string
1930 @param reason: the reason for executing this operation
1931
1932 @rtype: string
1933 @return: job id
1934
1935 """
1936 query = [("tag", t) for t in tags]
1937 _AppendDryRunIf(query, dry_run)
1938 _AppendReason(query, reason)
1939
1940 return self._SendRequest(HTTP_PUT,
1941 ("/%s/nodes/%s/tags" %
1942 (GANETI_RAPI_VERSION, node)), query, tags)
1943
1944 def DeleteNodeTags(self, node, tags, dry_run=False, reason=None):
1945 """Delete tags from a node.
1946
1947 @type node: str
1948 @param node: node to remove tags from
1949 @type tags: list of str
1950 @param tags: tags to remove from the node
1951 @type dry_run: bool
1952 @param dry_run: whether to perform a dry run
1953 @type reason: string
1954 @param reason: the reason for executing this operation
1955
1956 @rtype: string
1957 @return: job id
1958
1959 """
1960 query = [("tag", t) for t in tags]
1961 _AppendDryRunIf(query, dry_run)
1962 _AppendReason(query, reason)
1963
1964 return self._SendRequest(HTTP_DELETE,
1965 ("/%s/nodes/%s/tags" %
1966 (GANETI_RAPI_VERSION, node)), query, None)
1967
1968 def GetNetworks(self, bulk=False, reason=None):
1969 """Gets all networks in the cluster.
1970
1971 @type bulk: bool
1972 @param bulk: whether to return all information about the networks
1973
1974 @rtype: list of dict or str
1975 @return: if bulk is true, a list of dictionaries with info about all
1976 networks in the cluster, else a list of names of those networks
1977
1978 """
1979 query = []
1980 _AppendIf(query, bulk, ("bulk", 1))
1981 _AppendReason(query, reason)
1982
1983 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1984 query, None)
1985 if bulk:
1986 return networks
1987 else:
1988 return [n["name"] for n in networks]
1989
1990 def GetNetwork(self, network, reason=None):
1991 """Gets information about a network.
1992
1993 @type network: str
1994 @param network: name of the network whose info to return
1995 @type reason: string
1996 @param reason: the reason for executing this operation
1997
1998 @rtype: dict
1999 @return: info about the network
2000
2001 """
2002 query = []
2003 _AppendReason(query, reason)
2004
2005 return self._SendRequest(HTTP_GET,
2006 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
2007 query, None)
2008
2009 def CreateNetwork(self, network_name, network, gateway=None, network6=None,
2010 gateway6=None, mac_prefix=None,
2011 add_reserved_ips=None, tags=None, dry_run=False,
2012 reason=None):
2013 """Creates a new network.
2014
2015 @type network_name: str
2016 @param network_name: the name of network to create
2017 @type dry_run: bool
2018 @param dry_run: whether to peform a dry run
2019 @type reason: string
2020 @param reason: the reason for executing this operation
2021
2022 @rtype: string
2023 @return: job id
2024
2025 """
2026 query = []
2027 _AppendDryRunIf(query, dry_run)
2028 _AppendReason(query, reason)
2029
2030 if add_reserved_ips:
2031 add_reserved_ips = add_reserved_ips.split(",")
2032
2033 if tags:
2034 tags = tags.split(",")
2035
2036 body = {
2037 "network_name": network_name,
2038 "gateway": gateway,
2039 "network": network,
2040 "gateway6": gateway6,
2041 "network6": network6,
2042 "mac_prefix": mac_prefix,
2043 "add_reserved_ips": add_reserved_ips,
2044 "tags": tags,
2045 }
2046
2047 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2048 query, body)
2049
2050 def ConnectNetwork(self, network_name, group_name, mode, link,
2051 vlan="", dry_run=False, reason=None):
2052 """Connects a Network to a NodeGroup with the given netparams
2053
2054 """
2055 body = {
2056 "group_name": group_name,
2057 "network_mode": mode,
2058 "network_link": link,
2059 "network_vlan": vlan,
2060 }
2061
2062 query = []
2063 _AppendDryRunIf(query, dry_run)
2064 _AppendReason(query, reason)
2065
2066 return self._SendRequest(HTTP_PUT,
2067 ("/%s/networks/%s/connect" %
2068 (GANETI_RAPI_VERSION, network_name)), query, body)
2069
2070 def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2071 reason=None):
2072 """Connects a Network to a NodeGroup with the given netparams
2073
2074 """
2075 body = {
2076 "group_name": group_name,
2077 }
2078
2079 query = []
2080 _AppendDryRunIf(query, dry_run)
2081 _AppendReason(query, reason)
2082
2083 return self._SendRequest(HTTP_PUT,
2084 ("/%s/networks/%s/disconnect" %
2085 (GANETI_RAPI_VERSION, network_name)), query, body)
2086
2087 def ModifyNetwork(self, network, reason=None, **kwargs):
2088 """Modifies a network.
2089
2090 More details for parameters can be found in the RAPI documentation.
2091
2092 @type network: string
2093 @param network: Network name
2094 @type reason: string
2095 @param reason: the reason for executing this operation
2096 @rtype: string
2097 @return: job id
2098
2099 """
2100 query = []
2101 _AppendReason(query, reason)
2102
2103 return self._SendRequest(HTTP_PUT,
2104 ("/%s/networks/%s/modify" %
2105 (GANETI_RAPI_VERSION, network)), None, kwargs)
2106
2107 def DeleteNetwork(self, network, dry_run=False, reason=None):
2108 """Deletes a network.
2109
2110 @type network: str
2111 @param network: the network to delete
2112 @type dry_run: bool
2113 @param dry_run: whether to peform a dry run
2114 @type reason: string
2115 @param reason: the reason for executing this operation
2116
2117 @rtype: string
2118 @return: job id
2119
2120 """
2121 query = []
2122 _AppendDryRunIf(query, dry_run)
2123 _AppendReason(query, reason)
2124
2125 return self._SendRequest(HTTP_DELETE,
2126 ("/%s/networks/%s" %
2127 (GANETI_RAPI_VERSION, network)), query, None)
2128
2129 def GetNetworkTags(self, network, reason=None):
2130 """Gets tags for a network.
2131
2132 @type network: string
2133 @param network: Node group whose tags to return
2134 @type reason: string
2135 @param reason: the reason for executing this operation
2136
2137 @rtype: list of strings
2138 @return: tags for the network
2139
2140 """
2141 query = []
2142 _AppendReason(query, reason)
2143
2144 return self._SendRequest(HTTP_GET,
2145 ("/%s/networks/%s/tags" %
2146 (GANETI_RAPI_VERSION, network)), query, None)
2147
2148 def AddNetworkTags(self, network, tags, dry_run=False, reason=None):
2149 """Adds tags to a network.
2150
2151 @type network: str
2152 @param network: network to add tags to
2153 @type tags: list of string
2154 @param tags: tags to add to the network
2155 @type dry_run: bool
2156 @param dry_run: whether to perform a dry run
2157 @type reason: string
2158 @param reason: the reason for executing this operation
2159
2160 @rtype: string
2161 @return: job id
2162
2163 """
2164 query = [("tag", t) for t in tags]
2165 _AppendDryRunIf(query, dry_run)
2166 _AppendReason(query, reason)
2167
2168 return self._SendRequest(HTTP_PUT,
2169 ("/%s/networks/%s/tags" %
2170 (GANETI_RAPI_VERSION, network)), query, None)
2171
2172 def DeleteNetworkTags(self, network, tags, dry_run=False, reason=None):
2173 """Deletes tags from a network.
2174
2175 @type network: str
2176 @param network: network to delete tags from
2177 @type tags: list of string
2178 @param tags: tags to delete
2179 @type dry_run: bool
2180 @param dry_run: whether to perform a dry run
2181 @type reason: string
2182 @param reason: the reason for executing this operation
2183 @rtype: string
2184 @return: job id
2185
2186 """
2187 query = [("tag", t) for t in tags]
2188 _AppendDryRunIf(query, dry_run)
2189 _AppendReason(query, reason)
2190
2191 return self._SendRequest(HTTP_DELETE,
2192 ("/%s/networks/%s/tags" %
2193 (GANETI_RAPI_VERSION, network)), query, None)
2194
2195 def GetGroups(self, bulk=False, reason=None):
2196 """Gets all node groups in the cluster.
2197
2198 @type bulk: bool
2199 @param bulk: whether to return all information about the groups
2200 @type reason: string
2201 @param reason: the reason for executing this operation
2202
2203 @rtype: list of dict or str
2204 @return: if bulk is true, a list of dictionaries with info about all node
2205 groups in the cluster, else a list of names of those node groups
2206
2207 """
2208 query = []
2209 _AppendIf(query, bulk, ("bulk", 1))
2210 _AppendReason(query, reason)
2211
2212 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2213 query, None)
2214 if bulk:
2215 return groups
2216 else:
2217 return [g["name"] for g in groups]
2218
2219 def GetGroup(self, group, reason=None):
2220 """Gets information about a node group.
2221
2222 @type group: str
2223 @param group: name of the node group whose info to return
2224 @type reason: string
2225 @param reason: the reason for executing this operation
2226
2227 @rtype: dict
2228 @return: info about the node group
2229
2230 """
2231 query = []
2232 _AppendReason(query, reason)
2233
2234 return self._SendRequest(HTTP_GET,
2235 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2236 query, None)
2237
2238 def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2239 """Creates a new node group.
2240
2241 @type name: str
2242 @param name: the name of node group to create
2243 @type alloc_policy: str
2244 @param alloc_policy: the desired allocation policy for the group, if any
2245 @type dry_run: bool
2246 @param dry_run: whether to peform a dry run
2247 @type reason: string
2248 @param reason: the reason for executing this operation
2249
2250 @rtype: string
2251 @return: job id
2252
2253 """
2254 query = []
2255 _AppendDryRunIf(query, dry_run)
2256 _AppendReason(query, reason)
2257
2258 body = {
2259 "name": name,
2260 "alloc_policy": alloc_policy,
2261 }
2262
2263 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2264 query, body)
2265
2266 def ModifyGroup(self, group, reason=None, **kwargs):
2267 """Modifies a node group.
2268
2269 More details for parameters can be found in the RAPI documentation.
2270
2271 @type group: string
2272 @param group: Node group name
2273 @type reason: string
2274 @param reason: the reason for executing this operation
2275 @rtype: string
2276 @return: job id
2277
2278 """
2279 query = []
2280 _AppendReason(query, reason)
2281
2282 return self._SendRequest(HTTP_PUT,
2283 ("/%s/groups/%s/modify" %
2284 (GANETI_RAPI_VERSION, group)), query, kwargs)
2285
2286 def DeleteGroup(self, group, dry_run=False, reason=None):
2287 """Deletes a node group.
2288
2289 @type group: str
2290 @param group: the node group to delete
2291 @type dry_run: bool
2292 @param dry_run: whether to peform a dry run
2293 @type reason: string
2294 @param reason: the reason for executing this operation
2295
2296 @rtype: string
2297 @return: job id
2298
2299 """
2300 query = []
2301 _AppendDryRunIf(query, dry_run)
2302 _AppendReason(query, reason)
2303
2304 return self._SendRequest(HTTP_DELETE,
2305 ("/%s/groups/%s" %
2306 (GANETI_RAPI_VERSION, group)), query, None)
2307
2308 def RenameGroup(self, group, new_name, reason=None):
2309 """Changes the name of a node group.
2310
2311 @type group: string
2312 @param group: Node group name
2313 @type new_name: string
2314 @param new_name: New node group name
2315 @type reason: string
2316 @param reason: the reason for executing this operation
2317
2318 @rtype: string
2319 @return: job id
2320
2321 """
2322 body = {
2323 "new_name": new_name,
2324 }
2325
2326 query = []
2327 _AppendReason(query, reason)
2328
2329 return self._SendRequest(HTTP_PUT,
2330 ("/%s/groups/%s/rename" %
2331 (GANETI_RAPI_VERSION, group)), query, body)
2332
2333 def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2334 reason=None):
2335 """Assigns nodes to a group.
2336
2337 @type group: string
2338 @param group: Node group name
2339 @type nodes: list of strings
2340 @param nodes: List of nodes to assign to the group
2341 @type reason: string
2342 @param reason: the reason for executing this operation
2343
2344 @rtype: string
2345 @return: job id
2346
2347 """
2348 query = []
2349 _AppendForceIf(query, force)
2350 _AppendDryRunIf(query, dry_run)
2351 _AppendReason(query, reason)
2352
2353 body = {
2354 "nodes": nodes,
2355 }
2356
2357 return self._SendRequest(HTTP_PUT,
2358 ("/%s/groups/%s/assign-nodes" %
2359 (GANETI_RAPI_VERSION, group)), query, body)
2360
2361 def GetGroupTags(self, group, reason=None):
2362 """Gets tags for a node group.
2363
2364 @type group: string
2365 @param group: Node group whose tags to return
2366 @type reason: string
2367 @param reason: the reason for executing this operation
2368
2369 @rtype: list of strings
2370 @return: tags for the group
2371
2372 """
2373 query = []
2374 _AppendReason(query, reason)
2375
2376 return self._SendRequest(HTTP_GET,
2377 ("/%s/groups/%s/tags" %
2378 (GANETI_RAPI_VERSION, group)), query, None)
2379
2380 def AddGroupTags(self, group, tags, dry_run=False, reason=None):
2381 """Adds tags to a node group.
2382
2383 @type group: str
2384 @param group: group to add tags to
2385 @type tags: list of string
2386 @param tags: tags to add to the group
2387 @type dry_run: bool
2388 @param dry_run: whether to perform a dry run
2389 @type reason: string
2390 @param reason: the reason for executing this operation
2391
2392 @rtype: string
2393 @return: job id
2394
2395 """
2396 query = [("tag", t) for t in tags]
2397 _AppendDryRunIf(query, dry_run)
2398 _AppendReason(query, reason)
2399
2400 return self._SendRequest(HTTP_PUT,
2401 ("/%s/groups/%s/tags" %
2402 (GANETI_RAPI_VERSION, group)), query, None)
2403
2404 def DeleteGroupTags(self, group, tags, dry_run=False, reason=None):
2405 """Deletes tags from a node group.
2406
2407 @type group: str
2408 @param group: group to delete tags from
2409 @type tags: list of string
2410 @param tags: tags to delete
2411 @type dry_run: bool
2412 @param dry_run: whether to perform a dry run
2413 @type reason: string
2414 @param reason: the reason for executing this operation
2415 @rtype: string
2416 @return: job id
2417
2418 """
2419 query = [("tag", t) for t in tags]
2420 _AppendDryRunIf(query, dry_run)
2421 _AppendReason(query, reason)
2422
2423 return self._SendRequest(HTTP_DELETE,
2424 ("/%s/groups/%s/tags" %
2425 (GANETI_RAPI_VERSION, group)), query, None)
2426
2427 def Query(self, what, fields, qfilter=None, reason=None):
2428 """Retrieves information about resources.
2429
2430 @type what: string
2431 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2432 @type fields: list of string
2433 @param fields: Requested fields
2434 @type qfilter: None or list
2435 @param qfilter: Query filter
2436 @type reason: string
2437 @param reason: the reason for executing this operation
2438
2439 @rtype: string
2440 @return: job id
2441
2442 """
2443 query = []
2444 _AppendReason(query, reason)
2445
2446 body = {
2447 "fields": fields,
2448 }
2449
2450 _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2451 # TODO: remove "filter" after 2.7
2452 _SetItemIf(body, qfilter is not None, "filter", qfilter)
2453
2454 return self._SendRequest(HTTP_PUT,
2455 ("/%s/query/%s" %
2456 (GANETI_RAPI_VERSION, what)), query, body)
2457
2458 def QueryFields(self, what, fields=None, reason=None):
2459 """Retrieves available fields for a resource.
2460
2461 @type what: string
2462 @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2463 @type fields: list of string
2464 @param fields: Requested fields
2465 @type reason: string
2466 @param reason: the reason for executing this operation
2467
2468 @rtype: string
2469 @return: job id
2470
2471 """
2472 query = []
2473 _AppendReason(query, reason)
2474
2475 if fields is not None:
2476 _AppendIf(query, True, ("fields", ",".join(fields)))
2477
2478 return self._SendRequest(HTTP_GET,
2479 ("/%s/query/%s/fields" %
2480 (GANETI_RAPI_VERSION, what)), query, None)
2481
2482 def GetFilters(self, bulk=False):
2483 """Gets all filter rules in the cluster.
2484
2485 @type bulk: bool
2486 @param bulk: whether to return all information about the networks
2487
2488 @rtype: list of dict or str
2489 @return: if bulk is true, a list of dictionaries with info about all
2490 filter rules in the cluster, else a list of UUIDs of those
2491 filters
2492
2493 """
2494 query = []
2495 _AppendIf(query, bulk, ("bulk", 1))
2496
2497 filters = self._SendRequest(HTTP_GET, "/%s/filters" % GANETI_RAPI_VERSION,
2498 query, None)
2499 if bulk:
2500 return filters
2501 else:
2502 return [f["uuid"] for f in filters]
2503
2504 def GetFilter(self, filter_uuid):
2505 """Gets information about a filter rule.
2506
2507 @type filter_uuid: str
2508 @param filter_uuid: UUID of the filter whose info to return
2509
2510 @rtype: dict
2511 @return: info about the filter
2512
2513 """
2514 query = []
2515
2516 return self._SendRequest(HTTP_GET,
2517 "/%s/filters/%s" % (GANETI_RAPI_VERSION,
2518 filter_uuid),
2519 query, None)
2520
2521 def AddFilter(self, priority, predicates, action, reason_trail=None):
2522 """Adds a filter rule
2523
2524 @type reason_trail: list of (str, str, int) triples
2525 @param reason_trail: the reason trail for executing this operation,
2526 or None
2527
2528 @rtype: string
2529 @return: filter UUID of the added filter
2530
2531 """
2532 if reason_trail is None:
2533 reason_trail = []
2534
2535 assert isinstance(reason_trail, list)
2536
2537 reason_trail.append(("gnt:client", "", EpochNano(),)) # add client reason
2538
2539 body = {
2540 "priority": priority,
2541 "predicates": predicates,
2542 "action": action,
2543 "reason": reason_trail,
2544 }
2545
2546 query = []
2547
2548 return self._SendRequest(HTTP_POST,
2549 ("/%s/filters" % (GANETI_RAPI_VERSION)),
2550 query, body)
2551
2552 def ReplaceFilter(self, uuid, priority, predicates, action,
2553 reason_trail=None):
2554 """Replaces a filter rule, or creates one if it doesn't already exist
2555
2556 @type reason_trail: list of (str, str, int) triples
2557 @param reason_trail: the reason trail for executing this operation,
2558 or None
2559
2560 @rtype: string
2561 @return: filter UUID of the replaced/added filter
2562
2563 """
2564 if reason_trail is None:
2565 reason_trail = []
2566
2567 assert isinstance(reason_trail, list)
2568
2569 reason_trail.append(("gnt:client", "", EpochNano(),)) # add client reason
2570
2571 body = {
2572 "priority": priority,
2573 "predicates": predicates,
2574 "action": action,
2575 "reason": reason_trail,
2576 }
2577
2578 query = []
2579
2580 return self._SendRequest(HTTP_PUT,
2581 ("/%s/filters/%s" % (GANETI_RAPI_VERSION, uuid)),
2582 query, body)
2583
2584 def DeleteFilter(self, uuid):
2585 """Deletes a filter rule
2586
2587 @return: None
2588
2589 """
2590 body = {}
2591 query = []
2592
2593 return self._SendRequest(HTTP_DELETE,
2594 ("/%s/filters/%s" % (GANETI_RAPI_VERSION, uuid)),
2595 query, body)