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