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