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 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   
  43  try: 
  44    from cStringIO import StringIO 
  45  except ImportError: 
  46    from StringIO import StringIO 
  47   
  48   
  49  GANETI_RAPI_PORT = 5080 
  50  GANETI_RAPI_VERSION = 2 
  51   
  52  HTTP_DELETE = "DELETE" 
  53  HTTP_GET = "GET" 
  54  HTTP_PUT = "PUT" 
  55  HTTP_POST = "POST" 
  56  HTTP_OK = 200 
  57  HTTP_NOT_FOUND = 404 
  58  HTTP_APP_JSON = "application/json" 
  59   
  60  REPLACE_DISK_PRI = "replace_on_primary" 
  61  REPLACE_DISK_SECONDARY = "replace_on_secondary" 
  62  REPLACE_DISK_CHG = "replace_new_secondary" 
  63  REPLACE_DISK_AUTO = "replace_auto" 
  64   
  65  NODE_ROLE_DRAINED = "drained" 
  66  NODE_ROLE_MASTER_CANDIATE = "master-candidate" 
  67  NODE_ROLE_MASTER = "master" 
  68  NODE_ROLE_OFFLINE = "offline" 
  69  NODE_ROLE_REGULAR = "regular" 
  70   
  71  # Internal constants 
  72  _REQ_DATA_VERSION_FIELD = "__version__" 
  73  _INST_CREATE_REQV1 = "instance-create-reqv1" 
  74  _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" 
  75  _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"]) 
  76  _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"]) 
  77  _INST_CREATE_V0_PARAMS = frozenset([ 
  78    "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check", 
  79    "hypervisor", "file_storage_dir", "file_driver", "dry_run", 
  80    ]) 
  81  _INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"]) 
  82   
  83  # Older pycURL versions don't have all error constants 
  84  try: 
  85    _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT 
  86    _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE 
  87  except AttributeError: 
  88    _CURLE_SSL_CACERT = 60 
  89    _CURLE_SSL_CACERT_BADFILE = 77 
  90   
  91  _CURL_SSL_CERT_ERRORS = frozenset([ 
  92    _CURLE_SSL_CACERT, 
  93    _CURLE_SSL_CACERT_BADFILE, 
  94    ]) 
