Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 06c2fb4a

History | View | Annotate | Download (64.1 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 SnapshotInstance(self, instance, **kwargs):
871
    """Takes snapshot of instance's disks.
872

873
    More details for parameters can be found in the RAPI documentation.
874

875
    @type instance: string
876
    @param instance: Instance name
877
    @rtype: string
878
    @return: job id
879

880
    """
881
    body = kwargs
882

    
883
    return self._SendRequest(HTTP_PUT,
884
                             ("/%s/instances/%s/snapshot" %
885
                              (GANETI_RAPI_VERSION, instance)), None, body)
886

    
887
  def ActivateInstanceDisks(self, instance, ignore_size=None):
888
    """Activates an instance's disks.
889

890
    @type instance: string
891
    @param instance: Instance name
892
    @type ignore_size: bool
893
    @param ignore_size: Whether to ignore recorded size
894
    @rtype: string
895
    @return: job id
896

897
    """
898
    query = []
899
    _AppendIf(query, ignore_size, ("ignore_size", 1))
900

    
901
    return self._SendRequest(HTTP_PUT,
902
                             ("/%s/instances/%s/activate-disks" %
903
                              (GANETI_RAPI_VERSION, instance)), query, None)
904

    
905
  def DeactivateInstanceDisks(self, instance):
906
    """Deactivates an instance's disks.
907

908
    @type instance: string
909
    @param instance: Instance name
910
    @rtype: string
911
    @return: job id
912

913
    """
914
    return self._SendRequest(HTTP_PUT,
915
                             ("/%s/instances/%s/deactivate-disks" %
916
                              (GANETI_RAPI_VERSION, instance)), None, None)
917

    
918
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
919
    """Recreate an instance's disks.
920

921
    @type instance: string
922
    @param instance: Instance name
923
    @type disks: list of int
924
    @param disks: List of disk indexes
925
    @type nodes: list of string
926
    @param nodes: New instance nodes, if relocation is desired
927
    @rtype: string
928
    @return: job id
929

930
    """
931
    body = {}
932
    _SetItemIf(body, disks is not None, "disks", disks)
933
    _SetItemIf(body, nodes is not None, "nodes", nodes)
934

    
935
    return self._SendRequest(HTTP_POST,
936
                             ("/%s/instances/%s/recreate-disks" %
937
                              (GANETI_RAPI_VERSION, instance)), None, body)
938

    
939
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
940
    """Grows a disk of an instance.
941

942
    More details for parameters can be found in the RAPI documentation.
943

944
    @type instance: string
945
    @param instance: Instance name
946
    @type disk: integer
947
    @param disk: Disk index
948
    @type amount: integer
949
    @param amount: Grow disk by this amount (MiB)
950
    @type wait_for_sync: bool
951
    @param wait_for_sync: Wait for disk to synchronize
952
    @rtype: string
953
    @return: job id
954

955
    """
956
    body = {
957
      "amount": amount,
958
      }
959

    
960
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
961

    
962
    return self._SendRequest(HTTP_POST,
963
                             ("/%s/instances/%s/disk/%s/grow" %
964
                              (GANETI_RAPI_VERSION, instance, disk)),
965
                             None, body)
966

    
967
  def GetInstanceTags(self, instance):
968
    """Gets tags for an instance.
969

970
    @type instance: str
971
    @param instance: instance whose tags to return
972

973
    @rtype: list of str
974
    @return: tags for the instance
975

976
    """
977
    return self._SendRequest(HTTP_GET,
978
                             ("/%s/instances/%s/tags" %
979
                              (GANETI_RAPI_VERSION, instance)), None, None)
980

    
981
  def AddInstanceTags(self, instance, tags, dry_run=False):
982
    """Adds tags to an instance.
983

984
    @type instance: str
985
    @param instance: instance to add tags to
986
    @type tags: list of str
987
    @param tags: tags to add to the instance
988
    @type dry_run: bool
989
    @param dry_run: whether to perform a dry run
990

991
    @rtype: string
992
    @return: job id
993

994
    """
995
    query = [("tag", t) for t in tags]
996
    _AppendDryRunIf(query, dry_run)
997

    
998
    return self._SendRequest(HTTP_PUT,
999
                             ("/%s/instances/%s/tags" %
1000
                              (GANETI_RAPI_VERSION, instance)), query, None)
1001

    
1002
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
1003
    """Deletes tags from an instance.
1004

1005
    @type instance: str
1006
    @param instance: instance to delete tags from
1007
    @type tags: list of str
1008
    @param tags: tags to delete
1009
    @type dry_run: bool
1010
    @param dry_run: whether to perform a dry run
1011
    @rtype: string
1012
    @return: job id
1013

1014
    """
1015
    query = [("tag", t) for t in tags]
1016
    _AppendDryRunIf(query, dry_run)
1017

    
1018
    return self._SendRequest(HTTP_DELETE,
1019
                             ("/%s/instances/%s/tags" %
1020
                              (GANETI_RAPI_VERSION, instance)), query, None)
1021

    
1022
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1023
                     dry_run=False, reason=None, **kwargs):
1024
    """Reboots an instance.
1025

1026
    @type instance: str
1027
    @param instance: instance to reboot
1028
    @type reboot_type: str
1029
    @param reboot_type: one of: hard, soft, full
1030
    @type ignore_secondaries: bool
1031
    @param ignore_secondaries: if True, ignores errors for the secondary node
1032
        while re-assembling disks (in hard-reboot mode only)
1033
    @type dry_run: bool
1034
    @param dry_run: whether to perform a dry run
1035
    @type reason: string
1036
    @param reason: the reason for the reboot
1037
    @rtype: string
1038
    @return: job id
1039

1040
    """
