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