Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 11b61970

History | View | Annotate | Download (64.2 kB)

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 _SetItemIf(container, condition, item, value):
223
  """Sets an item if a condition evaluates to truth.
224

225
  """
226
  if condition:
227
    container[item] = value
228

    
229
  return condition
230

    
231

    
232
def UsesRapiClient(fn):
233
  """Decorator for code using RAPI client to initialize pycURL.
234

235
  """
236
  def wrapper(*args, **kwargs):
237
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
238
    # one thread running. This check is just a safety measure -- it doesn't
239
    # cover all cases.
240
    assert threading.activeCount() == 1, \
241
           "Found active threads when initializing pycURL"
242

    
243
    pycurl.global_init(pycurl.GLOBAL_ALL)
244
    try:
245
      return fn(*args, **kwargs)
246
    finally:
247
      pycurl.global_cleanup()
248

    
249
  return wrapper
250

    
251

    
252
def GenericCurlConfig(verbose=False, use_signal=False,
253
                      use_curl_cabundle=False, cafile=None, capath=None,
254
                      proxy=None, verify_hostname=False,
255
                      connect_timeout=None, timeout=None,
256
                      _pycurl_version_fn=pycurl.version_info):
257
  """Curl configuration function generator.
258

259
  @type verbose: bool
260
  @param verbose: Whether to set cURL to verbose mode
261
  @type use_signal: bool
262
  @param use_signal: Whether to allow cURL to use signals
263
  @type use_curl_cabundle: bool
264
  @param use_curl_cabundle: Whether to use cURL's default CA bundle
265
  @type cafile: string
266
  @param cafile: In which file we can find the certificates
267
  @type capath: string
268
  @param capath: In which directory we can find the certificates
269
  @type proxy: string
270
  @param proxy: Proxy to use, None for default behaviour and empty string for
271
                disabling proxies (see curl_easy_setopt(3))
272
  @type verify_hostname: bool
273
  @param verify_hostname: Whether to verify the remote peer certificate's
274
                          commonName
275
  @type connect_timeout: number
276
  @param connect_timeout: Timeout for establishing connection in seconds
277
  @type timeout: number
278
  @param timeout: Timeout for complete transfer in seconds (see
279
                  curl_easy_setopt(3)).
280

281
  """
282
  if use_curl_cabundle and (cafile or capath):
283
    raise Error("Can not use default CA bundle when CA file or path is set")
284

    
285
  def _ConfigCurl(curl, logger):
286
    """Configures a cURL object
287

288
    @type curl: pycurl.Curl
289
    @param curl: cURL object
290

291
    """
292
    logger.debug("Using cURL version %s", pycurl.version)
293

    
294
    # pycurl.version_info returns a tuple with information about the used
295
    # version of libcurl. Item 5 is the SSL library linked to it.
296
    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
297
    # 0, '1.2.3.3', ...)
298
    sslver = _pycurl_version_fn()[5]
299
    if not sslver:
300
      raise Error("No SSL support in cURL")
301

    
302
    lcsslver = sslver.lower()
303
    if lcsslver.startswith("openssl/"):
304
      pass
305
    elif lcsslver.startswith("nss/"):
306
      # TODO: investigate compatibility beyond a simple test
307
      pass
308
    elif lcsslver.startswith("gnutls/"):
309
      if capath:
310
        raise Error("cURL linked against GnuTLS has no support for a"
311
                    " CA path (%s)" % (pycurl.version, ))
312
    else:
313
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
314
                                sslver)
315

    
316
    curl.setopt(pycurl.VERBOSE, verbose)
317
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
318

    
319
    # Whether to verify remote peer's CN
320
    if verify_hostname:
321
      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
322
      # certificate must indicate that the server is the server to which you
323
      # meant to connect, or the connection fails. [...] When the value is 1,
324
      # the certificate must contain a Common Name field, but it doesn't matter
325
      # what name it says. [...]"
326
      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
327
    else:
328
      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
329

    
330
    if cafile or capath or use_curl_cabundle:
331
      # Require certificates to be checked
332
      curl.setopt(pycurl.SSL_VERIFYPEER, True)
333
      if cafile:
334
        curl.setopt(pycurl.CAINFO, str(cafile))
335
      if capath:
336
        curl.setopt(pycurl.CAPATH, str(capath))
337
      # Not changing anything for using default CA bundle
338
    else:
339
      # Disable SSL certificate verification
340
      curl.setopt(pycurl.SSL_VERIFYPEER, False)
341

    
342
    if proxy is not None:
343
      curl.setopt(pycurl.PROXY, str(proxy))
344

    
345
    # Timeouts
346
    if connect_timeout is not None:
347
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
348
    if timeout is not None:
349
      curl.setopt(pycurl.TIMEOUT, timeout)
350

    
351
  return _ConfigCurl
352

    
353

    
354
class GanetiRapiClient(object): # pylint: disable=R0904
355
  """Ganeti RAPI client.
356

357
  """
358
  USER_AGENT = "Ganeti RAPI Client"
359
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
360

    
361
  def __init__(self, host, port=GANETI_RAPI_PORT,
362
               username=None, password=None, logger=logging,
363
               curl_config_fn=None, curl_factory=None):
364
    """Initializes this class.
365

366
    @type host: string
367
    @param host: the ganeti cluster master to interact with
368
    @type port: int
369
    @param port: the port on which the RAPI is running (default is 5080)
370
    @type username: string
371
    @param username: the username to connect with
372
    @type password: string
373
    @param password: the password to connect with
374
    @type curl_config_fn: callable
375
    @param curl_config_fn: Function to configure C{pycurl.Curl} object
376
    @param logger: Logging object
377

378
    """
379
    self._username = username
380
    self._password = password
381
    self._logger = logger
382
    self._curl_config_fn = curl_config_fn
383
    self._curl_factory = curl_factory
384

    
385
    try:
386
      socket.inet_pton(socket.AF_INET6, host)
387
      address = "[%s]:%s" % (host, port)
388
    except socket.error:
389
      address = "%s:%s" % (host, port)
390

    
391
    self._base_url = "https://%s" % address
392

    
393
    if username is not None:
394
      if password is None:
395
        raise Error("Password not specified")
396
    elif password:
397
      raise Error("Specified password without username")
398

    
399
  def _CreateCurl(self):
400
    """Creates a cURL object.
401

402
    """
403
    # Create pycURL object if no factory is provided
404
    if self._curl_factory:
405
      curl = self._curl_factory()
406
    else:
407
      curl = pycurl.Curl()
408

    
409
    # Default cURL settings
410
    curl.setopt(pycurl.VERBOSE, False)
411
    curl.setopt(pycurl.FOLLOWLOCATION, False)
412
    curl.setopt(pycurl.MAXREDIRS, 5)
413
    curl.setopt(pycurl.NOSIGNAL, True)
414
    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
415
    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
416
    curl.setopt(pycurl.SSL_VERIFYPEER, False)
417
    curl.setopt(pycurl.HTTPHEADER, [
418
      "Accept: %s" % HTTP_APP_JSON,
419
      "Content-type: %s" % HTTP_APP_JSON,
420
      ])
421

    
422
    assert ((self._username is None and self._password is None) ^
423
            (self._username is not None and self._password is not None))
424

    
425
    if self._username:
426
      # Setup authentication
427
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
428
      curl.setopt(pycurl.USERPWD,
429
                  str("%s:%s" % (self._username, self._password)))
430

    
431
    # Call external configuration function
432
    if self._curl_config_fn:
433
      self._curl_config_fn(curl, self._logger)
434

    
435
    return curl
436

    
437
  @staticmethod
438
  def _EncodeQuery(query):
439
    """Encode query values for RAPI URL.
440

441
    @type query: list of two-tuples
442
    @param query: Query arguments
443
    @rtype: list
444
    @return: Query list with encoded values
445

446
    """
447
    result = []
448

    
449
    for name, value in query:
450
      if value is None:
451
        result.append((name, ""))
452

    
453
      elif isinstance(value, bool):
454
        # Boolean values must be encoded as 0 or 1
455
        result.append((name, int(value)))
456

    
457
      elif isinstance(value, (list, tuple, dict)):
458
        raise ValueError("Invalid query data type %r" % type(value).__name__)
459

    
460
      else:
461
        result.append((name, value))
462

    
463
    return result
464

    
465
  def _SendRequest(self, method, path, query, content):
466
    """Sends an HTTP request.
467

468
    This constructs a full URL, encodes and decodes HTTP bodies, and
469
    handles invalid responses in a pythonic way.
470

471
    @type method: string
472
    @param method: HTTP method to use
473
    @type path: string
474
    @param path: HTTP URL path
475
    @type query: list of two-tuples
476
    @param query: query arguments to pass to urllib.urlencode
477
    @type content: str or None
478
    @param content: HTTP body content
479

480
    @rtype: str
481
    @return: JSON-Decoded response
482

483
    @raises CertificateError: If an invalid SSL certificate is found
484
    @raises GanetiApiError: If an invalid response is returned
485

486
    """
487
    assert path.startswith("/")
488

    
489
    curl = self._CreateCurl()
490

    
491
    if content is not None:
492
      encoded_content = self._json_encoder.encode(content)
493
    else:
494
      encoded_content = ""
495

    
496
    # Build URL
497
    urlparts = [self._base_url, path]
498
    if query:
499
      urlparts.append("?")
500
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
501

    
502
    url = "".join(urlparts)
503

    
504
    self._logger.debug("Sending request %s %s (content=%r)",
505
                       method, url, encoded_content)
506

    
507
    # Buffer for response
508
    encoded_resp_body = StringIO()
509

    
510
    # Configure cURL
511
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
512
    curl.setopt(pycurl.URL, str(url))
513
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
514
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
515

    
516
    try:
517
      # Send request and wait for response
518
      try:
519
        curl.perform()
520
      except pycurl.error, err:
521
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
522
          raise CertificateError("SSL certificate error %s" % err,
523
                                 code=err.args[0])
524

    
525
        raise GanetiApiError(str(err), code=err.args[0])
526
    finally:
527
      # Reset settings to not keep references to large objects in memory
528
      # between requests
529
      curl.setopt(pycurl.POSTFIELDS, "")
530
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
531

    
532
    # Get HTTP response code
533
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
534

    
535
    # Was anything written to the response buffer?
536
    if encoded_resp_body.tell():
537
      response_content = simplejson.loads(encoded_resp_body.getvalue())
538
    else:
539
      response_content = None
540

    
541
    if http_code != HTTP_OK:
542
      if isinstance(response_content, dict):
543
        msg = ("%s %s: %s" %
544
               (response_content["code"],
545
                response_content["message"],
546
                response_content["explain"]))
547
      else:
548
        msg = str(response_content)
549

    
550
      raise GanetiApiError(msg, code=http_code)
551

    
552
    return response_content
553

    
554
  def GetVersion(self):
555
    """Gets the Remote API version running on the cluster.
556

557
    @rtype: int
558
    @return: Ganeti Remote API version
559

560
    """
561
    return self._SendRequest(HTTP_GET, "/version", None, None)
562

    
563
  def GetFeatures(self):
564
    """Gets the list of optional features supported by RAPI server.
565

566
    @rtype: list
567
    @return: List of optional features
568

569
    """
570
    try:
571
      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
572
                               None, None)
573
    except GanetiApiError, err:
574
      # Older RAPI servers don't support this resource
575
      if err.code == HTTP_NOT_FOUND:
576
        return []
577

    
578
      raise
579

    
580
  def GetOperatingSystems(self):
581
    """Gets the Operating Systems running in the Ganeti cluster.
582

583
    @rtype: list of str
584
    @return: operating systems
585

586
    """
587
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
588
                             None, None)
589

    
590
  def GetInfo(self):
591
    """Gets info about the cluster.
592

593
    @rtype: dict
594
    @return: information about the cluster
595

596
    """
597
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
598
                             None, None)
599

    
600
  def RedistributeConfig(self):
601
    """Tells the cluster to redistribute its configuration files.
602

603
    @rtype: string
604
    @return: job id
605

606
    """
607
    return self._SendRequest(HTTP_PUT,
608
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
609
                             None, None)
610

    
611
  def ModifyCluster(self, **kwargs):
612
    """Modifies cluster parameters.
613

614
    More details for parameters can be found in the RAPI documentation.
615

616
    @rtype: string
617
    @return: job id
618

619
    """
620
    body = kwargs
621

    
622
    return self._SendRequest(HTTP_PUT,
623
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
624

    
625
  def GetClusterTags(self):
626
    """Gets the cluster tags.
627

628
    @rtype: list of str
629
    @return: cluster tags
630

631
    """
632
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
633
                             None, None)
634

    
635
  def AddClusterTags(self, tags, dry_run=False):
636
    """Adds tags to the cluster.
637

638
    @type tags: list of str
639
    @param tags: tags to add to the cluster
640
    @type dry_run: bool
641
    @param dry_run: whether to perform a dry run
642

643
    @rtype: string
644
    @return: job id
645

646
    """
647
    query = [("tag", t) for t in tags]
648
    _AppendDryRunIf(query, dry_run)
649

    
650
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
651
                             query, None)
652

    
653
  def DeleteClusterTags(self, tags, dry_run=False):
654
    """Deletes tags from the cluster.
655

656
    @type tags: list of str
657
    @param tags: tags to delete
658
    @type dry_run: bool
659
    @param dry_run: whether to perform a dry run
660
    @rtype: string
661
    @return: job id
662

663
    """
664
    query = [("tag", t) for t in tags]
665
    _AppendDryRunIf(query, dry_run)
666

    
667
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
668
                             query, None)
669

    
670
  def GetInstances(self, bulk=False):
671
    """Gets information about instances on the cluster.
672

673
    @type bulk: bool
674
    @param bulk: whether to return all information about all instances
675

676
    @rtype: list of dict or list of str
677
    @return: if bulk is True, info about the instances, else a list of instances
678

679
    """
680
    query = []
681
    _AppendIf(query, bulk, ("bulk", 1))
682

    
683
    instances = self._SendRequest(HTTP_GET,
684
                                  "/%s/instances" % GANETI_RAPI_VERSION,
685
                                  query, None)
686
    if bulk:
687
      return instances
688
    else:
689
      return [i["id"] for i in instances]
690

    
691
  def GetInstance(self, instance):
692
    """Gets information about an instance.
693

694
    @type instance: str
695
    @param instance: instance whose info to return
696

697
    @rtype: dict
698
    @return: info about the instance
699

700
    """
701
    return self._SendRequest(HTTP_GET,
702
                             ("/%s/instances/%s" %
703
                              (GANETI_RAPI_VERSION, instance)), None, None)
704

    
705
  def GetInstanceInfo(self, instance, static=None):
706
    """Gets information about an instance.
707

708
    @type instance: string
709
    @param instance: Instance name
710
    @rtype: string
711
    @return: Job ID
712

713
    """
714
    if static is not None:
715
      query = [("static", static)]
716
    else:
717
      query = None
718

    
719
    return self._SendRequest(HTTP_GET,
720
                             ("/%s/instances/%s/info" %
721
                              (GANETI_RAPI_VERSION, instance)), query, None)
722

    
723
  @staticmethod
724
  def _UpdateWithKwargs(base, **kwargs):
725
    """Updates the base with params from kwargs.
726

727
    @param base: The base dict, filled with required fields
728

729
    @note: This is an inplace update of base
730

731
    """
732
    conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
733
    if conflicts:
734
      raise GanetiApiError("Required fields can not be specified as"
735
                           " keywords: %s" % ", ".join(conflicts))
736

    
737
    base.update((key, value) for key, value in kwargs.iteritems()
738
                if key != "dry_run")