1041
    query = []
1042
    body = kwargs
1043

    
1044
    _AppendDryRunIf(query, dry_run)
1045
    _AppendIf(query, reboot_type, ("type", reboot_type))
1046
    _AppendIf(query, ignore_secondaries is not None,
1047
              ("ignore_secondaries", ignore_secondaries))
1048
    _AppendIf(query, reason, ("reason", reason))
1049

    
1050
    return self._SendRequest(HTTP_POST,
1051
                             ("/%s/instances/%s/reboot" %
1052
                              (GANETI_RAPI_VERSION, instance)), query, body)
1053

    
1054
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1055
                       reason=None, **kwargs):
1056
    """Shuts down an instance.
1057

1058
    @type instance: str
1059
    @param instance: the instance to shut down
1060
    @type dry_run: bool
1061
    @param dry_run: whether to perform a dry run
1062
    @type no_remember: bool
1063
    @param no_remember: if true, will not record the state change
1064
    @type reason: string
1065
    @param reason: the reason for the shutdown
1066
    @rtype: string
1067
    @return: job id
1068

1069
    """
1070
    query = []
1071
    body = kwargs
1072

    
1073
    _AppendDryRunIf(query, dry_run)
1074
    _AppendIf(query, no_remember, ("no_remember", 1))
1075
    _AppendIf(query, reason, ("reason", reason))
1076

    
1077
    return self._SendRequest(HTTP_PUT,
1078
                             ("/%s/instances/%s/shutdown" %
1079
                              (GANETI_RAPI_VERSION, instance)), query, body)
1080

    
1081
  def StartupInstance(self, instance, dry_run=False, no_remember=False,
1082
                      reason=None):
1083
    """Starts up an instance.
1084

1085
    @type instance: str
1086
    @param instance: the instance to start up
1087
    @type dry_run: bool
1088
    @param dry_run: whether to perform a dry run
1089
    @type no_remember: bool
1090
    @param no_remember: if true, will not record the state change
1091
    @type reason: string
1092
    @param reason: the reason for the startup
1093
    @rtype: string
1094
    @return: job id
1095

1096
    """
1097
    query = []
1098
    _AppendDryRunIf(query, dry_run)
1099
    _AppendIf(query, no_remember, ("no_remember", 1))
1100
    _AppendIf(query, reason, ("reason", reason))
1101

    
1102
    return self._SendRequest(HTTP_PUT,
1103
                             ("/%s/instances/%s/startup" %
1104
                              (GANETI_RAPI_VERSION, instance)), query, None)
1105

    
1106
  def ReinstallInstance(self, instance, os=None, no_startup=False,
1107
                        osparams=None):
1108
    """Reinstalls an instance.
1109

1110
    @type instance: str
1111
    @param instance: The instance to reinstall
1112
    @type os: str or None
1113
    @param os: The operating system to reinstall. If None, the instance's
1114
        current operating system will be installed again
1115
    @type no_startup: bool
1116
    @param no_startup: Whether to start the instance automatically
1117
    @rtype: string
1118
    @return: job id
1119

1120
    """
1121
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
1122
      body = {
1123
        "start": not no_startup,
1124
        }
1125
      _SetItemIf(body, os is not None, "os", os)
1126
      _SetItemIf(body, osparams is not None, "osparams", osparams)
1127
      return self._SendRequest(HTTP_POST,
1128
                               ("/%s/instances/%s/reinstall" %
1129
                                (GANETI_RAPI_VERSION, instance)), None, body)
1130

    
1131
    # Use old request format
1132
    if osparams:
1133
      raise GanetiApiError("Server does not support specifying OS parameters"
1134
                           " for instance reinstallation")
1135

    
1136
    query = []
1137
    _AppendIf(query, os, ("os", os))
1138
    _AppendIf(query, no_startup, ("nostartup", 1))
1139

    
1140
    return self._SendRequest(HTTP_POST,
1141
                             ("/%s/instances/%s/reinstall" %
1142
                              (GANETI_RAPI_VERSION, instance)), query, None)
1143

    
1144
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1145
                           remote_node=None, iallocator=None):
1146
    """Replaces disks on an instance.
1147

1148
    @type instance: str
1149
    @param instance: instance whose disks to replace
1150
    @type disks: list of ints
1151
    @param disks: Indexes of disks to replace
1152
    @type mode: str
1153
    @param mode: replacement mode to use (defaults to replace_auto)
1154
    @type remote_node: str or None
1155
    @param remote_node: new secondary node to use (for use with
1156
        replace_new_secondary mode)
1157
    @type iallocator: str or None
1158
    @param iallocator: instance allocator plugin to use (for use with
1159
                       replace_auto mode)
1160

1161
    @rtype: string
1162
    @return: job id
1163

1164
    """
1165
    query = [
1166
      ("mode", mode),
1167
      ]
1168

    
1169
    # TODO: Convert to body parameters
1170

    
1171
    if disks is not None:
1172
      _AppendIf(query, True,
1173
                ("disks", ",".join(str(idx) for idx in disks)))
1174

    
1175
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1176
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1177

    
1178
    return self._SendRequest(HTTP_POST,
1179
                             ("/%s/instances/%s/replace-disks" %
1180
                              (GANETI_RAPI_VERSION, instance)), query, None)