95 96 97 -class Error(Exception):
98 """Base error class for this module. 99 100 """ 101 pass
102
103 104 -class CertificateError(Error):
105 """Raised when a problem is found with the SSL certificate. 106 107 """ 108 pass
109
110 111 -class GanetiApiError(Error):
112 """Generic error raised from Ganeti API. 113 114 """
115 - def __init__(self, msg, code=None):
116 Error.__init__(self, msg) 117 self.code = code
118
119 120 -def UsesRapiClient(fn):
121 """Decorator for code using RAPI client to initialize pycURL. 122 123 """ 124 def wrapper(*args, **kwargs): 125 # curl_global_init(3) and curl_global_cleanup(3) must be called with only 126 # one thread running. This check is just a safety measure -- it doesn't 127 # cover all cases. 128 assert threading.activeCount() == 1, \ 129 "Found active threads when initializing pycURL" 130 131 pycurl.global_init(pycurl.GLOBAL_ALL) 132 try: 133 return fn(*args, **kwargs) 134 finally: 135 pycurl.global_cleanup()
136 137 return wrapper 138
139 140 -def GenericCurlConfig(verbose=False, use_signal=False, 141 use_curl_cabundle=False, cafile=None, capath=None, 142 proxy=None, verify_hostname=False, 143 connect_timeout=None, timeout=None, 144 _pycurl_version_fn=pycurl.version_info):
145 """Curl configuration function generator. 146 147 @type verbose: bool 148 @param verbose: Whether to set cURL to verbose mode 149 @type use_signal: bool 150 @param use_signal: Whether to allow cURL to use signals 151 @type use_curl_cabundle: bool 152 @param use_curl_cabundle: Whether to use cURL's default CA bundle 153 @type cafile: string 154 @param cafile: In which file we can find the certificates 155 @type capath: string 156 @param capath: In which directory we can find the certificates 157 @type proxy: string 158 @param proxy: Proxy to use, None for default behaviour and empty string for 159 disabling proxies (see curl_easy_setopt(3)) 160 @type verify_hostname: bool 161 @param verify_hostname: Whether to verify the remote peer certificate's 162 commonName 163 @type connect_timeout: number 164 @param connect_timeout: Timeout for establishing connection in seconds 165 @type timeout: number 166 @param timeout: Timeout for complete transfer in seconds (see 167 curl_easy_setopt(3)). 168 169 """ 170 if use_curl_cabundle and (cafile or capath): 171 raise Error("Can not use default CA bundle when CA file or path is set") 172 173 def _ConfigCurl(curl, logger): 174 """Configures a cURL object 175 176 @type curl: pycurl.Curl 177 @param curl: cURL object 178 179 """ 180 logger.debug("Using cURL version %s", pycurl.version) 181 182 # pycurl.version_info returns a tuple with information about the used 183 # version of libcurl. Item 5 is the SSL library linked to it. 184 # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4', 185 # 0, '1.2.3.3', ...) 186 sslver = _pycurl_version_fn()[5] 187 if not sslver: 188 raise Error("No SSL support in cURL") 189 190 lcsslver = sslver.lower() 191 if lcsslver.startswith("openssl/"): 192 pass 193 elif lcsslver.startswith("gnutls/"): 194 if capath: 195 raise Error("cURL linked against GnuTLS has no support for a" 196 " CA path (%s)" % (pycurl.version, )) 197 else: 198 raise NotImplementedError("cURL uses unsupported SSL version '%s'" % 199 sslver) 200 201 curl.setopt(pycurl.VERBOSE, verbose) 202 curl.setopt(pycurl.NOSIGNAL, not use_signal) 203 204 # Whether to verify remote peer's CN 205 if verify_hostname: 206 # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that 207 # certificate must indicate that the server is the server to which you 208 # meant to connect, or the connection fails. [...] When the value is 1, 209 # the certificate must contain a Common Name field, but it doesn't matter 210 # what name it says. [...]" 211 curl.setopt(pycurl.SSL_VERIFYHOST, 2) 212 else: 213 curl.setopt(pycurl.SSL_VERIFYHOST, 0) 214 215 if cafile or capath or use_curl_cabundle: 216 # Require certificates to be checked 217 curl.setopt(pycurl.SSL_VERIFYPEER, True) 218 if cafile: 219 curl.setopt(pycurl.CAINFO, str(cafile)) 220 if capath: 221 curl.setopt(pycurl.CAPATH, str(capath)) 222 # Not changing anything for using default CA bundle 223 else: 224 # Disable SSL certificate verification 225 curl.setopt(pycurl.SSL_VERIFYPEER, False) 226 227 if proxy is not None: 228 curl.setopt(pycurl.PROXY, str(proxy)) 229 230 # Timeouts 231 if connect_timeout is not None: 232 curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout) 233 if timeout is not None: 234 curl.setopt(pycurl.TIMEOUT, timeout)
235 236 return _ConfigCurl 237
238 239 -class GanetiRapiClient(object): # pylint: disable-msg=R0904
240 """Ganeti RAPI client. 241 242 """ 243 USER_AGENT = "Ganeti RAPI Client" 244 _json_encoder = simplejson.JSONEncoder(sort_keys=True) 245
246 - def __init__(self, host, port=GANETI_RAPI_PORT, 247 username=None, password=None, logger=logging, 248 curl_config_fn=None, curl_factory=None):
249 """Initializes this class. 250 251 @type host: string 252 @param host: the ganeti cluster master to interact with 253 @type port: int 254 @param port: the port on which the RAPI is running (default is 5080) 255 @type username: string 256 @param username: the username to connect with 257 @type password: string 258 @param password: the password to connect with 259 @type curl_config_fn: callable 260 @param curl_config_fn: Function to configure C{pycurl.Curl} object 261 @param logger: Logging object 262 263 """ 264 self._username = username 265 self._password = password 266 self._logger = logger 267 self._curl_config_fn = curl_config_fn 268 self._curl_factory = curl_factory 269 270 try: 271 socket.inet_pton(socket.AF_INET6, host) 272 address = "[%s]:%s" % (host, port) 273 except socket.error: 274 address = "%s:%s" % (host, port) 275 276 self._base_url = "https://%s" % address 277 278 if username is not None: 279 if password is None: 280 raise Error("Password not specified") 281 elif password: 282 raise Error("Specified password without username")
283
284 - def _CreateCurl(self):
285 """Creates a cURL object. 286 287 """ 288 # Create pycURL object if no factory is provided 289 if self._curl_factory: 290 curl = self._curl_factory() 291 else: 292 curl = pycurl.Curl() 293 294 # Default cURL settings 295 curl.setopt(pycurl.VERBOSE, False) 296 curl.setopt(pycurl.FOLLOWLOCATION, False) 297 curl.setopt(pycurl.MAXREDIRS, 5) 298 curl.setopt(pycurl.NOSIGNAL, True) 299 curl.setopt(pycurl.USERAGENT, self.USER_AGENT) 300 curl.setopt(pycurl.SSL_VERIFYHOST, 0) 301 curl.setopt(pycurl.SSL_VERIFYPEER, False) 302 curl.setopt(pycurl.HTTPHEADER, [ 303 "Accept: %s" % HTTP_APP_JSON, 304 "Content-type: %s" % HTTP_APP_JSON, 305 ]) 306 307 assert ((self._username is None and self._password is None) ^ 308 (self._username is not None and self._password is not None)) 309 310 if self._username: 311 # Setup authentication 312 curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) 313 curl.setopt(pycurl.USERPWD, 314 str("%s:%s" % (self._username, self._password))) 315 316 # Call external configuration function 317 if self._curl_config_fn: 318 self._curl_config_fn(curl, self._logger) 319 320 return curl
321 322 @staticmethod
323 - def _EncodeQuery(query):
324 """Encode query values for RAPI URL. 325 326 @type query: list of two-tuples 327 @param query: Query arguments 328 @rtype: list 329 @return: Query list with encoded values 330 331 """ 332 result = [] 333 334 for name, value in query: 335 if value is None: 336 result.append((name, "")) 337 338 elif isinstance(value, bool): 339 # Boolean values must be encoded as 0 or 1 340 result.append((name, int(value))) 341 342 elif isinstance(value, (list, tuple, dict)): 343 raise ValueError("Invalid query data type %r" % type(value).__name__) 344 345 else: 346 result.append((name, value)) 347 348 return result
349
350 - def _SendRequest(self, method, path, query, content):
351 """Sends an HTTP request. 352 353 This constructs a full URL, encodes and decodes HTTP bodies, and 354 handles invalid responses in a pythonic way. 355 356 @type method: string 357 @param method: HTTP method to use 358 @type path: string 359 @param path: HTTP URL path 360 @type query: list of two-tuples 361 @param query: query arguments to pass to urllib.urlencode 362 @type content: str or None 363 @param content: HTTP body content 364 365 @rtype: str 366 @return: JSON-Decoded response 367 368 @raises CertificateError: If an invalid SSL certificate is found 369 @raises GanetiApiError: If an invalid response is returned 370 371 """ 372 assert path.startswith("/") 373 374 curl = self._CreateCurl() 375 376 if content is not None: 377 encoded_content = self._json_encoder.encode(content) 378 else: 379 encoded_content = "" 380 381 # Build URL 382 urlparts = [self._base_url, path] 383 if query: 384 urlparts.append("?") 385 urlparts.append(urllib.urlencode(self._EncodeQuery(query))) 386 387 url = "".join(urlparts) 388 389 self._logger.debug("Sending request %s %s (content=%r)", 390 method, url, encoded_content) 391 392 # Buffer for response 393 encoded_resp_body = StringIO() 394 395 # Configure cURL 396 curl.setopt(pycurl.CUSTOMREQUEST, str(method)) 397 curl.setopt(pycurl.URL, str(url)) 398 curl.setopt(pycurl.POSTFIELDS, str(encoded_content)) 399 curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write) 400 401 try: 402 # Send request and wait for response 403 try: 404 curl.perform() 405 except pycurl.error, err: 406 if err.args[0] in _CURL_SSL_CERT_ERRORS: 407 raise CertificateError("SSL certificate error %s" % err) 408 409 raise GanetiApiError(str(err)) 410 finally: 411 # Reset settings to not keep references to large objects in memory 412 # between requests 413 curl.setopt(pycurl.POSTFIELDS, "") 414 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None) 415 416 # Get HTTP response code 417 http_code = curl.getinfo(pycurl.RESPONSE_CODE) 418 419 # Was anything written to the response buffer? 420 if encoded_resp_body.tell(): 421 response_content = simplejson.loads(encoded_resp_body.getvalue()) 422 else: 423 response_content = None 424 425 if http_code != HTTP_OK: 426 if isinstance(response_content, dict): 427 msg = ("%s %s: %s" % 428 (response_content["code"], 429 response_content["message"], 430 response_content["explain"])) 431 else: 432 msg = str(response_content) 433 434 raise GanetiApiError(msg, code=http_code) 435 436 return response_content
437
438 - def GetVersion(self):
439 """Gets the Remote API version running on the cluster. 440 441 @rtype: int 442 @return: Ganeti Remote API version 443 444 """ 445 return self._SendRequest(HTTP_GET, "/version", None, None)
446
447 - def GetFeatures(self):
448 """Gets the list of optional features supported by RAPI server. 449 450 @rtype: list 451 @return: List of optional features 452 453 """ 454 try: 455 return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION, 456 None, None) 457 except GanetiApiError, err: 458 # Older RAPI servers don't support this resource 459 if err.code == HTTP_NOT_FOUND: 460 return [] 461 462 raise
463
464 - def GetOperatingSystems(self):
465 """Gets the Operating Systems running in the Ganeti cluster. 466 467 @rtype: list of str 468 @return: operating systems 469 470 """ 471 return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION, 472 None, None)
473
474 - def GetInfo(self):
475 """Gets info about the cluster. 476 477 @rtype: dict 478 @return: information about the cluster 479 480 """ 481 return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION, 482 None, None)
483
484 - def RedistributeConfig(self):
485 """Tells the cluster to redistribute its configuration files. 486 487 @return: job id 488 489 """ 490 return self._SendRequest(HTTP_PUT, 491 "/%s/redistribute-config" % GANETI_RAPI_VERSION, 492 None, None)
493
494 - def ModifyCluster(self, **kwargs):
495 """Modifies cluster parameters. 496 497 More details for parameters can be found in the RAPI documentation. 498 499 @rtype: int 500 @return: job id 501 502 """ 503 body = kwargs 504 505 return self._SendRequest(HTTP_PUT, 506 "/%s/modify" % GANETI_RAPI_VERSION, None, body)
507
508 - def GetClusterTags(self):
509 """Gets the cluster tags. 510 511 @rtype: list of str 512 @return: cluster tags 513 514 """ 515 return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION, 516 None, None)
517
518 - def AddClusterTags(self, tags, dry_run=False):
519 """Adds tags to the cluster. 520 521 @type tags: list of str 522 @param tags: tags to add to the cluster 523 @type dry_run: bool 524 @param dry_run: whether to perform a dry run 525 526 @rtype: int 527 @return: job id 528 529 """ 530 query = [("tag", t) for t in tags] 531 if dry_run: 532 query.append(("dry-run", 1)) 533 534 return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION, 535 query, None)
536
537 - def DeleteClusterTags(self, tags, dry_run=False):
538 """Deletes tags from the cluster. 539 540 @type tags: list of str 541 @param tags: tags to delete 542 @type dry_run: bool 543 @param dry_run: whether to perform a dry run 544 545 """ 546 query = [("tag", t) for t in tags] 547 if dry_run: 548 query.append(("dry-run", 1)) 549 550 return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION, 551 query, None)
552
553 - def GetInstances(self, bulk=False):
554 """Gets information about instances on the cluster. 555 556 @type bulk: bool 557 @param bulk: whether to return all information about all instances 558 559 @rtype: list of dict or list of str 560 @return: if bulk is True, info about the instances, else a list of instances 561 562 """ 563 query = [] 564 if bulk: 565 query.append(("bulk", 1)) 566 567 instances = self._SendRequest(HTTP_GET, 568 "/%s/instances" % GANETI_RAPI_VERSION, 569 query, None) 570 if bulk: 571 return instances 572 else: 573 return [i["id"] for i in instances]
574
575 - def GetInstance(self, instance):
576 """Gets information about an instance. 577 578 @type instance: str 579 @param instance: instance whose info to return 580 581 @rtype: dict 582 @return: info about the instance 583 584 """ 585 return self._SendRequest(HTTP_GET, 586 ("/%s/instances/%s" % 587 (GANETI_RAPI_VERSION, instance)), None, None)
588
589 - def GetInstanceInfo(self, instance, static=None):
590 """Gets information about an instance. 591 592 @type instance: string 593 @param instance: Instance name 594 @rtype: string 595 @return: Job ID 596 597 """ 598 if static is not None: 599 query = [("static", static)] 600 else: 601 query = None 602 603 return self._SendRequest(HTTP_GET, 604 ("/%s/instances/%s/info" % 605 (GANETI_RAPI_VERSION, instance)), query, None)
606
607 - def CreateInstance(self, mode, name, disk_template, disks, nics, 608 **kwargs):
609 """Creates a new instance. 610 611 More details for parameters can be found in the RAPI documentation. 612 613 @type mode: string 614 @param mode: Instance creation mode 615 @type name: string 616 @param name: Hostname of the instance to create 617 @type disk_template: string 618 @param disk_template: Disk template for instance (e.g. plain, diskless, 619 file, or drbd) 620 @type disks: list of dicts 621 @param disks: List of disk definitions 622 @type nics: list of dicts 623 @param nics: List of NIC definitions 624 @type dry_run: bool 625 @keyword dry_run: whether to perform a dry run 626 627 @rtype: int 628 @return: job id 629 630 """ 631 query = [] 632 633 if kwargs.get("dry_run"): 634 query.append(("dry-run", 1)) 635 636 if _INST_CREATE_REQV1 in self.GetFeatures(): 637 # All required fields for request data version 1 638 body = { 639 _REQ_DATA_VERSION_FIELD: 1, 640 "mode": mode, 641 "name": name, 642 "disk_template": disk_template, 643 "disks": disks, 644 "nics": nics, 645 } 646 647 conflicts = set(kwargs.iterkeys()) & set(body.iterkeys()) 648 if conflicts: 649 raise GanetiApiError("Required fields can not be specified as" 650 " keywords: %s" % ", ".join(conflicts)) 651 652 body.update((key, value) for key, value in kwargs.iteritems() 653 if key != "dry_run") 654 else: 655 # Old request format (version 0) 656 657 # The following code must make sure that an exception is raised when an 658 # unsupported setting is requested by the caller. Otherwise this can lead 659 # to bugs difficult to find. The interface of this function must stay 660 # exactly the same for version 0 and 1 (e.g. they aren't allowed to 661 # require different data types). 662 663 # Validate disks 664 for idx, disk in enumerate(disks): 665 unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS 666 if unsupported: 667 raise GanetiApiError("Server supports request version 0 only, but" 668 " disk %s specifies the unsupported parameters" 669 " %s, allowed are %s" % 670 (idx, unsupported, 671 list(_INST_CREATE_V0_DISK_PARAMS))) 672 673 assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and 674 "size" in _INST_CREATE_V0_DISK_PARAMS) 675 disk_sizes = [disk["size"] for disk in disks] 676 677 # Validate NICs 678 if not nics: 679 raise GanetiApiError("Server supports request version 0 only, but" 680 " no NIC specified") 681 elif len(nics) > 1: 682 raise GanetiApiError("Server supports request version 0 only, but" 683 " more than one NIC specified") 684 685 assert len(nics) == 1 686 687 unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS 688 if unsupported: 689 raise GanetiApiError("Server supports request version 0 only, but" 690 " NIC 0 specifies the unsupported parameters %s," 691 " allowed are %s" % 692 (unsupported, list(_INST_NIC_PARAMS))) 693 694 # Validate other parameters 695 unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS - 696 _INST_CREATE_V0_DPARAMS) 697 if unsupported: 698 allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS) 699 raise GanetiApiError("Server supports request version 0 only, but" 700 " the following unsupported parameters are" 701 " specified: %s, allowed are %s" % 702 (unsupported, list(allowed))) 703 704 # All required fields for request data version 0 705 body = { 706 _REQ_DATA_VERSION_FIELD: 0, 707 "name": name, 708 "disk_template": disk_template, 709 "disks": disk_sizes, 710 } 711 712 # NIC fields 713 assert len(nics) == 1 714 assert not (set(body.keys()) & set(nics[0].keys())) 715 body.update(nics[0]) 716 717 # Copy supported fields 718 assert not (set(body.keys()) & set(kwargs.keys())) 719 body.update(dict((key, value) for key, value in kwargs.items() 720 if key in _INST_CREATE_V0_PARAMS)) 721 722 # Merge dictionaries 723 for i in (value for key, value in kwargs.items() 724 if key in _INST_CREATE_V0_DPARAMS): 725 assert not (set(body.keys()) & set(i.keys())) 726 body.update(i) 727 728 assert not (set(kwargs.keys()) - 729 (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS)) 730 assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS) 731 732 return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION, 733 query, body)
734
735 - def DeleteInstance(self, instance, dry_run=False):
736 """Deletes an instance. 737 738 @type instance: str 739 @param instance: the instance to delete 740 741 @rtype: int 742 @return: job id 743 744 """ 745 query = [] 746 if dry_run: 747 query.append(("dry-run", 1)) 748 749 return self._SendRequest(HTTP_DELETE, 750 ("/%s/instances/%s" % 751 (GANETI_RAPI_VERSION, instance)), query, None)
752
753 - def ModifyInstance(self, instance, **kwargs):
754 """Modifies an instance. 755 756 More details for parameters can be found in the RAPI documentation. 757 758 @type instance: string 759 @param instance: Instance name 760 @rtype: int 761 @return: job id 762 763 """ 764 body = kwargs 765 766 return self._SendRequest(HTTP_PUT, 767 ("/%s/instances/%s/modify" % 768 (GANETI_RAPI_VERSION, instance)), None, body)
769
770 - def ActivateInstanceDisks(self, instance, ignore_size=None):
771 """Activates an instance's disks. 772 773 @type instance: string 774 @param instance: Instance name 775 @type ignore_size: bool 776 @param ignore_size: Whether to ignore recorded size 777 @return: job id 778 779 """ 780 query = [] 781 if ignore_size: 782 query.append(("ignore_size", 1)) 783 784 return self._SendRequest(HTTP_PUT, 785 ("/%s/instances/%s/activate-disks" % 786 (GANETI_RAPI_VERSION, instance)), query, None)
787
788 - def DeactivateInstanceDisks(self, instance):
789 """Deactivates an instance's disks. 790 791 @type instance: string 792 @param instance: Instance name 793 @return: job id 794 795 """ 796 return self._SendRequest(HTTP_PUT, 797 ("/%s/instances/%s/deactivate-disks" % 798 (GANETI_RAPI_VERSION, instance)), None, None)
799
800 - def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
801 """Grows a disk of an instance. 802 803 More details for parameters can be found in the RAPI documentation. 804 805 @type instance: string 806 @param instance: Instance name 807 @type disk: integer 808 @param disk: Disk index 809 @type amount: integer 810 @param amount: Grow disk by this amount (MiB) 811 @type wait_for_sync: bool 812 @param wait_for_sync: Wait for disk to synchronize 813 @rtype: int 814 @return: job id 815 816 """ 817 body = { 818 "amount": amount, 819 } 820 821 if wait_for_sync is not None: 822 body["wait_for_sync"] = wait_for_sync 823 824 return self._SendRequest(HTTP_POST, 825 ("/%s/instances/%s/disk/%s/grow" % 826 (GANETI_RAPI_VERSION, instance, disk)), 827 None, body)
828
829 - def GetInstanceTags(self, instance):
830 """Gets tags for an instance. 831 832 @type instance: str 833 @param instance: instance whose tags to return 834 835 @rtype: list of str 836 @return: tags for the instance 837 838 """ 839 return self._SendRequest(HTTP_GET, 840 ("/%s/instances/%s/tags" % 841 (GANETI_RAPI_VERSION, instance)), None, None)
842
843 - def AddInstanceTags(self, instance, tags, dry_run=False):
844 """Adds tags to an instance. 845 846 @type instance: str 847 @param instance: instance to add tags to 848 @type tags: list of str 849 @param tags: tags to add to the instance 850 @type dry_run: bool 851 @param dry_run: whether to perform a dry run 852 853 @rtype: int 854 @return: job id 855 856 """ 857 query = [("tag", t) for t in tags] 858 if dry_run: 859 query.append(("dry-run", 1)) 860 861 return self._SendRequest(HTTP_PUT, 862 ("/%s/instances/%s/tags" % 863 (GANETI_RAPI_VERSION, instance)), query, None)
864
865 - def DeleteInstanceTags(self, instance, tags, dry_run=False):
866 """Deletes tags from an instance. 867 868 @type instance: str 869 @param instance: instance to delete tags from 870 @type tags: list of str 871 @param tags: tags to delete 872 @type dry_run: bool 873 @param dry_run: whether to perform a dry run 874 875 """ 876 query = [("tag", t) for t in tags] 877 if dry_run: 878 query.append(("dry-run", 1)) 879 880 return self._SendRequest(HTTP_DELETE, 881 ("/%s/instances/%s/tags" % 882 (GANETI_RAPI_VERSION, instance)), query, None)
883
884 - def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None, 885 dry_run=False):
886 """Reboots an instance. 887 888 @type instance: str 889 @param instance: instance to rebot 890 @type reboot_type: str 891 @param reboot_type: one of: hard, soft, full 892 @type ignore_secondaries: bool 893 @param ignore_secondaries: if True, ignores errors for the secondary node 894 while re-assembling disks (in hard-reboot mode only) 895 @type dry_run: bool 896 @param dry_run: whether to perform a dry run 897 898 """ 899 query = [] 900 if reboot_type: 901 query.append(("type", reboot_type)) 902 if ignore_secondaries is not None: 903 query.append(("ignore_secondaries", ignore_secondaries)) 904 if dry_run: 905 query.append(("dry-run", 1)) 906 907 return self._SendRequest(HTTP_POST, 908 ("/%s/instances/%s/reboot" % 909 (GANETI_RAPI_VERSION, instance)), query, None)
910
911 - def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
912 """Shuts down an instance. 913 914 @type instance: str 915 @param instance: the instance to shut down 916 @type dry_run: bool 917 @param dry_run: whether to perform a dry run 918 @type no_remember: bool 919 @param no_remember: if true, will not record the state change 920 921 """ 922 query = [] 923 if dry_run: 924 query.append(("dry-run", 1)) 925 if no_remember: 926 query.append(("no-remember", 1)) 927 928 return self._SendRequest(HTTP_PUT, 929 ("/%s/instances/%s/shutdown" % 930 (GANETI_RAPI_VERSION, instance)), query, None)
931
932 - def StartupInstance(self, instance, dry_run=False, no_remember=False):
933 """Starts up an instance. 934 935 @type instance: str 936 @param instance: the instance to start up 937 @type dry_run: bool 938 @param dry_run: whether to perform a dry run 939 @type no_remember: bool 940 @param no_remember: if true, will not record the state change 941 942 """ 943 query = [] 944 if dry_run: 945 query.append(("dry-run", 1)) 946 if no_remember: 947 query.append(("no-remember", 1)) 948 949 return self._SendRequest(HTTP_PUT, 950 ("/%s/instances/%s/startup" % 951 (GANETI_RAPI_VERSION, instance)), query, None)
952
953 - def ReinstallInstance(self, instance, os=None, no_startup=False, 954 osparams=None):
955 """Reinstalls an instance. 956 957 @type instance: str 958 @param instance: The instance to reinstall 959 @type os: str or None 960 @param os: The operating system to reinstall. If None, the instance's 961 current operating system will be installed again 962 @type no_startup: bool 963 @param no_startup: Whether to start the instance automatically 964 965 """ 966 if _INST_REINSTALL_REQV1 in self.GetFeatures(): 967 body = { 968 "start": not no_startup, 969 } 970 if os is not None: 971 body["os"] = os 972 if osparams is not None: 973 body["osparams"] = osparams 974 return self._SendRequest(HTTP_POST, 975 ("/%s/instances/%s/reinstall" % 976 (GANETI_RAPI_VERSION, instance)), None, body) 977 978 # Use old request format 979 if osparams: 980 raise GanetiApiError("Server does not support specifying OS parameters" 981 " for instance reinstallation") 982 983 query = [] 984 if os: 985 query.append(("os", os)) 986 if no_startup: 987 query.append(("nostartup", 1)) 988 return self._SendRequest(HTTP_POST, 989 ("/%s/instances/%s/reinstall" % 990 (GANETI_RAPI_VERSION, instance)), query, None)
991
992 - def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO, 993 remote_node=None, iallocator=None, dry_run=False):
994 """Replaces disks on an instance. 995 996 @type instance: str 997 @param instance: instance whose disks to replace 998 @type disks: list of ints 999 @param disks: Indexes of disks to replace 1000 @type mode: str 1001 @param mode: replacement mode to use (defaults to replace_auto) 1002 @type remote_node: str or None 1003 @param remote_node: new secondary node to use (for use with 1004 replace_new_secondary mode) 1005 @type iallocator: str or None 1006 @param iallocator: instance allocator plugin to use (for use with 1007 replace_auto mode) 1008 @type dry_run: bool 1009 @param dry_run: whether to perform a dry run 1010 1011 @rtype: int 1012 @return: job id 1013 1014 """ 1015 query = [ 1016 ("mode", mode), 1017 ] 1018 1019 if disks: 1020 query.append(("disks", ",".join(str(idx) for idx in disks))) 1021 1022 if remote_node: 1023 query.append(("remote_node", remote_node)) 1024 1025 if iallocator: 1026 query.append(("iallocator", iallocator)) 1027 1028 if dry_run: 1029 query.append(("dry-run", 1)) 1030 1031 return self._SendRequest(HTTP_POST, 1032 ("/%s/instances/%s/replace-disks" % 1033 (GANETI_RAPI_VERSION, instance)), query, None)
1034
1035 - def PrepareExport(self, instance, mode):
1036 """Prepares an instance for an export. 1037 1038 @type instance: string 1039 @param instance: Instance name 1040 @type mode: string 1041 @param mode: Export mode 1042 @rtype: string 1043 @return: Job ID 1044 1045 """ 1046 query = [("mode", mode)] 1047 return self._SendRequest(HTTP_PUT, 1048 ("/%s/instances/%s/prepare-export" % 1049 (GANETI_RAPI_VERSION, instance)), query, None)
1050
1051 - def ExportInstance(self, instance, mode, destination, shutdown=None, 1052 remove_instance=None, 1053 x509_key_name=None, destination_x509_ca=None):
1054 """Exports an instance. 1055 1056 @type instance: string 1057 @param instance: Instance name 1058 @type mode: string 1059 @param mode: Export mode 1060 @rtype: string 1061 @return: Job ID 1062 1063 """ 1064 body = { 1065 "destination": destination, 1066 "mode": mode, 1067 } 1068 1069 if shutdown is not None: 1070 body["shutdown"] = shutdown 1071 1072 if remove_instance is not None: 1073 body["remove_instance"] = remove_instance 1074 1075 if x509_key_name is not None: 1076 body["x509_key_name"] = x509_key_name 1077 1078 if destination_x509_ca is not None: 1079 body["destination_x509_ca"] = destination_x509_ca 1080 1081 return self._SendRequest(HTTP_PUT, 1082 ("/%s/instances/%s/export" % 1083 (GANETI_RAPI_VERSION, instance)), None, body)
1084
1085 - def MigrateInstance(self, instance, mode=None, cleanup=None):
1086 """Migrates an instance. 1087 1088 @type instance: string 1089 @param instance: Instance name 1090 @type mode: string 1091 @param mode: Migration mode 1092 @type cleanup: bool 1093 @param cleanup: Whether to clean up a previously failed migration 1094 1095 """ 1096 body = {} 1097 1098 if mode is not None: 1099 body["mode"] = mode 1100 1101 if cleanup is not None: 1102 body["cleanup"] = cleanup 1103 1104 return self._SendRequest(HTTP_PUT, 1105 ("/%s/instances/%s/migrate" % 1106 (GANETI_RAPI_VERSION, instance)), None, body)
1107
1108 - def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1109 """Changes the name of an instance. 1110 1111 @type instance: string 1112 @param instance: Instance name 1113 @type new_name: string 1114 @param new_name: New instance name 1115 @type ip_check: bool 1116 @param ip_check: Whether to ensure instance's IP address is inactive 1117 @type name_check: bool 1118 @param name_check: Whether to ensure instance's name is resolvable 1119 1120 """ 1121 body = { 1122 "new_name": new_name, 1123 } 1124 1125 if ip_check is not None: 1126 body["ip_check"] = ip_check 1127 1128 if name_check is not None: 1129 body["name_check"] = name_check 1130 1131 return self._SendRequest(HTTP_PUT, 1132 ("/%s/instances/%s/rename" % 1133 (GANETI_RAPI_VERSION, instance)), None, body)
1134
1135 - def GetInstanceConsole(self, instance):
1136 """Request information for connecting to instance's console. 1137 1138 @type instance: string 1139 @param instance: Instance name 1140 1141 """ 1142 return self._SendRequest(HTTP_GET, 1143 ("/%s/instances/%s/console" % 1144 (GANETI_RAPI_VERSION, instance)), None, None)
1145
1146 - def GetJobs(self):
1147 """Gets all jobs for the cluster. 1148 1149 @rtype: list of int 1150 @return: job ids for the cluster 1151 1152 """ 1153 return [int(j["id"]) 1154 for j in self._SendRequest(HTTP_GET, 1155 "/%s/jobs" % GANETI_RAPI_VERSION, 1156 None, None)]
1157
1158 - def GetJobStatus(self, job_id):
1159 """Gets the status of a job. 1160 1161 @type job_id: int 1162 @param job_id: job id whose status to query 1163 1164 @rtype: dict 1165 @return: job status 1166 1167 """ 1168 return self._SendRequest(HTTP_GET, 1169 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1170 None, None)
1171
1172 - def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1173 """Waits for job changes. 1174 1175 @type job_id: int 1176 @param job_id: Job ID for which to wait 1177 1178 """ 1179 body = { 1180 "fields": fields, 1181 "previous_job_info": prev_job_info, 1182 "previous_log_serial": prev_log_serial, 1183 } 1184 1185 return self._SendRequest(HTTP_GET, 1186 "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id), 1187 None, body)
1188
1189 - def CancelJob(self, job_id, dry_run=False):
1190 """Cancels a job. 1191 1192 @type job_id: int 1193 @param job_id: id of the job to delete 1194 @type dry_run: bool 1195 @param dry_run: whether to perform a dry run 1196 1197 """ 1198 query = [] 1199 if dry_run: 1200 query.append(("dry-run", 1)) 1201 1202 return self._SendRequest(HTTP_DELETE, 1203 "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id), 1204 query, None)
1205
1206 - def GetNodes(self, bulk=False):
1207 """Gets all nodes in the cluster. 1208 1209 @type bulk: bool 1210 @param bulk: whether to return all information about all instances 1211 1212 @rtype: list of dict or str 1213 @return: if bulk is true, info about nodes in the cluster, 1214 else list of nodes in the cluster 1215 1216 """ 1217 query = [] 1218 if bulk: 1219 query.append(("bulk", 1)) 1220 1221 nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION, 1222 query, None) 1223 if bulk: 1224 return nodes 1225 else: 1226 return [n["id"] for n in nodes]
1227
1228 - def GetNode(self, node):
1229 """Gets information about a node. 1230 1231 @type node: str 1232 @param node: node whose info to return 1233 1234 @rtype: dict 1235 @return: info about the node 1236 1237 """ 1238 return self._SendRequest(HTTP_GET, 1239 "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node), 1240 None, None)
1241
1242 - def EvacuateNode(self, node, iallocator=None, remote_node=None, 1243 dry_run=False, early_release=False):
1244 """Evacuates instances from a Ganeti node. 1245 1246 @type node: str 1247 @param node: node to evacuate 1248 @type iallocator: str or None 1249 @param iallocator: instance allocator to use 1250 @type remote_node: str 1251 @param remote_node: node to evaucate to 1252 @type dry_run: bool 1253 @param dry_run: whether to perform a dry run 1254 @type early_release: bool 1255 @param early_release: whether to enable parallelization 1256 1257 @rtype: list 1258 @return: list of (job ID, instance name, new secondary node); if 1259 dry_run was specified, then the actual move jobs were not 1260 submitted and the job IDs will be C{None} 1261 1262 @raises GanetiApiError: if an iallocator and remote_node are both 1263 specified 1264 1265 """ 1266 if iallocator and remote_node: 1267 raise GanetiApiError("Only one of iallocator or remote_node can be used") 1268 1269 query = [] 1270 if iallocator: 1271 query.append(("iallocator", iallocator)) 1272 if remote_node: 1273 query.append(("remote_node", remote_node)) 1274 if dry_run: 1275 query.append(("dry-run", 1)) 1276 if early_release: 1277 query.append(("early_release", 1)) 1278 1279 return self._SendRequest(HTTP_POST, 1280 ("/%s/nodes/%s/evacuate" % 1281 (GANETI_RAPI_VERSION, node)), query, None)
1282
1283 - def MigrateNode(self, node, mode=None, dry_run=False):
1284 """Migrates all primary instances from a node. 1285 1286 @type node: str 1287 @param node: node to migrate 1288 @type mode: string 1289 @param mode: if passed, it will overwrite the live migration type, 1290 otherwise the hypervisor default will be used 1291 @type dry_run: bool 1292 @param dry_run: whether to perform a dry run 1293 1294 @rtype: int 1295 @return: job id 1296 1297 """ 1298 query = [] 1299 if mode is not None: 1300 query.append(("mode", mode)) 1301 if dry_run: 1302 query.append(("dry-run", 1)) 1303 1304 return self._SendRequest(HTTP_POST, 1305 ("/%s/nodes/%s/migrate" % 1306 (GANETI_RAPI_VERSION, node)), query, None)
1307
1308 - def GetNodeRole(self, node):
1309 """Gets the current role for a node. 1310 1311 @type node: str 1312 @param node: node whose role to return 1313 1314 @rtype: str 1315 @return: the current role for a node 1316 1317 """ 1318 return self._SendRequest(HTTP_GET, 1319 ("/%s/nodes/%s/role" % 1320 (GANETI_RAPI_VERSION, node)), None, None)
1321
1322 - def SetNodeRole(self, node, role, force=False):
1323 """Sets the role for a node. 1324 1325 @type node: str 1326 @param node: the node whose role to set 1327 @type role: str 1328 @param role: the role to set for the node 1329 @type force: bool 1330 @param force: whether to force the role change 1331 1332 @rtype: int 1333 @return: job id 1334 1335 """ 1336 query = [ 1337 ("force", force), 1338 ] 1339 1340 return self._SendRequest(HTTP_PUT, 1341 ("/%s/nodes/%s/role" % 1342 (GANETI_RAPI_VERSION, node)), query, role)
1343
1344 - def GetNodeStorageUnits(self, node, storage_type, output_fields):
1345 """Gets the storage units for a node. 1346 1347 @type node: str 1348 @param node: the node whose storage units to return 1349 @type storage_type: str 1350 @param storage_type: storage type whose units to return 1351 @type output_fields: str 1352 @param output_fields: storage type fields to return 1353 1354 @rtype: int 1355 @return: job id where results can be retrieved 1356 1357 """ 1358 query = [ 1359 ("storage_type", storage_type), 1360 ("output_fields", output_fields), 1361 ] 1362 1363 return self._SendRequest(HTTP_GET, 1364 ("/%s/nodes/%s/storage" % 1365 (GANETI_RAPI_VERSION, node)), query, None)
1366
1367 - def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1368 """Modifies parameters of storage units on the node. 1369 1370 @type node: str 1371 @param node: node whose storage units to modify 1372 @type storage_type: str 1373 @param storage_type: storage type whose units to modify 1374 @type name: str 1375 @param name: name of the storage unit 1376 @type allocatable: bool or None 1377 @param allocatable: Whether to set the "allocatable" flag on the storage 1378 unit (None=no modification, True=set, False=unset) 1379 1380 @rtype: int 1381 @return: job id 1382 1383 """ 1384 query = [ 1385 ("storage_type", storage_type), 1386 ("name", name), 1387 ] 1388 1389 if allocatable is not None: 1390 query.append(("allocatable", allocatable)) 1391 1392 return self._SendRequest(HTTP_PUT, 1393 ("/%s/nodes/%s/storage/modify" % 1394 (GANETI_RAPI_VERSION, node)), query, None)
1395
1396 - def RepairNodeStorageUnits(self, node, storage_type, name):
1397 """Repairs a storage unit on the node. 1398 1399 @type node: str 1400 @param node: node whose storage units to repair 1401 @type storage_type: str 1402 @param storage_type: storage type to repair 1403 @type name: str 1404 @param name: name of the storage unit to repair 1405 1406 @rtype: int 1407 @return: job id 1408 1409 """ 1410 query = [ 1411 ("storage_type", storage_type), 1412 ("name", name), 1413 ] 1414 1415 return self._SendRequest(HTTP_PUT, 1416 ("/%s/nodes/%s/storage/repair" % 1417 (GANETI_RAPI_VERSION, node)), query, None)
1418
1419 - def GetNodeTags(self, node):
1420 """Gets the tags for a node. 1421 1422 @type node: str 1423 @param node: node whose tags to return 1424 1425 @rtype: list of str 1426 @return: tags for the node 1427 1428 """ 1429 return self._SendRequest(HTTP_GET, 1430 ("/%s/nodes/%s/tags" % 1431 (GANETI_RAPI_VERSION, node)), None, None)
1432
1433 - def AddNodeTags(self, node, tags, dry_run=False):
1434 """Adds tags to a node. 1435 1436 @type node: str 1437 @param node: node to add tags to 1438 @type tags: list of str 1439 @param tags: tags to add to the node 1440 @type dry_run: bool 1441 @param dry_run: whether to perform a dry run 1442 1443 @rtype: int 1444 @return: job id 1445 1446 """ 1447 query = [("tag", t) for t in tags] 1448 if dry_run: 1449 query.append(("dry-run", 1)) 1450 1451 return self._SendRequest(HTTP_PUT, 1452 ("/%s/nodes/%s/tags" % 1453 (GANETI_RAPI_VERSION, node)), query, tags)
1454
1455 - def DeleteNodeTags(self, node, tags, dry_run=False):
1456 """Delete tags from a node. 1457 1458 @type node: str 1459 @param node: node to remove tags from 1460 @type tags: list of str 1461 @param tags: tags to remove from the node 1462 @type dry_run: bool 1463 @param dry_run: whether to perform a dry run 1464 1465 @rtype: int 1466 @return: job id 1467 1468 """ 1469 query = [("tag", t) for t in tags] 1470 if dry_run: 1471 query.append(("dry-run", 1)) 1472 1473 return self._SendRequest(HTTP_DELETE, 1474 ("/%s/nodes/%s/tags" % 1475 (GANETI_RAPI_VERSION, node)), query, None)
1476
1477 - def GetGroups(self, bulk=False):
1478 """Gets all node groups in the cluster. 1479 1480 @type bulk: bool 1481 @param bulk: whether to return all information about the groups 1482 1483 @rtype: list of dict or str 1484 @return: if bulk is true, a list of dictionaries with info about all node 1485 groups in the cluster, else a list of names of those node groups 1486 1487 """ 1488 query = [] 1489 if bulk: 1490 query.append(("bulk", 1)) 1491 1492 groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION, 1493 query, None) 1494 if bulk: 1495 return groups 1496 else: 1497 return [g["name"] for g in groups]
1498
1499 - def GetGroup(self, group):
1500 """Gets information about a node group. 1501 1502 @type group: str 1503 @param group: name of the node group whose info to return 1504 1505 @rtype: dict 1506 @return: info about the node group 1507 1508 """ 1509 return self._SendRequest(HTTP_GET, 1510 "/%s/groups/%s" % (GANETI_RAPI_VERSION, group), 1511 None, None)
1512
1513 - def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1514 """Creates a new node group. 1515 1516 @type name: str 1517 @param name: the name of node group to create 1518 @type alloc_policy: str 1519 @param alloc_policy: the desired allocation policy for the group, if any 1520 @type dry_run: bool 1521 @param dry_run: whether to peform a dry run 1522 1523 @rtype: int 1524 @return: job id 1525 1526 """ 1527 query = [] 1528 if dry_run: 1529 query.append(("dry-run", 1)) 1530 1531 body = { 1532 "name": name, 1533 "alloc_policy": alloc_policy 1534 } 1535 1536 return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION, 1537 query, body)
1538
1539 - def ModifyGroup(self, group, **kwargs):
1540 """Modifies a node group. 1541 1542 More details for parameters can be found in the RAPI documentation. 1543 1544 @type group: string 1545 @param group: Node group name 1546 @rtype: int 1547 @return: job id 1548 1549 """ 1550 return self._SendRequest(HTTP_PUT, 1551 ("/%s/groups/%s/modify" % 1552 (GANETI_RAPI_VERSION, group)), None, kwargs)
1553
1554 - def DeleteGroup(self, group, dry_run=False):
1555 """Deletes a node group. 1556 1557 @type group: str 1558 @param group: the node group to delete 1559 @type dry_run: bool 1560 @param dry_run: whether to peform a dry run 1561 1562 @rtype: int 1563 @return: job id 1564 1565 """ 1566 query = [] 1567 if dry_run: 1568 query.append(("dry-run", 1)) 1569 1570 return self._SendRequest(HTTP_DELETE, 1571 ("/%s/groups/%s" % 1572 (GANETI_RAPI_VERSION, group)), query, None)
1573
1574 - def RenameGroup(self, group, new_name):
1575 """Changes the name of a node group. 1576 1577 @type group: string 1578 @param group: Node group name 1579 @type new_name: string 1580 @param new_name: New node group name 1581 1582 @rtype: int 1583 @return: job id 1584 1585 """ 1586 body = { 1587 "new_name": new_name, 1588 } 1589 1590 return self._SendRequest(HTTP_PUT, 1591 ("/%s/groups/%s/rename" % 1592 (GANETI_RAPI_VERSION, group)), None, body)
1593 1594
1595 - def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1596 """Assigns nodes to a group. 1597 1598 @type group: string 1599 @param group: Node gropu name 1600 @type nodes: list of strings 1601 @param nodes: List of nodes to assign to the group 1602 1603 @rtype: int 1604 @return: job id 1605 1606 """ 1607 query = [] 1608 1609 if force: 1610 query.append(("force", 1)) 1611 1612 if dry_run: 1613 query.append(("dry-run", 1)) 1614 1615 body = { 1616 "nodes": nodes, 1617 } 1618 1619 return self._SendRequest(HTTP_PUT, 1620 ("/%s/groups/%s/assign-nodes" % 1621 (GANETI_RAPI_VERSION, group)), query, body)
1622