739

    
740
  def InstanceAllocation(self, mode, name, disk_template, disks, nics,
741
                         **kwargs):
742
    """Generates an instance allocation as used by multiallocate.
743

744
    More details for parameters can be found in the RAPI documentation.
745
    It is the same as used by CreateInstance.
746

747
    @type mode: string
748
    @param mode: Instance creation mode
749
    @type name: string
750
    @param name: Hostname of the instance to create
751
    @type disk_template: string
752
    @param disk_template: Disk template for instance (e.g. plain, diskless,
753
                          file, or drbd)
754
    @type disks: list of dicts
755
    @param disks: List of disk definitions
756
    @type nics: list of dicts
757
    @param nics: List of NIC definitions
758

759
    @return: A dict with the generated entry
760

761
    """
762
    # All required fields for request data version 1
763
    alloc = {
764
      "mode": mode,
765
      "name": name,
766
      "disk_template": disk_template,
767
      "disks": disks,
768
      "nics": nics,
769
      }
770

    
771
    self._UpdateWithKwargs(alloc, **kwargs)
772

    
773
    return alloc
774

    
775
  def InstancesMultiAlloc(self, instances, **kwargs):
776
    """Tries to allocate multiple instances.
777

778
    More details for parameters can be found in the RAPI documentation.
779

780
    @param instances: A list of L{InstanceAllocation} results
781

782
    """
783
    query = []
784
    body = {
785
      "instances": instances,
786
      }
787
    self._UpdateWithKwargs(body, **kwargs)
788

    
789
    _AppendDryRunIf(query, kwargs.get("dry_run"))
790

    
791
    return self._SendRequest(HTTP_POST,
792
                             "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
793
                             query, body)
794

    
795
  def CreateInstance(self, mode, name, disk_template, disks, nics,
796
                     **kwargs):
797
    """Creates a new instance.
798

799
    More details for parameters can be found in the RAPI documentation.
800

801
    @type mode: string
802
    @param mode: Instance creation mode
803
    @type name: string
804
    @param name: Hostname of the instance to create
805
    @type disk_template: string
806
    @param disk_template: Disk template for instance (e.g. plain, diskless,
807
                          file, or drbd)
808
    @type disks: list of dicts
809
    @param disks: List of disk definitions
810
    @type nics: list of dicts
811
    @param nics: List of NIC definitions
812
    @type dry_run: bool
813
    @keyword dry_run: whether to perform a dry run
814

815
    @rtype: string
816
    @return: job id
817

818
    """
819
    query = []
820

    
821
    _AppendDryRunIf(query, kwargs.get("dry_run"))
822

    
823
    if _INST_CREATE_REQV1 in self.GetFeatures():
824
      body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
825
                                     **kwargs)
826
      body[_REQ_DATA_VERSION_FIELD] = 1
827
    else:
828
      raise GanetiApiError("Server does not support new-style (version 1)"
829
                           " instance creation requests")
830

    
831
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
832
                             query, body)
833

    
834
  def DeleteInstance(self, instance, dry_run=False, **kwargs):
835
    """Deletes an instance.
836

837
    @type instance: str
838
    @param instance: the instance to delete
839

840
    @rtype: string
841
    @return: job id
842

843
    """
844
    query = []
845
    body = kwargs
846

    
847
    _AppendDryRunIf(query, dry_run)
848

    
849
    return self._SendRequest(HTTP_DELETE,
850
                             ("/%s/instances/%s" %
851
                              (GANETI_RAPI_VERSION, instance)), query, body)
852

    
853
  def ModifyInstance(self, instance, **kwargs):
854
    """Modifies an instance.
855

856
    More details for parameters can be found in the RAPI documentation.
857

858
    @type instance: string
859
    @param instance: Instance name
860
    @rtype: string
861
    @return: job id
862

863
    """
864
    body = kwargs
865

    
866
    return self._SendRequest(HTTP_PUT,
867
                             ("/%s/instances/%s/modify" %
868
                              (GANETI_RAPI_VERSION, instance)), None, body)
869

    
870
  def ActivateInstanceDisks(self, instance, ignore_size=None):
871
    """Activates an instance's disks.
872

873
    @type instance: string
874
    @param instance: Instance name
875
    @type ignore_size: bool
876
    @param ignore_size: Whether to ignore recorded size
877
    @rtype: string
878
    @return: job id
879

880
    """
881
    query = []
882
    _AppendIf(query, ignore_size, ("ignore_size", 1))
883

    
884
    return self._SendRequest(HTTP_PUT,
885
                             ("/%s/instances/%s/activate-disks" %
886
                              (GANETI_RAPI_VERSION, instance)), query, None)
887

    
888
  def DeactivateInstanceDisks(self, instance):
889
    """Deactivates an instance's disks.
890

891
    @type instance: string
892
    @param instance: Instance name
893
    @rtype: string
894
    @return: job id
895

896
    """
897
    return self._SendRequest(HTTP_PUT,
898
                             ("/%s/instances/%s/deactivate-disks" %
899
                              (GANETI_RAPI_VERSION, instance)), None, None)
900

    
901
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
902
    """Recreate an instance's disks.
903

904
    @type instance: string
905
    @param instance: Instance name
906
    @type disks: list of int
907
    @param disks: List of disk indexes
908
    @type nodes: list of string
909
    @param nodes: New instance nodes, if relocation is desired
910
    @rtype: string
911
    @return: job id
912

913
    """
914
    body = {}
915
    _SetItemIf(body, disks is not None, "disks", disks)
916
    _SetItemIf(body, nodes is not None, "nodes", nodes)
917

    
918
    return self._SendRequest(HTTP_POST,
919
                             ("/%s/instances/%s/recreate-disks" %
920
                              (GANETI_RAPI_VERSION, instance)), None, body)
921

    
922
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
923
    """Grows a disk of an instance.
924

925
    More details for parameters can be found in the RAPI documentation.
926

927
    @type instance: string
928
    @param instance: Instance name
929
    @type disk: integer
930
    @param disk: Disk index
931
    @type amount: integer
932
    @param amount: Grow disk by this amount (MiB)
933
    @type wait_for_sync: bool
934
    @param wait_for_sync: Wait for disk to synchronize
935
    @rtype: string
936
    @return: job id
937

938
    """
939
    body = {
940
      "amount": amount,
941
      }
942

    
943
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
944

    
945
    return self._SendRequest(HTTP_POST,
946
                             ("/%s/instances/%s/disk/%s/grow" %
947
                              (GANETI_RAPI_VERSION, instance, disk)),
948
                             None, body)
949

    
950
  def GetInstanceTags(self, instance):
951
    """Gets tags for an instance.
952

953
    @type instance: str
954
    @param instance: instance whose tags to return
955

956
    @rtype: list of str
957
    @return: tags for the instance
958

959
    """
960
    return self._SendRequest(HTTP_GET,
961
                             ("/%s/instances/%s/tags" %
962
                              (GANETI_RAPI_VERSION, instance)), None, None)
963

    
964
  def AddInstanceTags(self, instance, tags, dry_run=False):
965
    """Adds tags to an instance.
966

967
    @type instance: str
968
    @param instance: instance to add tags to
969
    @type tags: list of str
970
    @param tags: tags to add to the instance
971
    @type dry_run: bool
972
    @param dry_run: whether to perform a dry run
973

974
    @rtype: string
975
    @return: job id
976

977
    """
978
    query = [("tag", t) for t in tags]
979
    _AppendDryRunIf(query, dry_run)
980

    
981
    return self._SendRequest(HTTP_PUT,
982
                             ("/%s/instances/%s/tags" %
983
                              (GANETI_RAPI_VERSION, instance)), query, None)
984

    
985
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
986
    """Deletes tags from an instance.
987

988
    @type instance: str
989
    @param instance: instance to delete tags from
990
    @type tags: list of str
991
    @param tags: tags to delete
992
    @type dry_run: bool
993
    @param dry_run: whether to perform a dry run
994
    @rtype: string
995
    @return: job id
996

997
    """