1181

    
1182
  def PrepareExport(self, instance, mode):
1183
    """Prepares an instance for an export.
1184

1185
    @type instance: string
1186
    @param instance: Instance name
1187
    @type mode: string
1188
    @param mode: Export mode
1189
    @rtype: string
1190
    @return: Job ID
1191

1192
    """
1193
    query = [("mode", mode)]
1194
    return self._SendRequest(HTTP_PUT,
1195
                             ("/%s/instances/%s/prepare-export" %
1196
                              (GANETI_RAPI_VERSION, instance)), query, None)
1197

    
1198
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1199
                     remove_instance=None,
1200
                     x509_key_name=None, destination_x509_ca=None):
1201
    """Exports an instance.
1202

1203
    @type instance: string
1204
    @param instance: Instance name
1205
    @type mode: string
1206
    @param mode: Export mode
1207
    @rtype: string
1208
    @return: Job ID
1209

1210
    """
1211
    body = {
1212
      "destination": destination,
1213
      "mode": mode,
1214
      }
1215

    
1216
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1217
    _SetItemIf(body, remove_instance is not None,
1218
               "remove_instance", remove_instance)
1219
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1220
    _SetItemIf(body, destination_x509_ca is not None,
1221
               "destination_x509_ca", destination_x509_ca)
1222

    
1223
    return self._SendRequest(HTTP_PUT,
1224
                             ("/%s/instances/%s/export" %
1225
                              (GANETI_RAPI_VERSION, instance)), None, body)
1226

    
1227
  def MigrateInstance(self, instance, mode=None, cleanup=None,
1228
                      target_node=None):
1229
    """Migrates an instance.
1230

1231
    @type instance: string
1232
    @param instance: Instance name
1233
    @type mode: string
1234
    @param mode: Migration mode
1235
    @type cleanup: bool
1236
    @param cleanup: Whether to clean up a previously failed migration
1237
    @type target_node: string
1238
    @param target_node: Target Node for externally mirrored instances
1239
    @rtype: string
1240
    @return: job id
1241

1242
    """
1243
    body = {}
1244
    _SetItemIf(body, mode is not None, "mode", mode)
1245
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1246
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1247

    
1248
    return self._SendRequest(HTTP_PUT,
1249
                             ("/%s/instances/%s/migrate" %
1250
                              (GANETI_RAPI_VERSION, instance)), None, body)
1251

    
1252
  def FailoverInstance(self, instance, iallocator=None,
1253
                       ignore_consistency=None, target_node=None):
1254
    """Does a failover of an instance.
1255

1256
    @type instance: string
1257
    @param instance: Instance name
1258
    @type iallocator: string
1259
    @param iallocator: Iallocator for deciding the target node for
1260
      shared-storage instances
1261
    @type ignore_consistency: bool
1262
    @param ignore_consistency: Whether to ignore disk consistency
1263
    @type target_node: string
1264
    @param target_node: Target node for shared-storage instances
1265
    @rtype: string
1266
    @return: job id
1267

1268
    """
1269
    body = {}
1270
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1271
    _SetItemIf(body, ignore_consistency is not None,
1272
               "ignore_consistency", ignore_consistency)
1273
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1274

    
1275
    return self._SendRequest(HTTP_PUT,
1276
                             ("/%s/instances/%s/failover" %
1277
                              (GANETI_RAPI_VERSION, instance)), None, body)
1278

    
1279
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1280
    """Changes the name of an instance.
1281

1282
    @type instance: string
1283
    @param instance: Instance name
1284
    @type new_name: string
1285
    @param new_name: New instance name
1286
    @type ip_check: bool
1287
    @param ip_check: Whether to ensure instance's IP address is inactive
1288
    @type name_check: bool
1289
    @param name_check: Whether to ensure instance's name is resolvable
1290
    @rtype: string
1291
    @return: job id
1292

1293
    """
1294
    body = {
1295
      "new_name": new_name,
1296
      }
1297

    
1298
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1299
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1300

    
1301
    return self._SendRequest(HTTP_PUT,
1302
                             ("/%s/instances/%s/rename" %
1303
                              (GANETI_RAPI_VERSION, instance)), None, body)
1304

    
1305
  def GetInstanceConsole(self, instance):
1306
    """Request information for connecting to instance's console.
1307

1308
    @type instance: string
1309
    @param instance: Instance name
1310
    @rtype: dict
1311
    @return: dictionary containing information about instance's console
1312

1313
    """
1314
    return self._SendRequest(HTTP_GET,
1315
                             ("/%s/instances/%s/console" %
1316
                              (GANETI_RAPI_VERSION, instance)), None, None)
1317

    
1318
  def GetJobs(self, bulk=False):
1319
    """Gets all jobs for the cluster.
1320

1321
    @type bulk: bool
1322
    @param bulk: Whether to return detailed information about jobs.
1323
    @rtype: list of int
1324
    @return: List of job ids for the cluster or list of dicts with detailed
1325
             information about the jobs if bulk parameter was true.
1326

1327
    """
1328
    query = []
1329
    _AppendIf(query, bulk, ("bulk", 1))
1330

    
1331
    if bulk:
1332
      return self._SendRequest(HTTP_GET,
1333
                               "/%s/jobs" % GANETI_RAPI_VERSION,
1334
                               query, None)
1335
    else:
1336
      return [int(j["id"])
1337
              for j in self._SendRequest(HTTP_GET,
1338
                                         "/%s/jobs" % GANETI_RAPI_VERSION,
1339
                                         None, None)]
1340

    
1341
  def GetJobStatus(self, job_id):
1342
    """Gets the status of a job.
1343

1344
    @type job_id: string
1345
    @param job_id: job id whose status to query
1346

1347
    @rtype: dict
1348
    @return: job status
1349

1350
    """
1351
    return self._SendRequest(HTTP_GET,
1352
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1353
                             None, None)
1354

    
1355
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1356
    """Polls cluster for job status until completion.
1357

1358
    Completion is defined as any of the following states listed in
1359
    L{JOB_STATUS_FINALIZED}.
1360

1361
    @type job_id: string
1362
    @param job_id: job id to watch
1363
    @type period: int
1364
    @param period: how often to poll for status (optional, default 5s)
1365
    @type retries: int
1366
    @param retries: how many time to poll before giving up
1367
                    (optional, default -1 means unlimited)
1368

1369
    @rtype: bool
1370
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1371
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1372
      possible; L{WaitForJobChange} returns immediately after a job changed and
1373
      does not use polling
1374

1375
    """
1376
    while retries != 0:
1377
      job_result = self.GetJobStatus(job_id)
1378

    
1379
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1380
        return True
1381
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1382
        return False
1383

    
1384
      if period:
1385
        time.sleep(period)
1386

    
1387
      if retries > 0:
1388
        retries -= 1
1389

    
1390
    return False
1391

    
1392
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1393
    """Waits for job changes.
1394

1395
    @type job_id: string
1396
    @param job_id: Job ID for which to wait
1397
    @return: C{None} if no changes have been detected and a dict with two keys,
1398
      C{job_info} and C{log_entries} otherwise.
1399
    @rtype: dict
1400

1401
    """
1402
    body = {
1403
      "fields": fields,
1404
      "previous_job_info": prev_job_info,
1405
      "previous_log_serial": prev_log_serial,
1406
      }
1407

    
1408
    return self._SendRequest(HTTP_GET,
1409
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1410
                             None, body)
1411

    
1412
  def CancelJob(self, job_id, dry_run=False):
1413
    """Cancels a job.
1414

1415
    @type job_id: string
1416
    @param job_id: id of the job to delete
1417
    @type dry_run: bool
1418
    @param dry_run: whether to perform a dry run
1419
    @rtype: tuple
1420
    @return: tuple containing the result, and a message (bool, string)
1421

1422
    """
1423
    query = []
1424
    _AppendDryRunIf(query, dry_run)
1425

    
1426
    return self._SendRequest(HTTP_DELETE,
1427
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1428
                             query, None)
1429

    
1430
  def GetNodes(self, bulk=False):
1431
    """Gets all nodes in the cluster.
1432

1433
    @type bulk: bool
1434
    @param bulk: whether to return all information about all instances
1435

1436
    @rtype: list of dict or str
1437
    @return: if bulk is true, info about nodes in the cluster,
1438
        else list of nodes in the cluster
1439

1440
    """
1441
    query = []
1442
    _AppendIf(query, bulk, ("bulk", 1))
1443

    
1444
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1445
                              query, None)
1446
    if bulk:
1447
      return nodes
1448
    else:
1449
      return [n["id"] for n in nodes]
1450

    
1451
  def GetNode(self, node):
1452
    """Gets information about a node.
1453

1454
    @type node: str
1455
    @param node: node whose info to return
1456

1457
    @rtype: dict
1458
    @return: info about the node
1459

1460
    """
1461
    return self._SendRequest(HTTP_GET,
1462
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1463
                             None, None)
1464

    
1465
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1466
                   dry_run=False, early_release=None,
1467
                   mode=None, accept_old=False):
1468
    """Evacuates instances from a Ganeti node.
1469

1470
    @type node: str
1471
    @param node: node to evacuate
1472
    @type iallocator: str or None
1473
    @param iallocator: instance allocator to use
1474
    @type remote_node: str
1475
    @param remote_node: node to evaucate to
1476
    @type dry_run: bool
1477
    @param dry_run: whether to perform a dry run
1478
    @type early_release: bool
1479
    @param early_release: whether to enable parallelization
1480
    @type mode: string
1481
    @param mode: Node evacuation mode
1482
    @type accept_old: bool
1483
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1484
        results
1485

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

1492
    @raises GanetiApiError: if an iallocator and remote_node are both
1493
        specified
1494

1495
    """
1496
    if iallocator and remote_node:
1497
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1498

    
1499
    query = []
1500
    _AppendDryRunIf(query, dry_run)
1501

    
1502
    if _NODE_EVAC_RES1 in self.GetFeatures():
1503
      # Server supports body parameters
1504
      body = {}
1505

    
1506
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1507
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1508
      _SetItemIf(body, early_release is not None,
1509
                 "early_release", early_release)
1510
      _SetItemIf(body, mode is not None, "mode", mode)
1511
    else:
1512
      # Pre-2.5 request format
1513
      body = None
1514

    
1515
      if not accept_old:
1516
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1517
                             " not accept old-style results (parameter"
1518
                             " accept_old)")
1519

    
1520
      # Pre-2.5 servers can only evacuate secondaries
1521
      if mode is not None and mode != NODE_EVAC_SEC:
1522
        raise GanetiApiError("Server can only evacuate secondary instances")
