Package ganeti :: Package rapi :: Module client
[hide private]
[frames] | no frames]

Source Code for Module ganeti.rapi.client

   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, force=False):
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 _AppendForceIf(query, force) 982 _AppendReason(query, reason) 983 return self._SendRequest(HTTP_PUT, 984 ("/%s/instances/%s/deactivate-disks" % 985 (GANETI_RAPI_VERSION, instance)), query, None)
986
987 - def RecreateInstanceDisks(self, instance, disks=None, nodes=None, 988 reason=None, iallocator=None):
989 """Recreate an instance's disks. 990 991 @type instance: string 992 @param instance: Instance name 993 @type disks: list of int 994 @param disks: List of disk indexes 995 @type nodes: list of string 996 @param nodes: New instance nodes, if relocation is desired 997 @type reason: string 998 @param reason: the reason for executing this operation 999 @type iallocator: str or None 1000 @param iallocator: instance allocator plugin to use 1001 @rtype: string 1002 @return: job id 1003 1004 """ 1005 body = {} 1006 _SetItemIf(body, disks is not None, "disks", disks) 1007 _SetItemIf(body, nodes is not None, "nodes", nodes) 1008 _SetItemIf(body, iallocator is not None, "iallocator", iallocator) 1009 1010 query = [] 1011 _AppendReason(query, reason) 1012 1013 return self._SendRequest(HTTP_POST, 1014 ("/%s/instances/%s/recreate-disks" % 1015 (GANETI_RAPI_VERSION, instance)), query, body)
1016
1017 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None, 1018 reason=None):
1019 """Grows a disk of an instance. 1020 1021 More details for parameters can be found in the RAPI documentation. 1022 1023 @type instance: string 1024 @param instance: Instance name 1025 @type disk: integer 1026 @param disk: Disk index 1027 @type amount: integer 1028 @param amount: Grow disk by this amount (MiB) 1029 @type wait_for_sync: bool 1030 @param wait_for_sync: Wait for disk to synchronize 1031 @type reason: string 1032 @param reason: the reason for executing this operation 1033 @rtype: string 1034 @return: job id 1035 1036 """ 1037 body = { 1038 "amount": amount, 1039 } 1040 1041 _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync) 1042 1043 query = [] 1044 _AppendReason(query, reason) 1045 1046 return self._SendRequest(HTTP_POST, 1047 ("/%s/instances/%s/disk/%s/grow" % 1048 (GANETI_RAPI_VERSION, instance, disk)), 1049 query, body)
1050
1051 - def GetInstanceTags(self, instance, reason=None):
1052 """Gets tags for an instance. 1053 1054 @type instance: str 1055 @param instance: instance whose tags to return 1056 @type reason: string 1057 @param reason: the reason for executing this operation 1058 1059 @rtype: list of str 1060 @return: tags for the instance 1061 1062 """ 1063 query = [] 1064 _AppendReason(query, reason) 1065 return self._SendRequest(HTTP_GET, 1066 ("/%s/instances/%s/tags" % 1067 (GANETI_RAPI_VERSION, instance)), query, None)
1068
1069 - def AddInstanceTags(self, instance, tags, dry_run=False, reason=None):
1070 """Adds tags to an instance. 1071 1072 @type instance: str 1073 @param instance: instance to add tags to 1074 @type tags: list of str 1075 @param tags: tags to add to the instance 1076 @type dry_run: bool 1077 @param dry_run: whether to perform a dry run 1078 @type reason: string 1079 @param reason: the reason for executing this operation 1080 1081 @rtype: string 1082 @return: job id 1083 1084 """ 1085 query = [("tag", t) for t in tags] 1086 _AppendDryRunIf(query, dry_run) 1087 _AppendReason(query, reason) 1088 1089 return self._SendRequest(HTTP_PUT, 1090 ("/%s/instances/%s/tags" % 1091 (GANETI_RAPI_VERSION, instance)), query, None)
1092
1093 - def DeleteInstanceTags(self, instance, tags, dry_run=False, reason=None):
1094 """Deletes tags from an instance. 1095 1096 @type instance: str 1097 @param instance: instance to delete tags from 1098 @type tags: list of str 1099 @param tags: tags to delete 1100 @type dry_run: bool 1101 @param dry_run: whether to perform a dry run 1102 @type reason: string 1103 @param reason: the reason for executing this operation 1104 @rtype: string 1105 @return: job id 1106 1107 """ 1108 query = [("tag", t) for t in tags] 1109 _AppendDryRunIf(query, dry_run) 1110 _AppendReason(query, reason) 1111 1112 return self._SendRequest(HTTP_DELETE, 1113 ("/%s/instances/%s/tags" % 1114 (GANETI_RAPI_VERSION, instance)), query, None)
1115
1116 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None, 1117 dry_run=False, reason=None, **kwargs):
1118 """Reboots an instance. 1119 1120 @type instance: str 1121 @param instance: instance to reboot 1122 @type reboot_type: str 1123 @param reboot_type: one of: hard, soft, full 1124 @type ignore_secondaries: bool 1125 @param ignore_secondaries: if True, ignores errors for the secondary node 1126 while re-assembling disks (in hard-reboot mode only) 1127 @type dry_run: bool 1128 @param dry_run: whether to perform a dry run 1129 @type reason: string 1130 @param reason: the reason for the reboot 1131 @rtype: string 1132 @return: job id 1133 1134 """ 1135 query = [] 1136 body = kwargs 1137 1138 _AppendDryRunIf(query, dry_run) 1139 _AppendIf(query, reboot_type, ("type", reboot_type)) 1140 _AppendIf(query, ignore_secondaries is not None, 1141 ("ignore_secondaries", ignore_secondaries)) 1142 _AppendReason(query, reason) 1143 1144 return self._SendRequest(HTTP_POST, 1145 ("/%s/instances/%s/reboot" % 1146 (GANETI_RAPI_VERSION, instance)), query, body)
1147
1148 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False, 1149 reason=None, **kwargs):
1150 """Shuts down an instance. 1151 1152 @type instance: str 1153 @param instance: the instance to shut down 1154 @type dry_run: bool 1155 @param dry_run: whether to perform a dry run 1156 @type no_remember: bool 1157 @param no_remember: if true, will not record the state change 1158 @type reason: string 1159 @param reason: the reason for the shutdown 1160 @rtype: string 1161 @return: job id 1162 1163 """ 1164 query = [] 1165 body = kwargs 1166 1167 _AppendDryRunIf(query, dry_run) 1168 _AppendIf(query, no_remember, ("no_remember", 1)) 1169 _AppendReason(query, reason) 1170 1171 return self._SendRequest(HTTP_PUT, 1172 ("/%s/instances/%s/shutdown" % 1173 (GANETI_RAPI_VERSION, instance)), query, body)
1174
1175 - def StartupInstance(self, instance, dry_run=False, no_remember=False, 1176 reason=None):
1177 """Starts up an instance. 1178 1179 @type instance: str 1180 @param instance: the instance to start up 1181 @type dry_run: bool 1182 @param dry_run: whether to perform a dry run 1183 @type no_remember: bool 1184 @param no_remember: if true, will not record the state change 1185 @type reason: string 1186 @param reason: the reason for the startup 1187 @rtype: string 1188 @return: job id 1189 1190 """ 1191 query = [] 1192 _AppendDryRunIf(query, dry_run) 1193 _AppendIf(query, no_remember, ("no_remember", 1)) 1194 _AppendReason(query, reason) 1195 1196 return self._SendRequest(HTTP_PUT, 1197 ("/%s/instances/%s/startup" % 1198 (GANETI_RAPI_VERSION, instance)), query, None)
1199
1200 - def ReinstallInstance(self, instance, os=None, no_startup=False, 1201 osparams=None, reason=None):
1202 """Reinstalls an instance. 1203 1204 @type instance: str 1205 @param instance: The instance to reinstall 1206 @type os: str or None 1207 @param os: The operating system to reinstall. If None, the instance's 1208 current operating system will be installed again 1209 @type no_startup: bool 1210 @param no_startup: Whether to start the instance automatically 1211 @type reason: string 1212 @param reason: the reason for executing this operation 1213 @rtype: string 1214 @return: job id 1215 1216 """ 1217 query = [] 1218 _AppendReason(query, reason) 1219 1220 if _INST_REINSTALL_REQV1 in self.GetFeatures(): 1221 body = { 1222 "start": not no_startup, 1223 } 1224 _SetItemIf(body, os is not None, "os", os) 1225 _SetItemIf(body, osparams is not None, "osparams", osparams) 1226 return self._SendRequest(HTTP_POST, 1227 ("/%s/instances/%s/reinstall" % 1228 (GANETI_RAPI_VERSION, instance)), query, body) 1229 1230 # Use old request format 1231 if osparams: 1232 raise GanetiApiError("Server does not support specifying OS parameters" 1233 " for instance reinstallation") 1234 1235 query = [] 1236 _AppendIf(query, os, ("os", os)) 1237 _AppendIf(query, no_startup, ("nostartup", 1)) 1238 1239 return self._SendRequest(HTTP_POST, 1240 ("/%s/instances/%s/reinstall" % 1241 (GANETI_RAPI_VERSION, instance)), query, None)
1242
1243 - def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO, 1244 remote_node=None, iallocator=None, reason=None, 1245 early_release=None):
1246 """Replaces disks on an instance. 1247 1248 @type instance: str 1249 @param instance: instance whose disks to replace 1250 @type disks: list of ints 1251 @param disks: Indexes of disks to replace 1252 @type mode: str 1253 @param mode: replacement mode to use (defaults to replace_auto) 1254 @type remote_node: str or None 1255 @param remote_node: new secondary node to use (for use with 1256 replace_new_secondary mode) 1257 @type iallocator: str or None 1258 @param iallocator: instance allocator plugin to use (for use with 1259 replace_auto mode) 1260 @type reason: string 1261 @param reason: the reason for executing this operation 1262 @type early_release: bool 1263 @param early_release: whether to release locks as soon as possible 1264 1265 @rtype: string 1266 @return: job id 1267 1268 """ 1269 query = [ 1270 ("mode", mode), 1271 ] 1272 1273 # TODO: Convert to body parameters 1274 1275 if disks is not None: 1276 _AppendIf(query, True, 1277 ("disks", ",".join(str(idx) for idx in disks))) 1278 1279 _AppendIf(query, remote_node is not None, ("remote_node", remote_node)) 1280 _AppendIf(query, iallocator is not None, ("iallocator", iallocator)) 1281 _AppendReason(query, reason) 1282 _AppendIf(query, early_release is not None, 1283 ("early_release", early_release)) 1284 1285 return self._SendRequest(HTTP_POST, 1286 ("/%s/instances/%s/replace-disks" % 1287 (GANETI_RAPI_VERSION, instance)), query, None)
1288
1289 - def PrepareExport(self, instance, mode, reason=None):
1290 """Prepares an instance for an export. 1291 1292 @type instance: string 1293 @param instance: Instance name 1294 @type mode: string 1295 @param mode: Export mode 1296 @type reason: string 1297 @param reason: the reason for executing this operation 1298 @rtype: string 1299 @return: Job ID 1300 1301 """ 1302 query = [("mode", mode)] 1303 _AppendReason(query, reason) 1304 return self._SendRequest(HTTP_PUT, 1305 ("/%s/instances/%s/prepare-export" % 1306 (GANETI_RAPI_VERSION, instance)), query, None)
1307
1308 - def ExportInstance(self, instance, mode, destination, shutdown=None, 1309 remove_instance=None, x509_key_name=None, 1310 destination_x509_ca=None, compress=None, reason=None):
1311 """Exports an instance. 1312 1313 @type instance: string 1314 @param instance: Instance name 1315 @type mode: string 1316 @param mode: Export mode 1317 @type reason: string 1318 @param reason: the reason for executing this operation 1319 @rtype: string 1320 @return: Job ID 1321 1322 """ 1323 body = { 1324 "destination": destination, 1325 "mode": mode, 1326 } 1327 1328 _SetItemIf(body, shutdown is not None, "shutdown", shutdown) 1329 _SetItemIf(body, remove_instance is not None, 1330 "remove_instance", remove_instance) 1331 _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name) 1332 _SetItemIf(body, destination_x509_ca is not None, 1333 "destination_x509_ca", destination_x509_ca) 1334 _SetItemIf(body, compress is not None, "compress", compress) 1335 1336 query = [] 1337 _AppendReason(query, reason) 1338 1339 return self._SendRequest(HTTP_PUT, 1340 ("/%s/instances/%s/export" % 1341 (GANETI_RAPI_VERSION, instance)), query, body)
1342
1343 - def MigrateInstance(self, instance, mode=None, cleanup=None, 1344 target_node=None, reason=None):
1345 """Migrates an instance. 1346 1347 @type instance: string 1348 @param instance: Instance name 1349 @type mode: string 1350 @param mode: Migration mode 1351 @type cleanup: bool 1352 @param cleanup: Whether to clean up a previously failed migration 1353 @type target_node: string 1354 @param target_node: Target Node for externally mirrored instances 1355 @type reason: string 1356 @param reason: the reason for executing this operation 1357 @rtype: string 1358 @return: job id 1359 1360 """ 1361 body = {} 1362 _SetItemIf(body, mode is not None, "mode", mode) 1363 _SetItemIf(body, cleanup is not None, "cleanup", cleanup) 1364 _SetItemIf(body, target_node is not None, "target_node", target_node) 1365 1366 query = [] 1367 _AppendReason(query, reason) 1368 1369 return self._SendRequest(HTTP_PUT, 1370 ("/%s/instances/%s/migrate" % 1371 (GANETI_RAPI_VERSION, instance)), query, body)
1372
1373 - def FailoverInstance(self, instance, iallocator=None, 1374 ignore_consistency=None, target_node=None, reason=None):
1375 """Does a failover of an instance. 1376 1377 @type instance: string 1378 @param instance: Instance name 1379 @type iallocator: string 1380 @param iallocator: Iallocator for deciding the target node for 1381 shared-storage instances 1382 @type ignore_consistency: bool 1383 @param ignore_consistency: Whether to ignore disk consistency 1384 @type target_node: string 1385 @param target_node: Target node for shared-storage instances 1386 @type reason: string 1387 @param reason: the reason for executing this operation 1388 @rtype: string 1389 @return: job id 1390 1391 """ 1392 body = {} 1393 _SetItemIf(body, iallocator is not None, "iallocator", iallocator) 1394 _SetItemIf(body, ignore_consistency is not None, 1395 "ignore_consistency", ignore_consistency) 1396 _SetItemIf(body, target_node is not None, "target_node", target_node) 1397 1398 query = [] 1399 _AppendReason(query, reason) 1400 1401 return self._SendRequest(HTTP_PUT, 1402 ("/%s/instances/%s/failover" % 1403 (GANETI_RAPI_VERSION, instance)), query, body)
1404
1405 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None, 1406 reason=None):
1407 """Changes the name of an instance. 1408 1409 @type instance: string 1410 @param instance: Instance name 1411 @type new_name: string 1412 @param new_name: New instance name 1413 @type ip_check: bool 1414 @param ip_check: Whether to ensure instance's IP address is inactive 1415 @type name_check: bool 1416 @param name_check: Whether to ensure instance's name is resolvable 1417 @type reason: string 1418 @param reason: the reason for executing this operation 1419 @rtype: string 1420 @return: job id 1421 1422 """ 1423 body = { 1424 "new_name": new_name, 1425 } 1426 1427 _SetItemIf(body, ip_check is not None, "ip_check", ip_check) 1428 _SetItemIf(body, name_check is not None, "name_check", name_check) 1429 1430 query = [] 1431 _AppendReason(query, reason) 1432 1433 return self._SendRequest(HTTP_PUT, 1434 ("/%s/instances/%s/rename" % 1435 (GANETI_RAPI_VERSION, instance)), query, body)
1436
1437 - def GetInstanceConsole(self, instance, reason=None):
1438 """Request information for connecting to instance's console. 1439 1440 @type instance: string 1441 @param instance: Instance name 1442 @type reason: string 1443 @param reason: the reason for executing this operation 1444 @rtype: dict 1445 @return: dictionary containing information about instance's console 1446 1447 """ 1448 query = [] 1449 _AppendReason(query, reason) 1450 return self._SendRequest(HTTP_GET, 1451 ("/%s/instances/%s/console" % 1452 (GANETI_RAPI_VERSION, instance)), query, None)
1453
1454 - def GetJobs(self, bulk=False):
1455 """Gets all jobs for the cluster. 1456 1457 @type bulk: bool 1458 @param bulk: Whether to return detailed information about jobs. 1459 @rtype: list of int 1460 @return: List of job ids for the cluster or list of dicts with detailed 1461 information about the jobs if bulk parameter was true. 1462 1463 """ 1464 query = [] 1465 _AppendIf(query, bulk, ("bulk", 1)) 1466 1467 if bulk: 1468 return self._SendRequest(HTTP_GET, 1469 "/%s/jobs" % GANETI_RAPI_VERSION, 1470 query, None) 1471 else: 1472 return [int(j["id"]) 1473 for j in self._SendRequest(HTTP_GET, 1474 "/%s/jobs" % GANETI_RAPI_VERSION, 1475 None, None)]
1476
1477 - def GetJobStatus(self, job_id):
1478 """Gets the status of a job. 1479 1480 @type job_id: string 1481 @param job_id: job id whose status to query 1482 1483 @rtype: dict 1484 @return: job status 1485 1486 """ 1487 return self._SendRequest(HTTP_GET, 1488 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1489 None, None)
1490
1491 - def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1492 """Polls cluster for job status until completion. 1493 1494 Completion is defined as any of the following states listed in 1495 L{JOB_STATUS_FINALIZED}. 1496 1497 @type job_id: string 1498 @param job_id: job id to watch 1499 @type period: int 1500 @param period: how often to poll for status (optional, default 5s) 1501 @type retries: int 1502 @param retries: how many time to poll before giving up 1503 (optional, default -1 means unlimited) 1504 1505 @rtype: bool 1506 @return: C{True} if job succeeded or C{False} if failed/status timeout 1507 @deprecated: It is recommended to use L{WaitForJobChange} wherever 1508 possible; L{WaitForJobChange} returns immediately after a job changed and 1509 does not use polling 1510 1511 """ 1512 while retries != 0: 1513 job_result = self.GetJobStatus(job_id) 1514 1515 if job_result and job_result["status"] == JOB_STATUS_SUCCESS: 1516 return True 1517 elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED: 1518 return False 1519 1520 if period: 1521 time.sleep(period) 1522 1523 if retries > 0: 1524 retries -= 1 1525 1526 return False
1527
1528 - def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1529 """Waits for job changes. 1530 1531 @type job_id: string 1532 @param job_id: Job ID for which to wait 1533 @return: C{None} if no changes have been detected and a dict with two keys, 1534 C{job_info} and C{log_entries} otherwise. 1535 @rtype: dict 1536 1537 """ 1538 body = { 1539 "fields": fields, 1540 "previous_job_info": prev_job_info, 1541 "previous_log_serial": prev_log_serial, 1542 } 1543 1544 return self._SendRequest(HTTP_GET, 1545 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id), 1546 None, body)
1547
1548 - def CancelJob(self, job_id, dry_run=False):
1549 """Cancels a job. 1550 1551 @type job_id: string 1552 @param job_id: id of the job to delete 1553 @type dry_run: bool 1554 @param dry_run: whether to perform a dry run 1555 @rtype: tuple 1556 @return: tuple containing the result, and a message (bool, string) 1557 1558 """ 1559 query = [] 1560 _AppendDryRunIf(query, dry_run) 1561 1562 return self._SendRequest(HTTP_DELETE, 1563 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1564 query, None)
1565
1566 - def GetNodes(self, bulk=False, reason=None):
1567 """Gets all nodes in the cluster. 1568 1569 @type bulk: bool 1570 @param bulk: whether to return all information about all instances 1571 @type reason: string 1572 @param reason: the reason for executing this operation 1573 1574 @rtype: list of dict or str 1575 @return: if bulk is true, info about nodes in the cluster, 1576 else list of nodes in the cluster 1577 1578 """ 1579 query = [] 1580 _AppendIf(query, bulk, ("bulk", 1)) 1581 _AppendReason(query, reason) 1582 1583 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION, 1584 query, None) 1585 if bulk: 1586 return nodes 1587 else: 1588 return [n["id"] for n in nodes]
1589
1590 - def GetNode(self, node, reason=None):
1591 """Gets information about a node. 1592 1593 @type node: str 1594 @param node: node whose info to return 1595 @type reason: string 1596 @param reason: the reason for executing this operation 1597 1598 @rtype: dict 1599 @return: info about the node 1600 1601 """ 1602 query = [] 1603 _AppendReason(query, reason) 1604 1605 return self._SendRequest(HTTP_GET, 1606 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node), 1607 query, None)
1608
1609 - def EvacuateNode(self, node, iallocator=None, remote_node=None, 1610 dry_run=False, early_release=None, 1611 mode=None, accept_old=False, reason=None):
1612 """Evacuates instances from a Ganeti node. 1613 1614 @type node: str 1615 @param node: node to evacuate 1616 @type iallocator: str or None 1617 @param iallocator: instance allocator to use 1618 @type remote_node: str 1619 @param remote_node: node to evaucate to 1620 @type dry_run: bool 1621 @param dry_run: whether to perform a dry run 1622 @type early_release: bool 1623 @param early_release: whether to enable parallelization 1624 @type mode: string 1625 @param mode: Node evacuation mode 1626 @type accept_old: bool 1627 @param accept_old: Whether caller is ready to accept old-style (pre-2.5) 1628 results 1629 @type reason: string 1630 @param reason: the reason for executing this operation 1631 1632 @rtype: string, or a list for pre-2.5 results 1633 @return: Job ID or, if C{accept_old} is set and server is pre-2.5, 1634 list of (job ID, instance name, new secondary node); if dry_run was 1635 specified, then the actual move jobs were not submitted and the job IDs 1636 will be C{None} 1637 1638 @raises GanetiApiError: if an iallocator and remote_node are both 1639 specified 1640 1641 """ 1642 if iallocator and remote_node: 1643 raise GanetiApiError("Only one of iallocator or remote_node can be used") 1644 1645 query = [] 1646 _AppendDryRunIf(query, dry_run) 1647 _AppendReason(query, reason) 1648 1649 if _NODE_EVAC_RES1 in self.GetFeatures(): 1650 # Server supports body parameters 1651 body = {} 1652 1653 _SetItemIf(body, iallocator is not None, "iallocator", iallocator) 1654 _SetItemIf(body, remote_node is not None, "remote_node", remote_node) 1655 _SetItemIf(body, early_release is not None, 1656 "early_release", early_release) 1657 _SetItemIf(body, mode is not None, "mode", mode) 1658 else: 1659 # Pre-2.5 request format 1660 body = None 1661 1662 if not accept_old: 1663 raise GanetiApiError("Server is version 2.4 or earlier and caller does" 1664 " not accept old-style results (parameter" 1665 " accept_old)") 1666 1667 # Pre-2.5 servers can only evacuate secondaries 1668 if mode is not None and mode != NODE_EVAC_SEC: 1669 raise GanetiApiError("Server can only evacuate secondary instances") 1670 1671 _AppendIf(query, iallocator, ("iallocator", iallocator)) 1672 _AppendIf(query, remote_node, ("remote_node", remote_node)) 1673 _AppendIf(query, early_release, ("early_release", 1)) 1674 1675 return self._SendRequest(HTTP_POST, 1676 ("/%s/nodes/%s/evacuate" % 1677 (GANETI_RAPI_VERSION, node)), query, body)
1678
1679 - def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None, 1680 target_node=None, reason=None):
1681 """Migrates all primary instances from a node. 1682 1683 @type node: str 1684 @param node: node to migrate 1685 @type mode: string 1686 @param mode: if passed, it will overwrite the live migration type, 1687 otherwise the hypervisor default will be used 1688 @type dry_run: bool 1689 @param dry_run: whether to perform a dry run 1690 @type iallocator: string 1691 @param iallocator: instance allocator to use 1692 @type target_node: string 1693 @param target_node: Target node for shared-storage instances 1694 @type reason: string 1695 @param reason: the reason for executing this operation 1696 1697 @rtype: string 1698 @return: job id 1699 1700 """ 1701 query = [] 1702 _AppendDryRunIf(query, dry_run) 1703 _AppendReason(query, reason) 1704 1705 if _NODE_MIGRATE_REQV1 in self.GetFeatures(): 1706 body = {} 1707 1708 _SetItemIf(body, mode is not None, "mode", mode) 1709 _SetItemIf(body, iallocator is not None, "iallocator", iallocator) 1710 _SetItemIf(body, target_node is not None, "target_node", target_node) 1711 1712 assert len(query) <= 1 1713 1714 return self._SendRequest(HTTP_POST, 1715 ("/%s/nodes/%s/migrate" % 1716 (GANETI_RAPI_VERSION, node)), query, body) 1717 else: 1718 # Use old request format 1719 if target_node is not None: 1720 raise GanetiApiError("Server does not support specifying target node" 1721 " for node migration") 1722 1723 _AppendIf(query, mode is not None, ("mode", mode)) 1724 1725 return self._SendRequest(HTTP_POST, 1726 ("/%s/nodes/%s/migrate" % 1727 (GANETI_RAPI_VERSION, node)), query, None)
1728
1729 - def GetNodeRole(self, node, reason=None):
1730 """Gets the current role for a node. 1731 1732 @type node: str 1733 @param node: node whose role to return 1734 @type reason: string 1735 @param reason: the reason for executing this operation 1736 1737 @rtype: str 1738 @return: the current role for a node 1739 1740 """ 1741 query = [] 1742 _AppendReason(query, reason) 1743 1744 return self._SendRequest(HTTP_GET, 1745 ("/%s/nodes/%s/role" % 1746 (GANETI_RAPI_VERSION, node)), query, None)
1747
1748 - def SetNodeRole(self, node, role, force=False, auto_promote=None, 1749 reason=None):
1750 """Sets the role for a node. 1751 1752 @type node: str 1753 @param node: the node whose role to set 1754 @type role: str 1755 @param role: the role to set for the node 1756 @type force: bool 1757 @param force: whether to force the role change 1758 @type auto_promote: bool 1759 @param auto_promote: Whether node(s) should be promoted to master candidate 1760 if necessary 1761 @type reason: string 1762 @param reason: the reason for executing this operation 1763 1764 @rtype: string 1765 @return: job id 1766 1767 """ 1768 query = [] 1769 _AppendForceIf(query, force) 1770 _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote)) 1771 _AppendReason(query, reason) 1772 1773 return self._SendRequest(HTTP_PUT, 1774 ("/%s/nodes/%s/role" % 1775 (GANETI_RAPI_VERSION, node)), query, role)
1776
1777 - def PowercycleNode(self, node, force=False, reason=None):
1778 """Powercycles a node. 1779 1780 @type node: string 1781 @param node: Node name 1782 @type force: bool 1783 @param force: Whether to force the operation 1784 @type reason: string 1785 @param reason: the reason for executing this operation 1786 @rtype: string 1787 @return: job id 1788 1789 """ 1790 query = [] 1791 _AppendForceIf(query, force) 1792 _AppendReason(query, reason) 1793 1794 return self._SendRequest(HTTP_POST, 1795 ("/%s/nodes/%s/powercycle" % 1796 (GANETI_RAPI_VERSION, node)), query, None)
1797
1798 - def ModifyNode(self, node, reason=None, **kwargs):
1799 """Modifies a node. 1800 1801 More details for parameters can be found in the RAPI documentation. 1802 1803 @type node: string 1804 @param node: Node name 1805 @type reason: string 1806 @param reason: the reason for executing this operation 1807 @rtype: string 1808 @return: job id 1809 1810 """ 1811 query = [] 1812 _AppendReason(query, reason) 1813 1814 return self._SendRequest(HTTP_POST, 1815 ("/%s/nodes/%s/modify" % 1816 (GANETI_RAPI_VERSION, node)), query, kwargs)
1817
1818 - def GetNodeStorageUnits(self, node, storage_type, output_fields, reason=None):
1819 """Gets the storage units for a node. 1820 1821 @type node: str 1822 @param node: the node whose storage units to return 1823 @type storage_type: str 1824 @param storage_type: storage type whose units to return 1825 @type output_fields: str 1826 @param output_fields: storage type fields to return 1827 @type reason: string 1828 @param reason: the reason for executing this operation 1829 1830 @rtype: string 1831 @return: job id where results can be retrieved 1832 1833 """ 1834 query = [ 1835 ("storage_type", storage_type), 1836 ("output_fields", output_fields), 1837 ] 1838 _AppendReason(query, reason) 1839 1840 return self._SendRequest(HTTP_GET, 1841 ("/%s/nodes/%s/storage" % 1842 (GANETI_RAPI_VERSION, node)), query, None)
1843
1844 - def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None, 1845 reason=None):
1846 """Modifies parameters of storage units on the node. 1847 1848 @type node: str 1849 @param node: node whose storage units to modify 1850 @type storage_type: str 1851 @param storage_type: storage type whose units to modify 1852 @type name: str 1853 @param name: name of the storage unit 1854 @type allocatable: bool or None 1855 @param allocatable: Whether to set the "allocatable" flag on the storage 1856 unit (None=no modification, True=set, False=unset) 1857 @type reason: string 1858 @param reason: the reason for executing this operation 1859 1860 @rtype: string 1861 @return: job id 1862 1863 """ 1864 query = [ 1865 ("storage_type", storage_type), 1866 ("name", name), 1867 ] 1868 1869 _AppendIf(query, allocatable is not None, ("allocatable", allocatable)) 1870 _AppendReason(query, reason) 1871 1872 return self._SendRequest(HTTP_PUT, 1873 ("/%s/nodes/%s/storage/modify" % 1874 (GANETI_RAPI_VERSION, node)), query, None)
1875
1876 - def RepairNodeStorageUnits(self, node, storage_type, name, reason=None):
1877 """Repairs a storage unit on the node. 1878 1879 @type node: str 1880 @param node: node whose storage units to repair 1881 @type storage_type: str 1882 @param storage_type: storage type to repair 1883 @type name: str 1884 @param name: name of the storage unit to repair 1885 @type reason: string 1886 @param reason: the reason for executing this operation 1887 1888 @rtype: string 1889 @return: job id 1890 1891 """ 1892 query = [ 1893 ("storage_type", storage_type), 1894 ("name", name), 1895 ] 1896 _AppendReason(query, reason) 1897 1898 return self._SendRequest(HTTP_PUT, 1899 ("/%s/nodes/%s/storage/repair" % 1900 (GANETI_RAPI_VERSION, node)), query, None)
1901
1902 - def GetNodeTags(self, node, reason=None):
1903 """Gets the tags for a node. 1904 1905 @type node: str 1906 @param node: node whose tags to return 1907 @type reason: string 1908 @param reason: the reason for executing this operation 1909 1910 @rtype: list of str 1911 @return: tags for the node 1912 1913 """ 1914 query = [] 1915 _AppendReason(query, reason) 1916 1917 return self._SendRequest(HTTP_GET, 1918 ("/%s/nodes/%s/tags" % 1919 (GANETI_RAPI_VERSION, node)), query, None)
1920
1921 - def AddNodeTags(self, node, tags, dry_run=False, reason=None):
1922 """Adds tags to a node. 1923 1924 @type node: str 1925 @param node: node to add tags to 1926 @type tags: list of str 1927 @param tags: tags to add to the node 1928 @type dry_run: bool 1929 @param dry_run: whether to perform a dry run 1930 @type reason: string 1931 @param reason: the reason for executing this operation 1932 1933 @rtype: string 1934 @return: job id 1935 1936 """ 1937 query = [("tag", t) for t in tags] 1938 _AppendDryRunIf(query, dry_run) 1939 _AppendReason(query, reason) 1940 1941 return self._SendRequest(HTTP_PUT, 1942 ("/%s/nodes/%s/tags" % 1943 (GANETI_RAPI_VERSION, node)), query, tags)
1944
1945 - def DeleteNodeTags(self, node, tags, dry_run=False, reason=None):
1946 """Delete tags from a node. 1947 1948 @type node: str 1949 @param node: node to remove tags from 1950 @type tags: list of str 1951 @param tags: tags to remove from the node 1952 @type dry_run: bool 1953 @param dry_run: whether to perform a dry run 1954 @type reason: string 1955 @param reason: the reason for executing this operation 1956 1957 @rtype: string 1958 @return: job id 1959 1960 """ 1961 query = [("tag", t) for t in tags] 1962 _AppendDryRunIf(query, dry_run) 1963 _AppendReason(query, reason) 1964 1965 return self._SendRequest(HTTP_DELETE, 1966 ("/%s/nodes/%s/tags" % 1967 (GANETI_RAPI_VERSION, node)), query, None)
1968
1969 - def GetNetworks(self, bulk=False, reason=None):
1970 """Gets all networks in the cluster. 1971 1972 @type bulk: bool 1973 @param bulk: whether to return all information about the networks 1974 1975 @rtype: list of dict or str 1976 @return: if bulk is true, a list of dictionaries with info about all 1977 networks in the cluster, else a list of names of those networks 1978 1979 """ 1980 query = [] 1981 _AppendIf(query, bulk, ("bulk", 1)) 1982 _AppendReason(query, reason) 1983 1984 networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION, 1985 query, None) 1986 if bulk: 1987 return networks 1988 else: 1989 return [n["name"] for n in networks]
1990
1991 - def GetNetwork(self, network, reason=None):
1992 """Gets information about a network. 1993 1994 @type network: str 1995 @param network: name of the network whose info to return 1996 @type reason: string 1997 @param reason: the reason for executing this operation 1998 1999 @rtype: dict 2000 @return: info about the network 2001 2002 """ 2003 query = [] 2004 _AppendReason(query, reason) 2005 2006 return self._SendRequest(HTTP_GET, 2007 "/%s/networks/%s" % (GANETI_RAPI_VERSION, network), 2008 query, None)
2009
2010 - def CreateNetwork(self, network_name, network, gateway=None, network6=None, 2011 gateway6=None, mac_prefix=None, 2012 add_reserved_ips=None, tags=None, dry_run=False, 2013 reason=None):
2014 """Creates a new network. 2015 2016 @type network_name: str 2017 @param network_name: the name of network to create 2018 @type dry_run: bool 2019 @param dry_run: whether to peform a dry run 2020 @type reason: string 2021 @param reason: the reason for executing this operation 2022 2023 @rtype: string 2024 @return: job id 2025 2026 """ 2027 query = [] 2028 _AppendDryRunIf(query, dry_run) 2029 _AppendReason(query, reason) 2030 2031 if add_reserved_ips: 2032 add_reserved_ips = add_reserved_ips.split(",") 2033 2034 if tags: 2035 tags = tags.split(",") 2036 2037 body = { 2038 "network_name": network_name, 2039 "gateway": gateway, 2040 "network": network, 2041 "gateway6": gateway6, 2042 "network6": network6, 2043 "mac_prefix": mac_prefix, 2044 "add_reserved_ips": add_reserved_ips, 2045 "tags": tags, 2046 } 2047 2048 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION, 2049 query, body)
2050
2051 - def ConnectNetwork(self, network_name, group_name, mode, link, 2052 vlan="", dry_run=False, reason=None):
2053 """Connects a Network to a NodeGroup with the given netparams 2054 2055 """ 2056 body = { 2057 "group_name": group_name, 2058 "network_mode": mode, 2059 "network_link": link, 2060 "network_vlan": vlan, 2061 } 2062 2063 query = [] 2064 _AppendDryRunIf(query, dry_run) 2065 _AppendReason(query, reason) 2066 2067 return self._SendRequest(HTTP_PUT, 2068 ("/%s/networks/%s/connect" % 2069 (GANETI_RAPI_VERSION, network_name)), query, body)
2070
2071 - def DisconnectNetwork(self, network_name, group_name, dry_run=False, 2072 reason=None):
2073 """Connects a Network to a NodeGroup with the given netparams 2074 2075 """ 2076 body = { 2077 "group_name": group_name, 2078 } 2079 2080 query = [] 2081 _AppendDryRunIf(query, dry_run) 2082 _AppendReason(query, reason) 2083 2084 return self._SendRequest(HTTP_PUT, 2085 ("/%s/networks/%s/disconnect" % 2086 (GANETI_RAPI_VERSION, network_name)), query, body)
2087
2088 - def ModifyNetwork(self, network, reason=None, **kwargs):
2089 """Modifies a network. 2090 2091 More details for parameters can be found in the RAPI documentation. 2092 2093 @type network: string 2094 @param network: Network name 2095 @type reason: string 2096 @param reason: the reason for executing this operation 2097 @rtype: string 2098 @return: job id 2099 2100 """ 2101 query = [] 2102 _AppendReason(query, reason) 2103 2104 return self._SendRequest(HTTP_PUT, 2105 ("/%s/networks/%s/modify" % 2106 (GANETI_RAPI_VERSION, network)), None, kwargs)
2107
2108 - def DeleteNetwork(self, network, dry_run=False, reason=None):
2109 """Deletes a network. 2110 2111 @type network: str 2112 @param network: the network to delete 2113 @type dry_run: bool 2114 @param dry_run: whether to peform a dry run 2115 @type reason: string 2116 @param reason: the reason for executing this operation 2117 2118 @rtype: string 2119 @return: job id 2120 2121 """ 2122 query = [] 2123 _AppendDryRunIf(query, dry_run) 2124 _AppendReason(query, reason) 2125 2126 return self._SendRequest(HTTP_DELETE, 2127 ("/%s/networks/%s" % 2128 (GANETI_RAPI_VERSION, network)), query, None)
2129
2130 - def GetNetworkTags(self, network, reason=None):
2131 """Gets tags for a network. 2132 2133 @type network: string 2134 @param network: Node group whose tags to return 2135 @type reason: string 2136 @param reason: the reason for executing this operation 2137 2138 @rtype: list of strings 2139 @return: tags for the network 2140 2141 """ 2142 query = [] 2143 _AppendReason(query, reason) 2144 2145 return self._SendRequest(HTTP_GET, 2146 ("/%s/networks/%s/tags" % 2147 (GANETI_RAPI_VERSION, network)), query, None)
2148
2149 - def AddNetworkTags(self, network, tags, dry_run=False, reason=None):
2150 """Adds tags to a network. 2151 2152 @type network: str 2153 @param network: network to add tags to 2154 @type tags: list of string 2155 @param tags: tags to add to the network 2156 @type dry_run: bool 2157 @param dry_run: whether to perform a dry run 2158 @type reason: string 2159 @param reason: the reason for executing this operation 2160 2161 @rtype: string 2162 @return: job id 2163 2164 """ 2165 query = [("tag", t) for t in tags] 2166 _AppendDryRunIf(query, dry_run) 2167 _AppendReason(query, reason) 2168 2169 return self._SendRequest(HTTP_PUT, 2170 ("/%s/networks/%s/tags" % 2171 (GANETI_RAPI_VERSION, network)), query, None)
2172
2173 - def DeleteNetworkTags(self, network, tags, dry_run=False, reason=None):
2174 """Deletes tags from a network. 2175 2176 @type network: str 2177 @param network: network to delete tags from 2178 @type tags: list of string 2179 @param tags: tags to delete 2180 @type dry_run: bool 2181 @param dry_run: whether to perform a dry run 2182 @type reason: string 2183 @param reason: the reason for executing this operation 2184 @rtype: string 2185 @return: job id 2186 2187 """ 2188 query = [("tag", t) for t in tags] 2189 _AppendDryRunIf(query, dry_run) 2190 _AppendReason(query, reason) 2191 2192 return self._SendRequest(HTTP_DELETE, 2193 ("/%s/networks/%s/tags" % 2194 (GANETI_RAPI_VERSION, network)), query, None)
2195
2196 - def GetGroups(self, bulk=False, reason=None):
2197 """Gets all node groups in the cluster. 2198 2199 @type bulk: bool 2200 @param bulk: whether to return all information about the groups 2201 @type reason: string 2202 @param reason: the reason for executing this operation 2203 2204 @rtype: list of dict or str 2205 @return: if bulk is true, a list of dictionaries with info about all node 2206 groups in the cluster, else a list of names of those node groups 2207 2208 """ 2209 query = [] 2210 _AppendIf(query, bulk, ("bulk", 1)) 2211 _AppendReason(query, reason) 2212 2213 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION, 2214 query, None) 2215 if bulk: 2216 return groups 2217 else: 2218 return [g["name"] for g in groups]
2219
2220 - def GetGroup(self, group, reason=None):
2221 """Gets information about a node group. 2222 2223 @type group: str 2224 @param group: name of the node group whose info to return 2225 @type reason: string 2226 @param reason: the reason for executing this operation 2227 2228 @rtype: dict 2229 @return: info about the node group 2230 2231 """ 2232 query = [] 2233 _AppendReason(query, reason) 2234 2235 return self._SendRequest(HTTP_GET, 2236 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group), 2237 query, None)
2238
2239 - def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2240 """Creates a new node group. 2241 2242 @type name: str 2243 @param name: the name of node group to create 2244 @type alloc_policy: str 2245 @param alloc_policy: the desired allocation policy for the group, if any 2246 @type dry_run: bool 2247 @param dry_run: whether to peform a dry run 2248 @type reason: string 2249 @param reason: the reason for executing this operation 2250 2251 @rtype: string 2252 @return: job id 2253 2254 """ 2255 query = [] 2256 _AppendDryRunIf(query, dry_run) 2257 _AppendReason(query, reason) 2258 2259 body = { 2260 "name": name, 2261 "alloc_policy": alloc_policy, 2262 } 2263 2264 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION, 2265 query, body)
2266
2267 - def ModifyGroup(self, group, reason=None, **kwargs):
2268 """Modifies a node group. 2269 2270 More details for parameters can be found in the RAPI documentation. 2271 2272 @type group: string 2273 @param group: Node group name 2274 @type reason: string 2275 @param reason: the reason for executing this operation 2276 @rtype: string 2277 @return: job id 2278 2279 """ 2280 query = [] 2281 _AppendReason(query, reason) 2282 2283 return self._SendRequest(HTTP_PUT, 2284 ("/%s/groups/%s/modify" % 2285 (GANETI_RAPI_VERSION, group)), query, kwargs)
2286
2287 - def DeleteGroup(self, group, dry_run=False, reason=None):
2288 """Deletes a node group. 2289 2290 @type group: str 2291 @param group: the node group to delete 2292 @type dry_run: bool 2293 @param dry_run: whether to peform a dry run 2294 @type reason: string 2295 @param reason: the reason for executing this operation 2296 2297 @rtype: string 2298 @return: job id 2299 2300 """ 2301 query = [] 2302 _AppendDryRunIf(query, dry_run) 2303 _AppendReason(query, reason) 2304 2305 return self._SendRequest(HTTP_DELETE, 2306 ("/%s/groups/%s" % 2307 (GANETI_RAPI_VERSION, group)), query, None)
2308
2309 - def RenameGroup(self, group, new_name, reason=None):
2310 """Changes the name of a node group. 2311 2312 @type group: string 2313 @param group: Node group name 2314 @type new_name: string 2315 @param new_name: New node group name 2316 @type reason: string 2317 @param reason: the reason for executing this operation 2318 2319 @rtype: string 2320 @return: job id 2321 2322 """ 2323 body = { 2324 "new_name": new_name, 2325 } 2326 2327 query = [] 2328 _AppendReason(query, reason) 2329 2330 return self._SendRequest(HTTP_PUT, 2331 ("/%s/groups/%s/rename" % 2332 (GANETI_RAPI_VERSION, group)), query, body)
2333
2334 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False, 2335 reason=None):
2336 """Assigns nodes to a group. 2337 2338 @type group: string 2339 @param group: Node group name 2340 @type nodes: list of strings 2341 @param nodes: List of nodes to assign to the group 2342 @type reason: string 2343 @param reason: the reason for executing this operation 2344 2345 @rtype: string 2346 @return: job id 2347 2348 """ 2349 query = [] 2350 _AppendForceIf(query, force) 2351 _AppendDryRunIf(query, dry_run) 2352 _AppendReason(query, reason) 2353 2354 body = { 2355 "nodes": nodes, 2356 } 2357 2358 return self._SendRequest(HTTP_PUT, 2359 ("/%s/groups/%s/assign-nodes" % 2360 (GANETI_RAPI_VERSION, group)), query, body)
2361
2362 - def GetGroupTags(self, group, reason=None):
2363 """Gets tags for a node group. 2364 2365 @type group: string 2366 @param group: Node group whose tags to return 2367 @type reason: string 2368 @param reason: the reason for executing this operation 2369 2370 @rtype: list of strings 2371 @return: tags for the group 2372 2373 """ 2374 query = [] 2375 _AppendReason(query, reason) 2376 2377 return self._SendRequest(HTTP_GET, 2378 ("/%s/groups/%s/tags" % 2379 (GANETI_RAPI_VERSION, group)), query, None)
2380
2381 - def AddGroupTags(self, group, tags, dry_run=False, reason=None):
2382 """Adds tags to a node group. 2383 2384 @type group: str 2385 @param group: group to add tags to 2386 @type tags: list of string 2387 @param tags: tags to add to the group 2388 @type dry_run: bool 2389 @param dry_run: whether to perform a dry run 2390 @type reason: string 2391 @param reason: the reason for executing this operation 2392 2393 @rtype: string 2394 @return: job id 2395 2396 """ 2397 query = [("tag", t) for t in tags] 2398 _AppendDryRunIf(query, dry_run) 2399 _AppendReason(query, reason) 2400 2401 return self._SendRequest(HTTP_PUT, 2402 ("/%s/groups/%s/tags" % 2403 (GANETI_RAPI_VERSION, group)), query, None)
2404
2405 - def DeleteGroupTags(self, group, tags, dry_run=False, reason=None):
2406 """Deletes tags from a node group. 2407 2408 @type group: str 2409 @param group: group to delete tags from 2410 @type tags: list of string 2411 @param tags: tags to delete 2412 @type dry_run: bool 2413 @param dry_run: whether to perform a dry run 2414 @type reason: string 2415 @param reason: the reason for executing this operation 2416 @rtype: string 2417 @return: job id 2418 2419 """ 2420 query = [("tag", t) for t in tags] 2421 _AppendDryRunIf(query, dry_run) 2422 _AppendReason(query, reason) 2423 2424 return self._SendRequest(HTTP_DELETE, 2425 ("/%s/groups/%s/tags" % 2426 (GANETI_RAPI_VERSION, group)), query, None)
2427
2428 - def Query(self, what, fields, qfilter=None, reason=None):
2429 """Retrieves information about resources. 2430 2431 @type what: string 2432 @param what: Resource name, one of L{constants.QR_VIA_RAPI} 2433 @type fields: list of string 2434 @param fields: Requested fields 2435 @type qfilter: None or list 2436 @param qfilter: Query filter 2437 @type reason: string 2438 @param reason: the reason for executing this operation 2439 2440 @rtype: string 2441 @return: job id 2442 2443 """ 2444 query = [] 2445 _AppendReason(query, reason) 2446 2447 body = { 2448 "fields": fields, 2449 } 2450 2451 _SetItemIf(body, qfilter is not None, "qfilter", qfilter) 2452 # TODO: remove "filter" after 2.7 2453 _SetItemIf(body, qfilter is not None, "filter", qfilter) 2454 2455 return self._SendRequest(HTTP_PUT, 2456 ("/%s/query/%s" % 2457 (GANETI_RAPI_VERSION, what)), query, body)
2458
2459 - def QueryFields(self, what, fields=None, reason=None):
2460 """Retrieves available fields for a resource. 2461 2462 @type what: string 2463 @param what: Resource name, one of L{constants.QR_VIA_RAPI} 2464 @type fields: list of string 2465 @param fields: Requested fields 2466 @type reason: string 2467 @param reason: the reason for executing this operation 2468 2469 @rtype: string 2470 @return: job id 2471 2472 """ 2473 query = [] 2474 _AppendReason(query, reason) 2475 2476 if fields is not None: 2477 _AppendIf(query, True, ("fields", ",".join(fields))) 2478 2479 return self._SendRequest(HTTP_GET, 2480 ("/%s/query/%s/fields" % 2481 (GANETI_RAPI_VERSION, what)), query, None)
2482
2483 - def GetFilters(self, bulk=False):
2484 """Gets all filter rules in the cluster. 2485 2486 @type bulk: bool 2487 @param bulk: whether to return all information about the networks 2488 2489 @rtype: list of dict or str 2490 @return: if bulk is true, a list of dictionaries with info about all 2491 filter rules in the cluster, else a list of UUIDs of those 2492 filters 2493 2494 """ 2495 query = [] 2496 _AppendIf(query, bulk, ("bulk", 1)) 2497 2498 filters = self._SendRequest(HTTP_GET, "/%s/filters" % GANETI_RAPI_VERSION, 2499 query, None) 2500 if bulk: 2501 return filters 2502 else: 2503 return [f["uuid"] for f in filters]
2504
2505 - def GetFilter(self, filter_uuid):
2506 """Gets information about a filter rule. 2507 2508 @type filter_uuid: str 2509 @param filter_uuid: UUID of the filter whose info to return 2510 2511 @rtype: dict 2512 @return: info about the filter 2513 2514 """ 2515 query = [] 2516 2517 return self._SendRequest(HTTP_GET, 2518 "/%s/filters/%s" % (GANETI_RAPI_VERSION, 2519 filter_uuid), 2520 query, None)
2521
2522 - def AddFilter(self, priority, predicates, action, reason_trail=None):
2523 """Adds a filter rule 2524 2525 @type reason_trail: list of (str, str, int) triples 2526 @param reason_trail: the reason trail for executing this operation, 2527 or None 2528 2529 @rtype: string 2530 @return: filter UUID of the added filter 2531 2532 """ 2533 if reason_trail is None: 2534 reason_trail = [] 2535 2536 assert isinstance(reason_trail, list) 2537 2538 reason_trail.append(("gnt:client", "", EpochNano(),)) # add client reason 2539 2540 body = { 2541 "priority": priority, 2542 "predicates": predicates, 2543 "action": action, 2544 "reason": reason_trail, 2545 } 2546 2547 query = [] 2548 2549 return self._SendRequest(HTTP_POST, 2550 ("/%s/filters" % (GANETI_RAPI_VERSION)), 2551 query, body)
2552
2553 - def ReplaceFilter(self, uuid, priority, predicates, action, 2554 reason_trail=None):
2555 """Replaces a filter rule, or creates one if it doesn't already exist 2556 2557 @type reason_trail: list of (str, str, int) triples 2558 @param reason_trail: the reason trail for executing this operation, 2559 or None 2560 2561 @rtype: string 2562 @return: filter UUID of the replaced/added filter 2563 2564 """ 2565 if reason_trail is None: 2566 reason_trail = [] 2567 2568 assert isinstance(reason_trail, list) 2569 2570 reason_trail.append(("gnt:client", "", EpochNano(),)) # add client reason 2571 2572 body = { 2573 "priority": priority, 2574 "predicates": predicates, 2575 "action": action, 2576 "reason": reason_trail, 2577 } 2578 2579 query = [] 2580 2581 return self._SendRequest(HTTP_PUT, 2582 ("/%s/filters/%s" % (GANETI_RAPI_VERSION, uuid)), 2583 query, body)
2584
2585 - def DeleteFilter(self, uuid):
2586 """Deletes a filter rule 2587 2588 @return: None 2589 2590 """ 2591 body = {} 2592 query = [] 2593 2594 return self._SendRequest(HTTP_DELETE, 2595 ("/%s/filters/%s" % (GANETI_RAPI_VERSION, uuid)), 2596 query, body)
2597