998
    query = [("tag", t) for t in tags]
999
    _AppendDryRunIf(query, dry_run)
1000

    
1001
    return self._SendRequest(HTTP_DELETE,
1002
                             ("/%s/instances/%s/tags" %
1003
                              (GANETI_RAPI_VERSION, instance)), query, None)
1004

    
1005
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1006
                     dry_run=False, reason=None, **kwargs):
1007
    """Reboots an instance.
1008

1009
    @type instance: str
1010
    @param instance: instance to reboot
1011
    @type reboot_type: str
1012
    @param reboot_type: one of: hard, soft, full
1013
    @type ignore_secondaries: bool
1014
    @param ignore_secondaries: if True, ignores errors for the secondary node
1015
        while re-assembling disks (in hard-reboot mode only)
1016
    @type dry_run: bool
1017
    @param dry_run: whether to perform a dry run
1018
    @type reason: string
1019
    @param reason: the reason for the reboot
1020
    @rtype: string
1021
    @return: job id
1022

1023
    """
1024
    query = []
1025
    body = kwargs
1026

    
1027
    _AppendDryRunIf(query, dry_run)
1028
    _AppendIf(query, reboot_type, ("type", reboot_type))
1029
    _AppendIf(query, ignore_secondaries is not None,
1030
              ("ignore_secondaries", ignore_secondaries))
1031
    _AppendIf(query, reason, ("reason", reason))
1032

    
1033
    return self._SendRequest(HTTP_POST,
1034
                             ("/%s/instances/%s/reboot" %
1035
                              (GANETI_RAPI_VERSION, instance)), query, body)
1036

    
1037
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1038
                       reason=None, **kwargs):
1039
    """Shuts down an instance.
1040

1041
    @type instance: str
1042
    @param instance: the instance to shut down
1043
    @type dry_run: bool
1044
    @param dry_run: whether to perform a dry run
1045
    @type no_remember: bool
1046
    @param no_remember: if true, will not record the state change
1047
    @type reason: string
1048
    @param reason: the reason for the shutdown
1049
    @rtype: string
1050
    @return: job id
1051

1052
    """
1053
    query = []
1054
    body = kwargs
1055

    
1056
    _AppendDryRunIf(query, dry_run)
1057
    _AppendIf(query, no_remember, ("no_remember", 1))
1058
    _AppendIf(query, reason, ("reason", reason))
1059

    
1060
    return self._SendRequest(HTTP_PUT,
1061
                             ("/%s/instances/%s/shutdown" %
1062
                              (GANETI_RAPI_VERSION, instance)), query, body)
1063

    
1064
  def StartupInstance(self, instance, dry_run=False, no_remember=False,
1065
                      reason=None):
1066
    """Starts up an instance.
1067

1068
    @type instance: str
1069
    @param instance: the instance to start up
1070
    @type dry_run: bool
1071
    @param dry_run: whether to perform a dry run
1072
    @type no_remember: bool
1073
    @param no_remember: if true, will not record the state change
1074
    @type reason: string
1075
    @param reason: the reason for the startup
1076
    @rtype: string
1077
    @return: job id
1078

1079
    """
1080
    query = []
1081
    _AppendDryRunIf(query, dry_run)
1082
    _AppendIf(query, no_remember, ("no_remember", 1))
1083
    _AppendIf(query, reason, ("reason", reason))
1084

    
1085
    return self._SendRequest(HTTP_PUT,
1086
                             ("/%s/instances/%s/startup" %
1087
                              (GANETI_RAPI_VERSION, instance)), query, None)
1088

    
1089
  def ReinstallInstance(self, instance, os=None, no_startup=False,
1090
                        osparams=None):
1091
    """Reinstalls an instance.
1092

1093
    @type instance: str
1094
    @param instance: The instance to reinstall
1095
    @type os: str or None
1096
    @param os: The operating system to reinstall. If None, the instance's
1097
        current operating system will be installed again
1098
    @type no_startup: bool
1099
    @param no_startup: Whether to start the instance automatically
1100
    @rtype: string
1101
    @return: job id
1102

1103
    """
1104
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
1105
      body = {
1106
        "start": not no_startup,
1107
        }
1108
      _SetItemIf(body, os is not None, "os", os)
1109
      _SetItemIf(body, osparams is not None, "osparams", osparams)
1110
      return self._SendRequest(HTTP_POST,
1111
                               ("/%s/instances/%s/reinstall" %
1112
                                (GANETI_RAPI_VERSION, instance)), None, body)
1113

    
1114
    # Use old request format
1115
    if osparams:
1116
      raise GanetiApiError("Server does not support specifying OS parameters"
1117
                           " for instance reinstallation")
1118

    
1119
    query = []
1120
    _AppendIf(query, os, ("os", os))
1121
    _AppendIf(query, no_startup, ("nostartup", 1))
1122

    
1123
    return self._SendRequest(HTTP_POST,
1124
                             ("/%s/instances/%s/reinstall" %
1125
                              (GANETI_RAPI_VERSION, instance)), query, None)
1126

    
1127
  def SnapshotInstance(self, instance, disks, dry_run=False):
1128
    """Snapshots disks on an instance.
1129

1130
    @type instance: str
1131
    @param instance: instance whose disks to replace
1132
    @type disks: list
1133
    @param disks: list of tuples (idend, snapshot_name)
1134

1135
    @rtype: string
1136
    @return: job id
1137

1138
    """
1139

    
1140
    body = {
1141
      "disks": disks,
1142
      }
1143

    
1144
    query = []
1145
    _AppendDryRunIf(query, dry_run)
1146

    
1147
    return self._SendRequest(HTTP_POST,
1148
                             ("/%s/instances/%s/snapshot" %
1149
                              (GANETI_RAPI_VERSION, instance)), query, body)
1150

    
1151
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1152
                           remote_node=None, iallocator=None):
1153
    """Replaces disks on an instance.
1154

1155
    @type instance: str
1156
    @param instance: instance whose disks to replace
1157
    @type disks: list of ints
1158
    @param disks: Indexes of disks to replace
1159
    @type mode: str
1160
    @param mode: replacement mode to use (defaults to replace_auto)
1161
    @type remote_node: str or None
1162
    @param remote_node: new secondary node to use (for use with
1163
        replace_new_secondary mode)
1164
    @type iallocator: str or None
1165
    @param iallocator: instance allocator plugin to use (for use with
1166
                       replace_auto mode)
1167

1168
    @rtype: string
1169
    @return: job id
1170

1171
    """
1172
    query = [
1173
      ("mode", mode),
1174
      ]
1175

    
1176
    # TODO: Convert to body parameters
1177

    
1178
    if disks is not None:
1179
      _AppendIf(query, True,
1180
                ("disks", ",".join(str(idx) for idx in disks)))
1181

    
1182
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1183
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1184

    
1185
    return self._SendRequest(HTTP_POST,
1186
                             ("/%s/instances/%s/replace-disks" %
1187
                              (GANETI_RAPI_VERSION, instance)), query, None)
1188

    
1189
  def PrepareExport(self, instance, mode):
1190
    """Prepares an instance for an export.
1191

1192
    @type instance: string
1193
    @param instance: Instance name
1194
    @type mode: string
1195
    @param mode: Export mode
1196
    @rtype: string
1197
    @return: Job ID
1198

1199
    """
1200
    query = [("mode", mode)]
1201
    return self._SendRequest(HTTP_PUT,
1202
                             ("/%s/instances/%s/prepare-export" %
1203
                              (GANETI_RAPI_VERSION, instance)), query, None)
1204

    
1205
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1206
                     remove_instance=None,
1207
                     x509_key_name=None, destination_x509_ca=None):
1208
    """Exports an instance.
1209

1210
    @type instance: string
1211
    @param instance: Instance name
1212
    @type mode: string
1213
    @param mode: Export mode
1214
    @rtype: string
1215
    @return: Job ID
1216

1217
    """