1523

    
1524
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1525
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1526
      _AppendIf(query, early_release, ("early_release", 1))
1527

    
1528
    return self._SendRequest(HTTP_POST,
1529
                             ("/%s/nodes/%s/evacuate" %
1530
                              (GANETI_RAPI_VERSION, node)), query, body)
1531

    
1532
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1533
                  target_node=None):
1534
    """Migrates all primary instances from a node.
1535

1536
    @type node: str
1537
    @param node: node to migrate
1538
    @type mode: string
1539
    @param mode: if passed, it will overwrite the live migration type,
1540
        otherwise the hypervisor default will be used
1541
    @type dry_run: bool
1542
    @param dry_run: whether to perform a dry run
1543
    @type iallocator: string
1544
    @param iallocator: instance allocator to use
1545
    @type target_node: string
1546
    @param target_node: Target node for shared-storage instances
1547

1548
    @rtype: string
1549
    @return: job id
1550

1551
    """
1552
    query = []
1553
    _AppendDryRunIf(query, dry_run)
1554

    
1555
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1556
      body = {}
1557

    
1558
      _SetItemIf(body, mode is not None, "mode", mode)
1559
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1560
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1561

    
1562
      assert len(query) <= 1
1563

    
1564
      return self._SendRequest(HTTP_POST,
1565
                               ("/%s/nodes/%s/migrate" %
1566
                                (GANETI_RAPI_VERSION, node)), query, body)
1567
    else:
1568
      # Use old request format
1569
      if target_node is not None:
1570
        raise GanetiApiError("Server does not support specifying target node"
1571
                             " for node migration")
1572

    
1573
      _AppendIf(query, mode is not None, ("mode", mode))
1574

    
1575
      return self._SendRequest(HTTP_POST,
1576
                               ("/%s/nodes/%s/migrate" %
1577
                                (GANETI_RAPI_VERSION, node)), query, None)
1578

    
1579
  def GetNodeRole(self, node):
1580
    """Gets the current role for a node.
1581

1582
    @type node: str
1583
    @param node: node whose role to return
1584

1585
    @rtype: str
1586
    @return: the current role for a node
1587

1588
    """
1589
    return self._SendRequest(HTTP_GET,
1590
                             ("/%s/nodes/%s/role" %
1591
                              (GANETI_RAPI_VERSION, node)), None, None)
1592

    
1593
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1594
    """Sets the role for a node.
1595

1596
    @type node: str
1597
    @param node: the node whose role to set
1598
    @type role: str
1599
    @param role: the role to set for the node
1600
    @type force: bool
1601
    @param force: whether to force the role change
1602
    @type auto_promote: bool
1603
    @param auto_promote: Whether node(s) should be promoted to master candidate
1604
                         if necessary
1605

1606
    @rtype: string
1607
    @return: job id
1608

1609
    """
1610
    query = []
1611
    _AppendForceIf(query, force)
1612
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1613

    
1614
    return self._SendRequest(HTTP_PUT,
1615
                             ("/%s/nodes/%s/role" %
1616
                              (GANETI_RAPI_VERSION, node)), query, role)
1617

    
1618
  def PowercycleNode(self, node, force=False):
1619
    """Powercycles a node.
1620

1621
    @type node: string
1622
    @param node: Node name
1623
    @type force: bool
1624
    @param force: Whether to force the operation
1625
    @rtype: string
1626
    @return: job id
1627

1628
    """
1629
    query = []
1630
    _AppendForceIf(query, force)
1631

    
1632
    return self._SendRequest(HTTP_POST,
1633
                             ("/%s/nodes/%s/powercycle" %
1634
                              (GANETI_RAPI_VERSION, node)), query, None)
1635

    
1636
  def ModifyNode(self, node, **kwargs):
1637
    """Modifies a node.
1638

1639
    More details for parameters can be found in the RAPI documentation.
1640

1641
    @type node: string
1642
    @param node: Node name
1643
    @rtype: string
1644
    @return: job id
1645

1646
    """
