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