1218
    body = {
1219
      "destination": destination,
1220
      "mode": mode,
1221
      }
1222

    
1223
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1224
    _SetItemIf(body, remove_instance is not None,
1225
               "remove_instance", remove_instance)
1226
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1227
    _SetItemIf(body, destination_x509_ca is not None,
1228
               "destination_x509_ca", destination_x509_ca)
1229

    
1230
    return self._SendRequest(HTTP_PUT,
1231
                             ("/%s/instances/%s/export" %
1232
                              (GANETI_RAPI_VERSION, instance)), None, body)
1233

    
1234
  def MigrateInstance(self, instance, mode=None, cleanup=None,
1235
                      target_node=None):
1236
    """Migrates an instance.
1237

1238
    @type instance: string
1239
    @param instance: Instance name
1240
    @type mode: string
1241
    @param mode: Migration mode
1242
    @type cleanup: bool
1243
    @param cleanup: Whether to clean up a previously failed migration
1244
    @type target_node: string
1245
    @param target_node: Target Node for externally mirrored instances
1246
    @rtype: string
1247
    @return: job id
1248

1249
    """
1250
    body = {}
1251
    _SetItemIf(body, mode is not None, "mode", mode)
1252
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1253
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1254

    
1255
    return self._SendRequest(HTTP_PUT,
1256
                             ("/%s/instances/%s/migrate" %
1257
                              (GANETI_RAPI_VERSION, instance)), None, body)
1258

    
1259
  def FailoverInstance(self, instance, iallocator=None,
1260
                       ignore_consistency=None, target_node=None):
1261
    """Does a failover of an instance.
1262

1263
    @type instance: string
1264
    @param instance: Instance name
1265
    @type iallocator: string
1266
    @param iallocator: Iallocator for deciding the target node for
1267
      shared-storage instances
1268
    @type ignore_consistency: bool
1269
    @param ignore_consistency: Whether to ignore disk consistency
1270
    @type target_node: string
1271
    @param target_node: Target node for shared-storage instances
1272
    @rtype: string
1273
    @return: job id
1274

1275
    """
1276
    body = {}
1277
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1278
    _SetItemIf(body, ignore_consistency is not None,
1279
               "ignore_consistency", ignore_consistency)
1280
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1281

    
1282
    return self._SendRequest(HTTP_PUT,
1283
                             ("/%s/instances/%s/failover" %
1284
                              (GANETI_RAPI_VERSION, instance)), None, body)
1285

    
1286
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1287
    """Changes the name of an instance.
1288

1289
    @type instance: string
1290
    @param instance: Instance name
1291
    @type new_name: string
1292
    @param new_name: New instance name
1293
    @type ip_check: bool
1294
    @param ip_check: Whether to ensure instance's IP address is inactive
1295
    @type name_check: bool
1296
    @param name_check: Whether to ensure instance's name is resolvable
1297
    @rtype: string
1298
    @return: job id
1299

1300
    """
1301
    body = {
1302
      "new_name": new_name,
1303
      }
1304

    
1305
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1306
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1307

    
1308
    return self._SendRequest(HTTP_PUT,
1309
                             ("/%s/instances/%s/rename" %
1310
                              (GANETI_RAPI_VERSION, instance)), None, body)
1311

    
1312
  def GetInstanceConsole(self, instance):
1313
    """Request information for connecting to instance's console.
1314

1315
    @type instance: string
1316
    @param instance: Instance name
1317
    @rtype: dict
1318
    @return: dictionary containing information about instance's console
1319

1320
    """
1321
    return self._SendRequest(HTTP_GET,
1322
                             ("/%s/instances/%s/console" %
1323
                              (GANETI_RAPI_VERSION, instance)), None, None)
1324

    
1325
  def GetJobs(self, bulk=False):
1326
    """Gets all jobs for the cluster.
1327

1328
    @type bulk: bool
1329
    @param bulk: Whether to return detailed information about jobs.
1330
    @rtype: list of int
1331
    @return: List of job ids for the cluster or list of dicts with detailed
1332
             information about the jobs if bulk parameter was true.
1333

1334
    """
1335
    query = []
1336
    _AppendIf(query, bulk, ("bulk", 1))
1337

    
1338
    if bulk:
1339
      return self._SendRequest(HTTP_GET,
1340
                               "/%s/jobs" % GANETI_RAPI_VERSION,
1341
                               query, None)
1342
    else:
1343
      return [int(j["id"])
1344
              for j in self._SendRequest(HTTP_GET,
1345
                                         "/%s/jobs" % GANETI_RAPI_VERSION,
1346
                                         None, None)]
1347

    
1348
  def GetJobStatus(self, job_id):
1349
    """Gets the status of a job.
1350

1351
    @type job_id: string
1352
    @param job_id: job id whose status to query
1353

1354
    @rtype: dict
1355
    @return: job status
1356

1357
    """
1358
    return self._SendRequest(HTTP_GET,
1359
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1360
                             None, None)
1361

    
1362
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1363
    """Polls cluster for job status until completion.
1364

1365
    Completion is defined as any of the following states listed in
1366
    L{JOB_STATUS_FINALIZED}.
1367

1368
    @type job_id: string
1369
    @param job_id: job id to watch
1370
    @type period: int
1371
    @param period: how often to poll for status (optional, default 5s)
1372
    @type retries: int
1373
    @param retries: how many time to poll before giving up
1374
                    (optional, default -1 means unlimited)
1375

1376
    @rtype: bool
1377
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1378
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1379
      possible; L{WaitForJobChange} returns immediately after a job changed and
1380
      does not use polling
1381

1382
    """
1383
    while retries != 0:
1384
      job_result = self.GetJobStatus(job_id)
1385

    
1386
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1387
        return True
1388
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1389
        return False
1390

    
1391
      if period:
1392
        time.sleep(period)
1393

    
1394
      if retries > 0:
1395
        retries -= 1
1396

    
1397
    return False
1398

    
1399
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1400
    """Waits for job changes.
1401

1402
    @type job_id: string
1403
    @param job_id: Job ID for which to wait
1404
    @return: C{None} if no changes have been detected and a dict with two keys,
1405
      C{job_info} and C{log_entries} otherwise.
1406
    @rtype: dict
1407

1408
    """
1409
    body = {
1410
      "fields": fields,
1411
      "previous_job_info": prev_job_info,
1412
      "previous_log_serial": prev_log_serial,
1413
      }
1414

    
1415
    return self._SendRequest(HTTP_GET,
1416
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1417
                             None, body)
1418

    
1419
  def CancelJob(self, job_id, dry_run=False):
1420
    """Cancels a job.
1421

1422
    @type job_id: string
1423
    @param job_id: id of the job to delete
1424
    @type dry_run: bool
1425
    @param dry_run: whether to perform a dry run
1426
    @rtype: tuple
1427
    @return: tuple containing the result, and a message (bool, string)
1428

1429
    """
1430
    query = []
1431
    _AppendDryRunIf(query, dry_run)
1432

    
1433
    return self._SendRequest(HTTP_DELETE,
1434
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1435
                             query, None)
1436

    
1437
  def GetNodes(self, bulk=False):
1438
    """Gets all nodes in the cluster.
1439

1440
    @type bulk: bool
1441
    @param bulk: whether to return all information about all instances
1442

1443
    @rtype: list of dict or str
1444
    @return: if bulk is true, info about nodes in the cluster,
1445
        else list of nodes in the cluster
1446

1447
    """
1448
    query = []
1449
    _AppendIf(query, bulk, ("bulk", 1))
1450

    
1451
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1452
                              query, None)
1453
    if bulk:
1454
      return nodes
1455
    else:
1456
      return [n["id"] for n in nodes]
1457

    
1458
  def GetNode(self, node):
1459
    """Gets information about a node.
1460

1461
    @type node: str
1462
    @param node: node whose info to return
1463

1464
    @rtype: dict
1465
    @return: info about the node
1466

1467
    """