1647
    return self._SendRequest(HTTP_POST,
1648
                             ("/%s/nodes/%s/modify" %
1649
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1650

    
1651
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1652
    """Gets the storage units for a node.
1653

1654
    @type node: str
1655
    @param node: the node whose storage units to return
1656
    @type storage_type: str
1657
    @param storage_type: storage type whose units to return
1658
    @type output_fields: str
1659
    @param output_fields: storage type fields to return
1660

1661
    @rtype: string
1662
    @return: job id where results can be retrieved
1663

1664
    """
1665
    query = [
1666
      ("storage_type", storage_type),
1667
      ("output_fields", output_fields),
1668
      ]
1669

    
1670
    return self._SendRequest(HTTP_GET,
1671
                             ("/%s/nodes/%s/storage" %
1672
                              (GANETI_RAPI_VERSION, node)), query, None)
1673

    
1674
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1675
    """Modifies parameters of storage units on the node.
1676

1677
    @type node: str
1678
    @param node: node whose storage units to modify
1679
    @type storage_type: str
1680
    @param storage_type: storage type whose units to modify
1681
    @type name: str
1682
    @param name: name of the storage unit
1683
    @type allocatable: bool or None
1684
    @param allocatable: Whether to set the "allocatable" flag on the storage
1685
                        unit (None=no modification, True=set, False=unset)
1686

1687
    @rtype: string
1688
    @return: job id
1689

1690
    """
1691
    query = [
1692
      ("storage_type", storage_type),
1693
      ("name", name),
1694
      ]
1695

    
1696
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1697

    
1698
    return self._SendRequest(HTTP_PUT,
1699
                             ("/%s/nodes/%s/storage/modify" %
1700
                              (GANETI_RAPI_VERSION, node)), query, None)
1701

    
1702
  def RepairNodeStorageUnits(self, node, storage_type, name):
1703
    """Repairs a storage unit on the node.
1704

1705
    @type node: str
1706
    @param node: node whose storage units to repair
1707
    @type storage_type: str
1708
    @param storage_type: storage type to repair
1709
    @type name: str
1710
    @param name: name of the storage unit to repair
1711

1712
    @rtype: string
1713
    @return: job id
1714

1715
    """
1716
    query = [
1717
      ("storage_type", storage_type),
1718
      ("name", name),
1719
      ]
1720

    
1721
    return self._SendRequest(HTTP_PUT,
1722
                             ("/%s/nodes/%s/storage/repair" %
1723
                              (GANETI_RAPI_VERSION, node)), query, None)
1724

    
1725
  def GetNodeTags(self, node):
1726
    """Gets the tags for a node.
1727

1728
    @type node: str
1729
    @param node: node whose tags to return
1730

1731
    @rtype: list of str
1732
    @return: tags for the node
1733

1734
    """
1735
    return self._SendRequest(HTTP_GET,
1736
                             ("/%s/nodes/%s/tags" %
1737
                              (GANETI_RAPI_VERSION, node)), None, None)
1738

    
1739
  def AddNodeTags(self, node, tags, dry_run=False):
1740
    """Adds tags to a node.
1741

1742
    @type node: str
1743
    @param node: node to add tags to
1744
    @type tags: list of str
1745
    @param tags: tags to add to the node
1746
    @type dry_run: bool
1747
    @param dry_run: whether to perform a dry run
1748

1749
    @rtype: string
1750
    @return: job id
1751

1752
    """
1753
    query = [("tag", t) for t in tags]
1754
    _AppendDryRunIf(query, dry_run)
1755

    
1756
    return self._SendRequest(HTTP_PUT,
1757
                             ("/%s/nodes/%s/tags" %
1758
                              (GANETI_RAPI_VERSION, node)), query, tags)
1759

    
1760
  def DeleteNodeTags(self, node, tags, dry_run=False):
1761
    """Delete tags from a node.
1762

1763
    @type node: str
1764
    @param node: node to remove tags from
1765
    @type tags: list of str
1766
    @param tags: tags to remove from the node
1767
    @type dry_run: bool
1768
    @param dry_run: whether to perform a dry run
1769

1770
    @rtype: string
1771
    @return: job id
1772

1773
    """
1774
    query = [("tag", t) for t in tags]
1775
    _AppendDryRunIf(query, dry_run)
1776

    
1777
    return self._SendRequest(HTTP_DELETE,
1778
                             ("/%s/nodes/%s/tags" %
1779
                              (GANETI_RAPI_VERSION, node)), query, None)
1780

    
1781
  def GetNetworks(self, bulk=False):
1782
    """Gets all networks in the cluster.
1783

1784
    @type bulk: bool
1785
    @param bulk: whether to return all information about the networks
1786

1787
    @rtype: list of dict or str
1788
    @return: if bulk is true, a list of dictionaries with info about all
1789
        networks in the cluster, else a list of names of those networks
1790

1791
    """
1792
    query = []
1793
    _AppendIf(query, bulk, ("bulk", 1))
1794

    
1795
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1796
                                 query, None)
1797
    if bulk:
1798
      return networks
1799
    else:
1800
      return [n["name"] for n in networks]
1801

    
1802
  def GetNetwork(self, network):
1803
    """Gets information about a network.
1804

1805
    @type network: str
1806
    @param network: name of the network whose info to return
1807

1808
    @rtype: dict
1809
    @return: info about the network
1810

1811
    """
1812
    return self._SendRequest(HTTP_GET,
1813
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1814
                             None, None)
1815

    
1816
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1817
                    gateway6=None, mac_prefix=None,
1818
                    add_reserved_ips=None, tags=None, dry_run=False):
1819
    """Creates a new network.
1820

1821
    @type network_name: str
1822
    @param network_name: the name of network to create
1823
    @type dry_run: bool
1824
    @param dry_run: whether to peform a dry run
1825

1826
    @rtype: string
1827
    @return: job id
1828

1829
    """
1830
    query = []
1831
    _AppendDryRunIf(query, dry_run)
1832

    
1833
    if add_reserved_ips:
1834
      add_reserved_ips = add_reserved_ips.split(",")
1835

    
1836
    if tags:
1837
      tags = tags.split(",")
1838

    
1839
    body = {
1840
      "network_name": network_name,
1841
      "gateway": gateway,
1842
      "network": network,
1843
      "gateway6": gateway6,
1844
      "network6": network6,
1845
      "mac_prefix": mac_prefix,
1846
      "add_reserved_ips": add_reserved_ips,
1847
      "tags": tags,
1848
      }
1849

    
1850
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1851
                             query, body)
1852

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

1856
    """
1857
    body = {
1858
      "group_name": group_name,
1859
      "network_mode": mode,
1860
      "network_link": link,
1861
      }
1862

    
1863
    query = []
1864
    _AppendDryRunIf(query, dry_run)
1865

    
1866
    return self._SendRequest(HTTP_PUT,
1867
                             ("/%s/networks/%s/connect" %
1868
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1869

    
1870
  def DisconnectNetwork(self, network_name, group_name, dry_run=False):
1871
    """Connects a Network to a NodeGroup with the given netparams
