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