1468
    return self._SendRequest(HTTP_GET,
1469
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1470
                             None, None)
1471

    
1472
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1473
                   dry_run=False, early_release=None,
1474
                   mode=None, accept_old=False):
1475
    """Evacuates instances from a Ganeti node.
1476

1477
    @type node: str
1478
    @param node: node to evacuate
1479
    @type iallocator: str or None
1480
    @param iallocator: instance allocator to use
1481
    @type remote_node: str
1482
    @param remote_node: node to evaucate to
1483
    @type dry_run: bool
1484
    @param dry_run: whether to perform a dry run
1485
    @type early_release: bool
1486
    @param early_release: whether to enable parallelization
1487
    @type mode: string
1488
    @param mode: Node evacuation mode
1489
    @type accept_old: bool
1490
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1491
        results
1492

1493
    @rtype: string, or a list for pre-2.5 results
1494
    @return: Job ID or, if C{accept_old} is set and server is pre-2.5,
1495
      list of (job ID, instance name, new secondary node); if dry_run was
1496
      specified, then the actual move jobs were not submitted and the job IDs
1497
      will be C{None}
1498

1499
    @raises GanetiApiError: if an iallocator and remote_node are both
1500
        specified
1501

1502
    """
1503
    if iallocator and remote_node:
1504
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1505

    
1506
    query = []
1507
    _AppendDryRunIf(query, dry_run)
1508

    
1509
    if _NODE_EVAC_RES1 in self.GetFeatures():
1510
      # Server supports body parameters
1511
      body = {}
1512

    
1513
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1514
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1515
      _SetItemIf(body, early_release is not None,
1516
                 "early_release", early_release)
1517
      _SetItemIf(body, mode is not None, "mode", mode)
1518
    else:
1519
      # Pre-2.5 request format
1520
      body = None
1521

    
1522
      if not accept_old:
1523
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1524
                             " not accept old-style results (parameter"
1525
                             " accept_old)")
1526

    
1527
      # Pre-2.5 servers can only evacuate secondaries
1528
      if mode is not None and mode != NODE_EVAC_SEC:
1529
        raise GanetiApiError("Server can only evacuate secondary instances")
1530

    
1531
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1532
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1533
      _AppendIf(query, early_release, ("early_release", 1))
1534

    
1535
    return self._SendRequest(HTTP_POST,
1536
                             ("/%s/nodes/%s/evacuate" %
1537
                              (GANETI_RAPI_VERSION, node)), query, body)
1538

    
1539
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1540
                  target_node=None):
1541
    """Migrates all primary instances from a node.
1542

1543
    @type node: str
1544
    @param node: node to migrate
1545
    @type mode: string
1546
    @param mode: if passed, it will overwrite the live migration type,
1547
        otherwise the hypervisor default will be used
1548
    @type dry_run: bool
1549
    @param dry_run: whether to perform a dry run
1550
    @type iallocator: string
1551
    @param iallocator: instance allocator to use
1552
    @type target_node: string
1553
    @param target_node: Target node for shared-storage instances
1554

1555
    @rtype: string
1556
    @return: job id
1557

1558
    """
1559
    query = []
1560
    _AppendDryRunIf(query, dry_run)
1561

    
1562
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1563
      body = {}
1564

    
1565
      _SetItemIf(body, mode is not None, "mode", mode)
1566
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1567
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1568

    
1569
      assert len(query) <= 1
1570

    
1571
      return self._SendRequest(HTTP_POST,
1572
                               ("/%s/nodes/%s/migrate" %
1573
                                (GANETI_RAPI_VERSION, node)), query, body)
1574
    else:
1575
      # Use old request format
1576
      if target_node is not None:
1577
        raise GanetiApiError("Server does not support specifying target node"
1578
                             " for node migration")
1579

    
1580
      _AppendIf(query, mode is not None, ("mode", mode))
1581

    
1582
      return self._SendRequest(HTTP_POST,
1583
                               ("/%s/nodes/%s/migrate" %
1584
                                (GANETI_RAPI_VERSION, node)), query, None)
1585

    
1586
  def GetNodeRole(self, node):
1587
    """Gets the current role for a node.
1588

1589
    @type node: str
1590
    @param node: node whose role to return
1591

1592
    @rtype: str
1593
    @return: the current role for a node
1594

1595
    """
1596
    return self._SendRequest(HTTP_GET,
1597
                             ("/%s/nodes/%s/role" %
1598
                              (GANETI_RAPI_VERSION, node)), None, None)
1599

    
1600
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1601
    """Sets the role for a node.
1602

1603
    @type node: str
1604
    @param node: the node whose role to set
1605
    @type role: str
1606
    @param role: the role to set for the node
1607
    @type force: bool
1608
    @param force: whether to force the role change
1609
    @type auto_promote: bool
1610
    @param auto_promote: Whether node(s) should be promoted to master candidate
1611
                         if necessary
1612

1613
    @rtype: string
1614
    @return: job id
1615

1616
    """
1617
    query = []
1618
    _AppendForceIf(query, force)
1619
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1620

    
1621
    return self._SendRequest(HTTP_PUT,
1622
                             ("/%s/nodes/%s/role" %
1623
                              (GANETI_RAPI_VERSION, node)), query, role)
1624

    
1625
  def PowercycleNode(self, node, force=False):
1626
    """Powercycles a node.
1627

1628
    @type node: string
1629
    @param node: Node name
1630
    @type force: bool
1631
    @param force: Whether to force the operation
1632
    @rtype: string
1633
    @return: job id
1634

1635
    """
1636
    query = []
1637
    _AppendForceIf(query, force)
1638

    
1639
    return self._SendRequest(HTTP_POST,
1640
                             ("/%s/nodes/%s/powercycle" %
1641
                              (GANETI_RAPI_VERSION, node)), query, None)
1642

    
1643
  def ModifyNode(self, node, **kwargs):
1644
    """Modifies a node.
1645

1646
    More details for parameters can be found in the RAPI documentation.
1647

1648
    @type node: string
1649
    @param node: Node name
1650
    @rtype: string
1651
    @return: job id
1652

1653
    """