1872

1873
    """
1874
    body = {
1875
      "group_name": group_name,
1876
      }
1877

    
1878
    query = []
1879
    _AppendDryRunIf(query, dry_run)
1880

    
1881
    return self._SendRequest(HTTP_PUT,
1882
                             ("/%s/networks/%s/disconnect" %
1883
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1884

    
1885
  def ModifyNetwork(self, network, **kwargs):
1886
    """Modifies a network.
1887

1888
    More details for parameters can be found in the RAPI documentation.
1889

1890
    @type network: string
1891
    @param network: Network name
1892
    @rtype: string
1893
    @return: job id
1894

1895
    """
1896
    return self._SendRequest(HTTP_PUT,
1897
                             ("/%s/networks/%s/modify" %
1898
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
1899

    
1900
  def DeleteNetwork(self, network, dry_run=False):
1901
    """Deletes a network.
1902

1903
    @type network: str
1904
    @param network: the network to delete
1905
    @type dry_run: bool
1906
    @param dry_run: whether to peform a dry run
1907

1908
    @rtype: string
1909
    @return: job id
1910

1911
    """
1912
    query = []
1913
    _AppendDryRunIf(query, dry_run)
1914

    
1915
    return self._SendRequest(HTTP_DELETE,
1916
                             ("/%s/networks/%s" %
1917
                              (GANETI_RAPI_VERSION, network)), query, None)
1918

    
1919
  def GetNetworkTags(self, network):
1920
    """Gets tags for a network.
1921

1922
    @type network: string
1923
    @param network: Node group whose tags to return
1924

1925
    @rtype: list of strings
1926
    @return: tags for the network
1927

1928
    """
1929
    return self._SendRequest(HTTP_GET,
1930
                             ("/%s/networks/%s/tags" %
1931
                              (GANETI_RAPI_VERSION, network)), None, None)
1932

    
1933
  def AddNetworkTags(self, network, tags, dry_run=False):
1934
    """Adds tags to a network.
1935

1936
    @type network: str
1937
    @param network: network to add tags to
1938
    @type tags: list of string
1939
    @param tags: tags to add to the network
1940
    @type dry_run: bool
1941
    @param dry_run: whether to perform a dry run
1942

1943
    @rtype: string
1944
    @return: job id
1945

1946
    """
1947
    query = [("tag", t) for t in tags]
1948
    _AppendDryRunIf(query, dry_run)
1949

    
1950
    return self._SendRequest(HTTP_PUT,
1951
                             ("/%s/networks/%s/tags" %
1952
                              (GANETI_RAPI_VERSION, network)), query, None)
1953

    
1954
  def DeleteNetworkTags(self, network, tags, dry_run=False):
1955
    """Deletes tags from a network.
1956

1957
    @type network: str
1958
    @param network: network to delete tags from
1959
    @type tags: list of string
1960
    @param tags: tags to delete
1961
    @type dry_run: bool
1962
    @param dry_run: whether to perform a dry run
1963
    @rtype: string
1964
    @return: job id
1965

1966
    """
1967
    query = [("tag", t) for t in tags]
1968
    _AppendDryRunIf(query, dry_run)
1969

    
1970
    return self._SendRequest(HTTP_DELETE,
1971
                             ("/%s/networks/%s/tags" %
1972
                              (GANETI_RAPI_VERSION, network)), query, None)
1973

    
1974
  def GetGroups(self, bulk=False):
1975
    """Gets all node groups in the cluster.
1976

1977
    @type bulk: bool
1978
    @param bulk: whether to return all information about the groups
1979

1980
    @rtype: list of dict or str
1981
    @return: if bulk is true, a list of dictionaries with info about all node
1982
        groups in the cluster, else a list of names of those node groups
1983

1984
    """
1985
    query = []
1986
    _AppendIf(query, bulk, ("bulk", 1))
1987

    
1988
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1989
                               query, None)
1990
    if bulk:
1991
      return groups
1992
    else:
1993
      return [g["name"] for g in groups]
1994

    
1995
  def GetGroup(self, group):
1996
    """Gets information about a node group.
1997

1998
    @type group: str
1999
    @param group: name of the node group whose info to return
2000

2001
    @rtype: dict
2002
    @return: info about the node group
2003

2004
    """
2005
    return self._SendRequest(HTTP_GET,
2006
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2007
                             None, None)
2008

    
2009
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
2010
    """Creates a new node group.
2011

2012
    @type name: str
2013
    @param name: the name of node group to create
2014
    @type alloc_policy: str
2015
    @param alloc_policy: the desired allocation policy for the group, if any
2016
    @type dry_run: bool
2017
    @param dry_run: whether to peform a dry run
2018

2019
    @rtype: string
2020
    @return: job id
2021

2022
    """
2023
    query = []
2024
    _AppendDryRunIf(query, dry_run)
2025

    
2026
    body = {
2027
      "name": name,
2028
      "alloc_policy": alloc_policy,
2029
      }
2030

    
2031
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2032
                             query, body)
2033

    
2034
  def ModifyGroup(self, group, **kwargs):
2035
    """Modifies a node group.
2036

2037
    More details for parameters can be found in the RAPI documentation.
2038

2039
    @type group: string
2040
    @param group: Node group name