1654
    return self._SendRequest(HTTP_POST,
1655
                             ("/%s/nodes/%s/modify" %
1656
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1657

    
1658
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1659
    """Gets the storage units for a node.
1660

1661
    @type node: str
1662
    @param node: the node whose storage units to return
1663
    @type storage_type: str
1664
    @param storage_type: storage type whose units to return
1665
    @type output_fields: str
1666
    @param output_fields: storage type fields to return
1667

1668
    @rtype: string
1669
    @return: job id where results can be retrieved
1670

1671
    """
1672
    query = [
1673
      ("storage_type", storage_type),
1674
      ("output_fields", output_fields),
1675
      ]
1676

    
1677
    return self._SendRequest(HTTP_GET,
1678
                             ("/%s/nodes/%s/storage" %
1679
                              (GANETI_RAPI_VERSION, node)), query, None)
1680

    
1681
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1682
    """Modifies parameters of storage units on the node.
1683

1684
    @type node: str
1685
    @param node: node whose storage units to modify
1686
    @type storage_type: str
1687
    @param storage_type: storage type whose units to modify
1688
    @type name: str
1689
    @param name: name of the storage unit
1690
    @type allocatable: bool or None
1691
    @param allocatable: Whether to set the "allocatable" flag on the storage
1692
                        unit (None=no modification, True=set, False=unset)
1693

1694
    @rtype: string
1695
    @return: job id
1696

1697
    """
1698
    query = [
1699
      ("storage_type", storage_type),
1700
      ("name", name),
1701
      ]
1702

    
1703
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1704

    
1705
    return self._SendRequest(HTTP_PUT,
1706
                             ("/%s/nodes/%s/storage/modify" %
1707
                              (GANETI_RAPI_VERSION, node)), query, None)
1708

    
1709
  def RepairNodeStorageUnits(self, node, storage_type, name):
1710
    """Repairs a storage unit on the node.
1711

1712
    @type node: str
1713
    @param node: node whose storage units to repair
1714
    @type storage_type: str
1715
    @param storage_type: storage type to repair
1716
    @type name: str
1717
    @param name: name of the storage unit to repair
1718

1719
    @rtype: string
1720
    @return: job id
1721

1722
    """
1723
    query = [
1724
      ("storage_type", storage_type),
1725
      ("name", name),
1726
      ]
1727

    
1728
    return self._SendRequest(HTTP_PUT,
1729
                             ("/%s/nodes/%s/storage/repair" %
1730
                              (GANETI_RAPI_VERSION, node)), query, None)
1731

    
1732
  def GetNodeTags(self, node):
1733
    """Gets the tags for a node.
1734

1735
    @type node: str
1736
    @param node: node whose tags to return
1737

1738
    @rtype: list of str
1739
    @return: tags for the node
1740

1741
    """
1742
    return self._SendRequest(HTTP_GET,
1743
                             ("/%s/nodes/%s/tags" %
1744
                              (GANETI_RAPI_VERSION, node)), None, None)
1745

    
1746
  def AddNodeTags(self, node, tags, dry_run=False):
1747
    """Adds tags to a node.
1748

1749
    @type node: str
1750
    @param node: node to add tags to
1751
    @type tags: list of str
1752
    @param tags: tags to add to the node
1753
    @type dry_run: bool
1754
    @param dry_run: whether to perform a dry run
1755

1756
    @rtype: string
1757
    @return: job id
1758

1759
    """
1760
    query = [("tag", t) for t in tags]
1761
    _AppendDryRunIf(query, dry_run)
1762

    
1763
    return self._SendRequest(HTTP_PUT,
1764
                             ("/%s/nodes/%s/tags" %
1765
                              (GANETI_RAPI_VERSION, node)), query, tags)
1766

    
1767
  def DeleteNodeTags(self, node, tags, dry_run=False):
1768
    """Delete tags from a node.
1769

1770
    @type node: str
1771
    @param node: node to remove tags from
1772
    @type tags: list of str
1773
    @param tags: tags to remove from the node
1774
    @type dry_run: bool
1775
    @param dry_run: whether to perform a dry run
1776

1777
    @rtype: string
1778
    @return: job id
1779

1780
    """
1781
    query = [("tag", t) for t in tags]
1782
    _AppendDryRunIf(query, dry_run)
1783

    
1784
    return self._SendRequest(HTTP_DELETE,
1785
                             ("/%s/nodes/%s/tags" %
1786
                              (GANETI_RAPI_VERSION, node)), query, None)
1787

    
1788
  def GetNetworks(self, bulk=False):
1789
    """Gets all networks in the cluster.
1790

1791
    @type bulk: bool
1792
    @param bulk: whether to return all information about the networks
1793

1794
    @rtype: list of dict or str
1795
    @return: if bulk is true, a list of dictionaries with info about all
1796
        networks in the cluster, else a list of names of those networks
1797

1798
    """
1799
    query = []
1800
    _AppendIf(query, bulk, ("bulk", 1))
1801

    
1802
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1803
                                 query, None)
1804
    if bulk:
1805
      return networks
1806
    else:
1807
      return [n["name"] for n in networks]
1808

    
1809
  def GetNetwork(self, network):
1810
    """Gets information about a network.
1811

1812
    @type network: str
1813
    @param network: name of the network whose info to return
1814

1815
    @rtype: dict
1816
    @return: info about the network
1817

1818
    """
1819
    return self._SendRequest(HTTP_GET,
1820
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1821
                             None, None)
1822

    
1823
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1824
                    gateway6=None, mac_prefix=None,
1825
                    add_reserved_ips=None, tags=None, dry_run=False):
1826
    """Creates a new network.
1827

1828
    @type network_name: str
1829
    @param network_name: the name of network to create
1830
    @type dry_run: bool
1831
    @param dry_run: whether to peform a dry run
1832

1833
    @rtype: string
1834
    @return: job id
1835

1836
    """
1837
    query = []
1838
    _AppendDryRunIf(query, dry_run)
1839

    
1840
    if add_reserved_ips:
1841
      add_reserved_ips = add_reserved_ips.split(",")
1842

    
1843
    if tags:
1844
      tags = tags.split(",")
1845

    
1846
    body = {
1847
      "network_name": network_name,
1848
      "gateway": gateway,
1849
      "network": network,
1850
      "gateway6": gateway6,
1851
      "network6": network6,
1852
      "mac_prefix": mac_prefix,
1853
      "add_reserved_ips": add_reserved_ips,
1854
      "tags": tags,
1855
      }
1856

    
1857
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1858
                             query, body)
1859

    
1860
  def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
1861
    """Connects a Network to a NodeGroup with the given netparams
1862

1863
    """
1864
    body = {
1865
      "group_name": group_name,
1866
      "network_mode": mode,
1867
      "network_link": link,
1868
      }
1869

    
1870
    query = []
1871
    _AppendDryRunIf(query, dry_run)
1872

    
1873
    return self._SendRequest(HTTP_PUT,
1874
                             ("/%s/networks/%s/connect" %
1875
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1876

    
1877
  def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1878
    """Connects a Network to a NodeGroup with the given netparams
1879

1880
    """
1881
    body = {
1882
      "group_name": group_name,
1883
      }
1884

    
1885
    query = []
1886
    _AppendDryRunIf(query, dry_run)
1887

    
1888
    return self._SendRequest(HTTP_PUT,
1889
                             ("/%s/networks/%s/disconnect" %
1890
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1891

    
1892
  def ModifyNetwork(self, network, **kwargs):
1893
    """Modifies a network.
1894

1895
    More details for parameters can be found in the RAPI documentation.
1896

1897
    @type network: string
1898
    @param network: Network name
1899
    @rtype: string
1900
    @return: job id
1901

1902
    """
1903
    return self._SendRequest(HTTP_PUT,
1904
                             ("/%s/networks/%s/modify" %
1905
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
1906

    
1907
  def DeleteNetwork(self, network, dry_run=False):
1908
    """Deletes a network.
1909

1910
    @type network: str
1911
    @param network: the network to delete
1912
    @type dry_run: bool
1913
    @param dry_run: whether to peform a dry run
1914

1915
    @rtype: string
1916
    @return: job id
1917

1918
    """
1919
    query = []
1920
    _AppendDryRunIf(query, dry_run)
1921

    
1922
    return self._SendRequest(HTTP_DELETE,
1923
                             ("/%s/networks/%s" %
1924
                              (GANETI_RAPI_VERSION, network)), query, None)
1925

    
1926
  def GetNetworkTags(self, network):
1927
    """Gets tags for a network.
1928

1929
    @type network: string
1930
    @param network: Node group whose tags to return
1931

1932
    @rtype: list of strings
1933
    @return: tags for the network
1934

1935
    """
1936
    return self._SendRequest(HTTP_GET,
1937
                             ("/%s/networks/%s/tags" %
1938
                              (GANETI_RAPI_VERSION, network)), None, None)
1939

    
1940
  def AddNetworkTags(self, network, tags, dry_run=False):
1941
    """Adds tags to a network.
1942

1943
    @type network: str
1944
    @param network: network to add tags to
1945
    @type tags: list of string
1946
    @param tags: tags to add to the network
1947
    @type dry_run: bool
1948
    @param dry_run: whether to perform a dry run
1949

1950
    @rtype: string
1951
    @return: job id
1952

1953
    """
1954
    query = [("tag", t) for t in tags]
1955
    _AppendDryRunIf(query, dry_run)
1956

    
1957
    return self._SendRequest(HTTP_PUT,
1958
                             ("/%s/networks/%s/tags" %
1959
                              (GANETI_RAPI_VERSION, network)), query, None)
1960

    
1961
  def DeleteNetworkTags(self, network, tags, dry_run=False):
1962
    """Deletes tags from a network.
1963

1964
    @type network: str
1965
    @param network: network to delete tags from
1966
    @type tags: list of string
1967
    @param tags: tags to delete
1968
    @type dry_run: bool
1969
    @param dry_run: whether to perform a dry run
1970
    @rtype: string
1971
    @return: job id
1972

1973
    """
1974
    query = [("tag", t) for t in tags]
1975
    _AppendDryRunIf(query, dry_run)
1976

    
1977
    return self._SendRequest(HTTP_DELETE,
1978
                             ("/%s/networks/%s/tags" %
1979
                              (GANETI_RAPI_VERSION, network)), query, None)
1980

    
1981
  def GetGroups(self, bulk=False):
1982
    """Gets all node groups in the cluster.
1983

1984
    @type bulk: bool
1985
    @param bulk: whether to return all information about the groups
1986

1987
    @rtype: list of dict or str
1988
    @return: if bulk is true, a list of dictionaries with info about all node
1989
        groups in the cluster, else a list of names of those node groups
1990

1991
    """
1992
    query = []
1993
    _AppendIf(query, bulk, ("bulk", 1))
1994

    
1995
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1996
                               query, None)
1997
    if bulk:
1998
      return groups
1999
    else:
2000
      return [g["name"] for g in groups]
2001

    
2002
  def GetGroup(self, group):
2003
    """Gets information about a node group.
2004

2005
    @type group: str
2006
    @param group: name of the node group whose info to return
2007

2008
    @rtype: dict
2009
    @return: info about the node group
2010

2011
    """
2012
    return self._SendRequest(HTTP_GET,
2013
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2014
                             None, None)
2015

    
2016
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
2017
    """Creates a new node group.
2018

2019
    @type name: str
2020
    @param name: the name of node group to create
2021
    @type alloc_policy: str
2022
    @param alloc_policy: the desired allocation policy for the group, if any
2023
    @type dry_run: bool
2024
    @param dry_run: whether to peform a dry run
2025

2026
    @rtype: string
2027
    @return: job id
2028

2029
    """
2030
    query = []
2031
    _AppendDryRunIf(query, dry_run)
2032

    
2033
    body = {
2034
      "name": name,
2035
      "alloc_policy": alloc_policy,
2036
      }
2037

    
2038
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2039
                             query, body)
2040

    
2041
  def ModifyGroup(self, group, **kwargs):
2042
    """Modifies a node group.
2043

2044
    More details for parameters can be found in the RAPI documentation.
2045

2046
    @type group: string
2047
    @param group: Node group name
2048
    @rtype: string
2049
    @return: job id
2050

2051
    """
2052
    return self._SendRequest(HTTP_PUT,
2053
                             ("/%s/groups/%s/modify" %
2054
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
2055

    
2056
  def DeleteGroup(self, group, dry_run=False):
2057
    """Deletes a node group.
2058

2059
    @type group: str
2060
    @param group: the node group to delete
2061
    @type dry_run: bool
2062
    @param dry_run: whether to peform a dry run
2063

2064
    @rtype: string
2065
    @return: job id
2066

2067
    """
2068
    query = []
2069
    _AppendDryRunIf(query, dry_run)
2070

    
2071
    return self._SendRequest(HTTP_DELETE,
2072
                             ("/%s/groups/%s" %
2073
                              (GANETI_RAPI_VERSION, group)), query, None)
2074

    
2075
  def RenameGroup(self, group, new_name):
2076
    """Changes the name of a node group.
2077

2078
    @type group: string
2079
    @param group: Node group name
2080
    @type new_name: string
2081
    @param new_name: New node group name
2082

2083
    @rtype: string
2084
    @return: job id
2085

2086
    """
2087
    body = {
2088
      "new_name": new_name,
2089
      }
2090

    
2091
    return self._SendRequest(HTTP_PUT,
2092
                             ("/%s/groups/%s/rename" %
2093
                              (GANETI_RAPI_VERSION, group)), None, body)
2094

    
2095
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2096
    """Assigns nodes to a group.
2097

2098
    @type group: string
2099
    @param group: Node group name
2100
    @type nodes: list of strings
2101
    @param nodes: List of nodes to assign to the group
2102

2103
    @rtype: string
2104
    @return: job id
2105

2106
    """
2107
    query = []
2108
    _AppendForceIf(query, force)
2109
    _AppendDryRunIf(query, dry_run)
2110

    
2111
    body = {
2112
      "nodes": nodes,
2113
      }
2114

    
2115
    return self._SendRequest(HTTP_PUT,
2116
                             ("/%s/groups/%s/assign-nodes" %
2117
                             (GANETI_RAPI_VERSION, group)), query, body)
2118

    
2119
  def GetGroupTags(self, group):
2120
    """Gets tags for a node group.
2121

2122
    @type group: string
2123
    @param group: Node group whose tags to return
2124

2125
    @rtype: list of strings
2126
    @return: tags for the group
2127

2128
    """
2129
    return self._SendRequest(HTTP_GET,
2130
                             ("/%s/groups/%s/tags" %
2131
                              (GANETI_RAPI_VERSION, group)), None, None)
2132

    
2133
  def AddGroupTags(self, group, tags, dry_run=False):
2134
    """Adds tags to a node group.
2135

2136
    @type group: str
2137
    @param group: group to add tags to
2138
    @type tags: list of string
2139
    @param tags: tags to add to the group
2140
    @type dry_run: bool
2141
    @param dry_run: whether to perform a dry run
2142

2143
    @rtype: string
2144
    @return: job id
2145

2146
    """
2147
    query = [("tag", t) for t in tags]
2148
    _AppendDryRunIf(query, dry_run)
2149

    
2150
    return self._SendRequest(HTTP_PUT,
2151
                             ("/%s/groups/%s/tags" %
2152
                              (GANETI_RAPI_VERSION, group)), query, None)
2153

    
2154
  def DeleteGroupTags(self, group, tags, dry_run=False):
2155
    """Deletes tags from a node group.
2156

2157
    @type group: str
2158
    @param group: group to delete tags from
2159
    @type tags: list of string
2160
    @param tags: tags to delete
2161
    @type dry_run: bool
2162
    @param dry_run: whether to perform a dry run
2163
    @rtype: string
2164
    @return: job id
2165

2166
    """
2167
    query = [("tag", t) for t in tags]
2168
    _AppendDryRunIf(query, dry_run)
2169

    
2170
    return self._SendRequest(HTTP_DELETE,
2171
                             ("/%s/groups/%s/tags" %
2172
                              (GANETI_RAPI_VERSION, group)), query, None)
2173

    
2174
  def Query(self, what, fields, qfilter=None):
2175
    """Retrieves information about resources.
2176

2177
    @type what: string
2178
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2179
    @type fields: list of string
2180
    @param fields: Requested fields
2181
    @type qfilter: None or list
2182
    @param qfilter: Query filter
2183

2184
    @rtype: string
2185
    @return: job id
2186

2187
    """
2188
    body = {
2189
      "fields": fields,
2190
      }
2191

    
2192
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2193
    # TODO: remove "filter" after 2.7
2194
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
2195

    
2196
    return self._SendRequest(HTTP_PUT,
2197
                             ("/%s/query/%s" %
2198
                              (GANETI_RAPI_VERSION, what)), None, body)
2199

    
2200
  def QueryFields(self, what, fields=None):
2201
    """Retrieves available fields for a resource.
2202

2203
    @type what: string
2204
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2205
    @type fields: list of string
2206
    @param fields: Requested fields
2207

2208
    @rtype: string
2209
    @return: job id
2210

2211
    """
2212
    query = []
2213

    
2214
    if fields is not None:
2215
      _AppendIf(query, True, ("fields", ",".join(fields)))
2216

    
2217
    return self._SendRequest(HTTP_GET,
2218
                             ("/%s/query/%s/fields" %
2219
                              (GANETI_RAPI_VERSION, what)), query, None)