2041
    @rtype: string
2042
    @return: job id
2043

2044
    """
2045
    return self._SendRequest(HTTP_PUT,
2046
                             ("/%s/groups/%s/modify" %
2047
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
2048

    
2049
  def DeleteGroup(self, group, dry_run=False):
2050
    """Deletes a node group.
2051

2052
    @type group: str
2053
    @param group: the node group to delete
2054
    @type dry_run: bool
2055
    @param dry_run: whether to peform a dry run
2056

2057
    @rtype: string
2058
    @return: job id
2059

2060
    """
2061
    query = []
2062
    _AppendDryRunIf(query, dry_run)
2063

    
2064
    return self._SendRequest(HTTP_DELETE,
2065
                             ("/%s/groups/%s" %
2066
                              (GANETI_RAPI_VERSION, group)), query, None)
2067

    
2068
  def RenameGroup(self, group, new_name):
2069
    """Changes the name of a node group.
2070

2071
    @type group: string
2072
    @param group: Node group name
2073
    @type new_name: string
2074
    @param new_name: New node group name
2075

2076
    @rtype: string
2077
    @return: job id
2078

2079
    """
2080
    body = {
2081
      "new_name": new_name,
2082
      }
2083

    
2084
    return self._SendRequest(HTTP_PUT,
2085
                             ("/%s/groups/%s/rename" %
2086
                              (GANETI_RAPI_VERSION, group)), None, body)
2087

    
2088
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
2089
    """Assigns nodes to a group.
2090

2091
    @type group: string
2092
    @param group: Node group name
2093
    @type nodes: list of strings
2094
    @param nodes: List of nodes to assign to the group
2095

2096
    @rtype: string
2097
    @return: job id
2098

2099
    """
2100
    query = []
2101
    _AppendForceIf(query, force)
2102
    _AppendDryRunIf(query, dry_run)
2103

    
2104
    body = {
2105
      "nodes": nodes,
2106
      }
2107

    
2108
    return self._SendRequest(HTTP_PUT,
2109
                             ("/%s/groups/%s/assign-nodes" %
2110
                             (GANETI_RAPI_VERSION, group)), query, body)
2111

    
2112
  def GetGroupTags(self, group):
2113
    """Gets tags for a node group.
2114

2115
    @type group: string
2116
    @param group: Node group whose tags to return
2117

2118
    @rtype: list of strings
2119
    @return: tags for the group
2120

2121
    """
2122
    return self._SendRequest(HTTP_GET,
2123
                             ("/%s/groups/%s/tags" %
2124
                              (GANETI_RAPI_VERSION, group)), None, None)
2125

    
2126
  def AddGroupTags(self, group, tags, dry_run=False):
2127
    """Adds tags to a node group.
2128

2129
    @type group: str
2130
    @param group: group to add tags to
2131
    @type tags: list of string
2132
    @param tags: tags to add to the group
2133
    @type dry_run: bool
2134
    @param dry_run: whether to perform a dry run
2135

2136
    @rtype: string
2137
    @return: job id
2138

2139
    """
2140
    query = [("tag", t) for t in tags]
2141
    _AppendDryRunIf(query, dry_run)
2142

    
2143
    return self._SendRequest(HTTP_PUT,
2144
                             ("/%s/groups/%s/tags" %
2145
                              (GANETI_RAPI_VERSION, group)), query, None)
2146

    
2147
  def DeleteGroupTags(self, group, tags, dry_run=False):
2148
    """Deletes tags from a node group.
2149

2150
    @type group: str
2151
    @param group: group to delete tags from
2152
    @type tags: list of string
2153
    @param tags: tags to delete
2154
    @type dry_run: bool
2155
    @param dry_run: whether to perform a dry run
2156
    @rtype: string
2157
    @return: job id
2158

2159
    """
2160
    query = [("tag", t) for t in tags]
2161
    _AppendDryRunIf(query, dry_run)
2162

    
2163
    return self._SendRequest(HTTP_DELETE,
2164
                             ("/%s/groups/%s/tags" %
2165
                              (GANETI_RAPI_VERSION, group)), query, None)
2166

    
2167
  def Query(self, what, fields, qfilter=None):
2168
    """Retrieves information about resources.
2169

2170
    @type what: string
2171
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2172
    @type fields: list of string
2173
    @param fields: Requested fields
2174
    @type qfilter: None or list
2175
    @param qfilter: Query filter
2176

2177
    @rtype: string
2178
    @return: job id
2179

2180
    """
2181
    body = {
2182
      "fields": fields,
2183
      }
2184

    
2185
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2186
    # TODO: remove "filter" after 2.7
2187
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
2188

    
2189
    return self._SendRequest(HTTP_PUT,
2190
                             ("/%s/query/%s" %
2191
                              (GANETI_RAPI_VERSION, what)), None, body)
2192

    
2193
  def QueryFields(self, what, fields=None):
2194
    """Retrieves available fields for a resource.
2195

2196
    @type what: string
2197
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2198
    @type fields: list of string
2199
    @param fields: Requested fields
2200

2201
    @rtype: string
2202
    @return: job id
2203

2204
    """
2205
    query = []
2206

    
2207
    if fields is not None:
2208
      _AppendIf(query, True, ("fields", ",".join(fields)))
2209

    
2210
    return self._SendRequest(HTTP_GET,
2211
                             ("/%s/query/%s/fields" %
2212
                              (GANETI_RAPI_VERSION, what)), query, None)