Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 7d81bb8b

History | View | Annotate | Download (72.9 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 _AppendReason(container, reason):
223
  """Appends an element to the reason trail.
224

225
  If the user provided a reason, it is added to the reason trail.
226

227
  """
228
  return _AppendIf(container, reason, ("reason", reason))
229

    
230

    
231
def _SetItemIf(container, condition, item, value):
232
  """Sets an item if a condition evaluates to truth.
233

234
  """
235
  if condition:
236
    container[item] = value
237

    
238
  return condition
239

    
240

    
241
def UsesRapiClient(fn):
242
  """Decorator for code using RAPI client to initialize pycURL.
243

244
  """
245
  def wrapper(*args, **kwargs):
246
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
247
    # one thread running. This check is just a safety measure -- it doesn't
248
    # cover all cases.
249
    assert threading.activeCount() == 1, \
250
           "Found active threads when initializing pycURL"
251

    
252
    pycurl.global_init(pycurl.GLOBAL_ALL)
253
    try:
254
      return fn(*args, **kwargs)
255
    finally:
256
      pycurl.global_cleanup()
257

    
258
  return wrapper
259

    
260

    
261
def GenericCurlConfig(verbose=False, use_signal=False,
262
                      use_curl_cabundle=False, cafile=None, capath=None,
263
                      proxy=None, verify_hostname=False,
264
                      connect_timeout=None, timeout=None,
265
                      _pycurl_version_fn=pycurl.version_info):
266
  """Curl configuration function generator.
267

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

290
  """
291
  if use_curl_cabundle and (cafile or capath):
292
    raise Error("Can not use default CA bundle when CA file or path is set")
293

    
294
  def _ConfigCurl(curl, logger):
295
    """Configures a cURL object
296

297
    @type curl: pycurl.Curl
298
    @param curl: cURL object
299

300
    """
301
    logger.debug("Using cURL version %s", pycurl.version)
302

    
303
    # pycurl.version_info returns a tuple with information about the used
304
    # version of libcurl. Item 5 is the SSL library linked to it.
305
    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
306
    # 0, '1.2.3.3', ...)
307
    sslver = _pycurl_version_fn()[5]
308
    if not sslver:
309
      raise Error("No SSL support in cURL")
310

    
311
    lcsslver = sslver.lower()
312
    if lcsslver.startswith("openssl/"):
313
      pass
314
    elif lcsslver.startswith("nss/"):
315
      # TODO: investigate compatibility beyond a simple test
316
      pass
317
    elif lcsslver.startswith("gnutls/"):
318
      if capath:
319
        raise Error("cURL linked against GnuTLS has no support for a"
320
                    " CA path (%s)" % (pycurl.version, ))
321
    else:
322
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
323
                                sslver)
324

    
325
    curl.setopt(pycurl.VERBOSE, verbose)
326
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
327

    
328
    # Whether to verify remote peer's CN
329
    if verify_hostname:
330
      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
331
      # certificate must indicate that the server is the server to which you
332
      # meant to connect, or the connection fails. [...] When the value is 1,
333
      # the certificate must contain a Common Name field, but it doesn't matter
334
      # what name it says. [...]"
335
      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
336
    else:
337
      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
338

    
339
    if cafile or capath or use_curl_cabundle:
340
      # Require certificates to be checked
341
      curl.setopt(pycurl.SSL_VERIFYPEER, True)
342
      if cafile:
343
        curl.setopt(pycurl.CAINFO, str(cafile))
344
      if capath:
345
        curl.setopt(pycurl.CAPATH, str(capath))
346
      # Not changing anything for using default CA bundle
347
    else:
348
      # Disable SSL certificate verification
349
      curl.setopt(pycurl.SSL_VERIFYPEER, False)
350

    
351
    if proxy is not None:
352
      curl.setopt(pycurl.PROXY, str(proxy))
353

    
354
    # Timeouts
355
    if connect_timeout is not None:
356
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
357
    if timeout is not None:
358
      curl.setopt(pycurl.TIMEOUT, timeout)
359

    
360
  return _ConfigCurl
361

    
362

    
363
class GanetiRapiClient(object): # pylint: disable=R0904
364
  """Ganeti RAPI client.
365

366
  """
367
  USER_AGENT = "Ganeti RAPI Client"
368
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
369

    
370
  def __init__(self, host, port=GANETI_RAPI_PORT,
371
               username=None, password=None, logger=logging,
372
               curl_config_fn=None, curl_factory=None):
373
    """Initializes this class.
374

375
    @type host: string
376
    @param host: the ganeti cluster master to interact with
377
    @type port: int
378
    @param port: the port on which the RAPI is running (default is 5080)
379
    @type username: string
380
    @param username: the username to connect with
381
    @type password: string
382
    @param password: the password to connect with
383
    @type curl_config_fn: callable
384
    @param curl_config_fn: Function to configure C{pycurl.Curl} object
385
    @param logger: Logging object
386

387
    """
388
    self._username = username
389
    self._password = password
390
    self._logger = logger
391
    self._curl_config_fn = curl_config_fn
392
    self._curl_factory = curl_factory
393

    
394
    try:
395
      socket.inet_pton(socket.AF_INET6, host)
396
      address = "[%s]:%s" % (host, port)
397
    except socket.error:
398
      address = "%s:%s" % (host, port)
399

    
400
    self._base_url = "https://%s" % address
401

    
402
    if username is not None:
403
      if password is None:
404
        raise Error("Password not specified")
405
    elif password:
406
      raise Error("Specified password without username")
407

    
408
  def _CreateCurl(self):
409
    """Creates a cURL object.
410

411
    """
412
    # Create pycURL object if no factory is provided
413
    if self._curl_factory:
414
      curl = self._curl_factory()
415
    else:
416
      curl = pycurl.Curl()
417

    
418
    # Default cURL settings
419
    curl.setopt(pycurl.VERBOSE, False)
420
    curl.setopt(pycurl.FOLLOWLOCATION, False)
421
    curl.setopt(pycurl.MAXREDIRS, 5)
422
    curl.setopt(pycurl.NOSIGNAL, True)
423
    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
424
    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
425
    curl.setopt(pycurl.SSL_VERIFYPEER, False)
426
    curl.setopt(pycurl.HTTPHEADER, [
427
      "Accept: %s" % HTTP_APP_JSON,
428
      "Content-type: %s" % HTTP_APP_JSON,
429
      ])
430

    
431
    assert ((self._username is None and self._password is None) ^
432
            (self._username is not None and self._password is not None))
433

    
434
    if self._username:
435
      # Setup authentication
436
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
437
      curl.setopt(pycurl.USERPWD,
438
                  str("%s:%s" % (self._username, self._password)))
439

    
440
    # Call external configuration function
441
    if self._curl_config_fn:
442
      self._curl_config_fn(curl, self._logger)
443

    
444
    return curl
445

    
446
  @staticmethod
447
  def _EncodeQuery(query):
448
    """Encode query values for RAPI URL.
449

450
    @type query: list of two-tuples
451
    @param query: Query arguments
452
    @rtype: list
453
    @return: Query list with encoded values
454

455
    """
456
    result = []
457

    
458
    for name, value in query:
459
      if value is None:
460
        result.append((name, ""))
461

    
462
      elif isinstance(value, bool):
463
        # Boolean values must be encoded as 0 or 1
464
        result.append((name, int(value)))
465

    
466
      elif isinstance(value, (list, tuple, dict)):
467
        raise ValueError("Invalid query data type %r" % type(value).__name__)
468

    
469
      else:
470
        result.append((name, value))
471

    
472
    return result
473

    
474
  def _SendRequest(self, method, path, query, content):
475
    """Sends an HTTP request.
476

477
    This constructs a full URL, encodes and decodes HTTP bodies, and
478
    handles invalid responses in a pythonic way.
479

480
    @type method: string
481
    @param method: HTTP method to use
482
    @type path: string
483
    @param path: HTTP URL path
484
    @type query: list of two-tuples
485
    @param query: query arguments to pass to urllib.urlencode
486
    @type content: str or None
487
    @param content: HTTP body content
488

489
    @rtype: str
490
    @return: JSON-Decoded response
491

492
    @raises CertificateError: If an invalid SSL certificate is found
493
    @raises GanetiApiError: If an invalid response is returned
494

495
    """
496
    assert path.startswith("/")
497

    
498
    curl = self._CreateCurl()
499

    
500
    if content is not None:
501
      encoded_content = self._json_encoder.encode(content)
502
    else:
503
      encoded_content = ""
504

    
505
    # Build URL
506
    urlparts = [self._base_url, path]
507
    if query:
508
      urlparts.append("?")
509
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
510

    
511
    url = "".join(urlparts)
512

    
513
    self._logger.debug("Sending request %s %s (content=%r)",
514
                       method, url, encoded_content)
515

    
516
    # Buffer for response
517
    encoded_resp_body = StringIO()
518

    
519
    # Configure cURL
520
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
521
    curl.setopt(pycurl.URL, str(url))
522
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
523
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
524

    
525
    try:
526
      # Send request and wait for response
527
      try:
528
        curl.perform()
529
      except pycurl.error, err:
530
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
531
          raise CertificateError("SSL certificate error %s" % err,
532
                                 code=err.args[0])
533

    
534
        raise GanetiApiError(str(err), code=err.args[0])
535
    finally:
536
      # Reset settings to not keep references to large objects in memory
537
      # between requests
538
      curl.setopt(pycurl.POSTFIELDS, "")
539
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
540

    
541
    # Get HTTP response code
542
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
543

    
544
    # Was anything written to the response buffer?
545
    if encoded_resp_body.tell():
546
      response_content = simplejson.loads(encoded_resp_body.getvalue())
547
    else:
548
      response_content = None
549

    
550
    if http_code != HTTP_OK:
551
      if isinstance(response_content, dict):
552
        msg = ("%s %s: %s" %
553
               (response_content["code"],
554
                response_content["message"],
555
                response_content["explain"]))
556
      else:
557
        msg = str(response_content)
558

    
559
      raise GanetiApiError(msg, code=http_code)
560

    
561
    return response_content
562

    
563
  def GetVersion(self):
564
    """Gets the Remote API version running on the cluster.
565

566
    @rtype: int
567
    @return: Ganeti Remote API version
568

569
    """
570
    return self._SendRequest(HTTP_GET, "/version", None, None)
571

    
572
  def GetFeatures(self):
573
    """Gets the list of optional features supported by RAPI server.
574

575
    @rtype: list
576
    @return: List of optional features
577

578
    """
579
    try:
580
      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
581
                               None, None)
582
    except GanetiApiError, err:
583
      # Older RAPI servers don't support this resource
584
      if err.code == HTTP_NOT_FOUND:
585
        return []
586

    
587
      raise
588

    
589
  def GetOperatingSystems(self, reason=None):
590
    """Gets the Operating Systems running in the Ganeti cluster.
591

592
    @rtype: list of str
593
    @return: operating systems
594
    @type reason: string
595
    @param reason: the reason for executing this operation
596

597
    """
598
    query = []
599
    _AppendReason(query, reason)
600
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
601
                             query, None)
602

    
603
  def GetInfo(self, reason=None):
604
    """Gets info about the cluster.
605

606
    @type reason: string
607
    @param reason: the reason for executing this operation
608
    @rtype: dict
609
    @return: information about the cluster
610

611
    """
612
    query = []
613
    _AppendReason(query, reason)
614
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
615
                             query, None)
616

    
617
  def RedistributeConfig(self, reason=None):
618
    """Tells the cluster to redistribute its configuration files.
619

620
    @type reason: string
621
    @param reason: the reason for executing this operation
622
    @rtype: string
623
    @return: job id
624

625
    """
626
    query = []
627
    _AppendReason(query, reason)
628
    return self._SendRequest(HTTP_PUT,
629
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
630
                             query, None)
631

    
632
  def ModifyCluster(self, reason=None, **kwargs):
633
    """Modifies cluster parameters.
634

635
    More details for parameters can be found in the RAPI documentation.
636

637
    @type reason: string
638
    @param reason: the reason for executing this operation
639
    @rtype: string
640
    @return: job id
641

642
    """
643
    query = []
644
    _AppendReason(query, reason)
645

    
646
    body = kwargs
647

    
648
    return self._SendRequest(HTTP_PUT,
649
                             "/%s/modify" % GANETI_RAPI_VERSION, query, body)
650

    
651
  def GetClusterTags(self, reason=None):
652
    """Gets the cluster tags.
653

654
    @type reason: string
655
    @param reason: the reason for executing this operation
656
    @rtype: list of str
657
    @return: cluster tags
658

659
    """
660
    query = []
661
    _AppendReason(query, reason)
662
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
663
                             query, None)
664

    
665
  def AddClusterTags(self, tags, dry_run=False, reason=None):
666
    """Adds tags to the cluster.
667

668
    @type tags: list of str
669
    @param tags: tags to add to the cluster
670
    @type dry_run: bool
671
    @param dry_run: whether to perform a dry run
672
    @type reason: string
673
    @param reason: the reason for executing this operation
674

675
    @rtype: string
676
    @return: job id
677

678
    """
679
    query = [("tag", t) for t in tags]
680
    _AppendDryRunIf(query, dry_run)
681
    _AppendReason(query, reason)
682

    
683
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
684
                             query, None)
685

    
686
  def DeleteClusterTags(self, tags, dry_run=False, reason=None):
687
    """Deletes tags from the cluster.
688

689
    @type tags: list of str
690
    @param tags: tags to delete
691
    @type dry_run: bool
692
    @param dry_run: whether to perform a dry run
693
    @type reason: string
694
    @param reason: the reason for executing this operation
695
    @rtype: string
696
    @return: job id
697

698
    """
699
    query = [("tag", t) for t in tags]
700
    _AppendDryRunIf(query, dry_run)
701
    _AppendReason(query, reason)
702

    
703
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
704
                             query, None)
705

    
706
  def GetInstances(self, bulk=False, reason=None):
707
    """Gets information about instances on the cluster.
708

709
    @type bulk: bool
710
    @param bulk: whether to return all information about all instances
711
    @type reason: string
712
    @param reason: the reason for executing this operation
713

714
    @rtype: list of dict or list of str
715
    @return: if bulk is True, info about the instances, else a list of instances
716

717
    """
718
    query = []
719
    _AppendIf(query, bulk, ("bulk", 1))
720
    _AppendReason(query, reason)
721

    
722
    instances = self._SendRequest(HTTP_GET,
723
                                  "/%s/instances" % GANETI_RAPI_VERSION,
724
                                  query, None)
725
    if bulk:
726
      return instances
727
    else:
728
      return [i["id"] for i in instances]
729

    
730
  def GetInstance(self, instance, reason=None):
731
    """Gets information about an instance.
732

733
    @type instance: str
734
    @param instance: instance whose info to return
735
    @type reason: string
736
    @param reason: the reason for executing this operation
737

738
    @rtype: dict
739
    @return: info about the instance
740

741
    """
742
    query = []
743
    _AppendReason(query, reason)
744

    
745
    return self._SendRequest(HTTP_GET,
746
                             ("/%s/instances/%s" %
747
                              (GANETI_RAPI_VERSION, instance)), query, None)
748

    
749
  def GetInstanceInfo(self, instance, static=None, reason=None):
750
    """Gets information about an instance.
751

752
    @type instance: string
753
    @param instance: Instance name
754
    @type reason: string
755
    @param reason: the reason for executing this operation
756
    @rtype: string
757
    @return: Job ID
758

759
    """
760
    query = []
761
    if static is not None:
762
      query.append(("static", static))
763
    _AppendReason(query, reason)
764

    
765
    return self._SendRequest(HTTP_GET,
766
                             ("/%s/instances/%s/info" %
767
                              (GANETI_RAPI_VERSION, instance)), query, None)
768

    
769
  @staticmethod
770
  def _UpdateWithKwargs(base, **kwargs):
771
    """Updates the base with params from kwargs.
772

773
    @param base: The base dict, filled with required fields
774

775
    @note: This is an inplace update of base
776

777
    """
778
    conflicts = set(kwargs.iterkeys()) & set(base.iterkeys())
779
    if conflicts:
780
      raise GanetiApiError("Required fields can not be specified as"
781
                           " keywords: %s" % ", ".join(conflicts))
782

    
783
    base.update((key, value) for key, value in kwargs.iteritems()
784
                if key != "dry_run")
785

    
786
  def InstanceAllocation(self, mode, name, disk_template, disks, nics,
787
                         **kwargs):
788
    """Generates an instance allocation as used by multiallocate.
789

790
    More details for parameters can be found in the RAPI documentation.
791
    It is the same as used by CreateInstance.
792

793
    @type mode: string
794
    @param mode: Instance creation mode
795
    @type name: string
796
    @param name: Hostname of the instance to create
797
    @type disk_template: string
798
    @param disk_template: Disk template for instance (e.g. plain, diskless,
799
                          file, or drbd)
800
    @type disks: list of dicts
801
    @param disks: List of disk definitions
802
    @type nics: list of dicts
803
    @param nics: List of NIC definitions
804

805
    @return: A dict with the generated entry
806

807
    """
808
    # All required fields for request data version 1
809
    alloc = {
810
      "mode": mode,
811
      "name": name,
812
      "disk_template": disk_template,
813
      "disks": disks,
814
      "nics": nics,
815
      }
816

    
817
    self._UpdateWithKwargs(alloc, **kwargs)
818

    
819
    return alloc
820

    
821
  def InstancesMultiAlloc(self, instances, reason=None, **kwargs):
822
    """Tries to allocate multiple instances.
823

824
    More details for parameters can be found in the RAPI documentation.
825

826
    @param instances: A list of L{InstanceAllocation} results
827

828
    """
829
    query = []
830
    body = {
831
      "instances": instances,
832
      }
833
    self._UpdateWithKwargs(body, **kwargs)
834

    
835
    _AppendDryRunIf(query, kwargs.get("dry_run"))
836
    _AppendReason(query, reason)
837

    
838
    return self._SendRequest(HTTP_POST,
839
                             "/%s/instances-multi-alloc" % GANETI_RAPI_VERSION,
840
                             query, body)
841

    
842
  def CreateInstance(self, mode, name, disk_template, disks, nics,
843
                     reason=None, **kwargs):
844
    """Creates a new instance.
845

846
    More details for parameters can be found in the RAPI documentation.
847

848
    @type mode: string
849
    @param mode: Instance creation mode
850
    @type name: string
851
    @param name: Hostname of the instance to create
852
    @type disk_template: string
853
    @param disk_template: Disk template for instance (e.g. plain, diskless,
854
                          file, or drbd)
855
    @type disks: list of dicts
856
    @param disks: List of disk definitions
857
    @type nics: list of dicts
858
    @param nics: List of NIC definitions
859
    @type dry_run: bool
860
    @keyword dry_run: whether to perform a dry run
861
    @type reason: string
862
    @param reason: the reason for executing this operation
863

864
    @rtype: string
865
    @return: job id
866

867
    """
868
    query = []
869

    
870
    _AppendDryRunIf(query, kwargs.get("dry_run"))
871
    _AppendReason(query, reason)
872

    
873
    if _INST_CREATE_REQV1 in self.GetFeatures():
874
      body = self.InstanceAllocation(mode, name, disk_template, disks, nics,
875
                                     **kwargs)
876
      body[_REQ_DATA_VERSION_FIELD] = 1
877
    else:
878
      raise GanetiApiError("Server does not support new-style (version 1)"
879
                           " instance creation requests")
880

    
881
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
882
                             query, body)
883

    
884
  def DeleteInstance(self, instance, dry_run=False, reason=None, **kwargs):
885
    """Deletes an instance.
886

887
    @type instance: str
888
    @param instance: the instance to delete
889
    @type reason: string
890
    @param reason: the reason for executing this operation
891

892
    @rtype: string
893
    @return: job id
894

895
    """
896
    query = []
897
    body = kwargs
898

    
899
    _AppendDryRunIf(query, dry_run)
900
    _AppendReason(query, reason)
901

    
902
    return self._SendRequest(HTTP_DELETE,
903
                             ("/%s/instances/%s" %
904
                              (GANETI_RAPI_VERSION, instance)), query, body)
905

    
906
  def ModifyInstance(self, instance, reason=None, **kwargs):
907
    """Modifies an instance.
908

909
    More details for parameters can be found in the RAPI documentation.
910

911
    @type instance: string
912
    @param instance: Instance name
913
    @type reason: string
914
    @param reason: the reason for executing this operation
915
    @rtype: string
916
    @return: job id
917

918
    """
919
    body = kwargs
920
    query = []
921
    _AppendReason(query, reason)
922

    
923
    return self._SendRequest(HTTP_PUT,
924
                             ("/%s/instances/%s/modify" %
925
                              (GANETI_RAPI_VERSION, instance)), query, body)
926

    
927
  def SnapshotInstance(self, instance, **kwargs):
928
    """Takes snapshot of instance's disks.
929

930
    More details for parameters can be found in the RAPI documentation.
931

932
    @type instance: string
933
    @param instance: Instance name
934
    @rtype: string
935
    @return: job id
936

937
    """
938
    body = kwargs
939

    
940
    return self._SendRequest(HTTP_PUT,
941
                             ("/%s/instances/%s/snapshot" %
942
                              (GANETI_RAPI_VERSION, instance)), None, body)
943

    
944
  def ActivateInstanceDisks(self, instance, ignore_size=None, reason=None):
945
    """Activates an instance's disks.
946

947
    @type instance: string
948
    @param instance: Instance name
949
    @type ignore_size: bool
950
    @param ignore_size: Whether to ignore recorded size
951
    @type reason: string
952
    @param reason: the reason for executing this operation
953
    @rtype: string
954
    @return: job id
955

956
    """
957
    query = []
958
    _AppendIf(query, ignore_size, ("ignore_size", 1))
959
    _AppendReason(query, reason)
960

    
961
    return self._SendRequest(HTTP_PUT,
962
                             ("/%s/instances/%s/activate-disks" %
963
                              (GANETI_RAPI_VERSION, instance)), query, None)
964

    
965
  def DeactivateInstanceDisks(self, instance, reason=None):
966
    """Deactivates an instance's disks.
967

968
    @type instance: string
969
    @param instance: Instance name
970
    @type reason: string
971
    @param reason: the reason for executing this operation
972
    @rtype: string
973
    @return: job id
974

975
    """
976
    query = []
977
    _AppendReason(query, reason)
978
    return self._SendRequest(HTTP_PUT,
979
                             ("/%s/instances/%s/deactivate-disks" %
980
                              (GANETI_RAPI_VERSION, instance)), query, None)
981

    
982
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None,
983
                            reason=None):
984
    """Recreate an instance's disks.
985

986
    @type instance: string
987
    @param instance: Instance name
988
    @type disks: list of int
989
    @param disks: List of disk indexes
990
    @type nodes: list of string
991
    @param nodes: New instance nodes, if relocation is desired
992
    @type reason: string
993
    @param reason: the reason for executing this operation
994
    @rtype: string
995
    @return: job id
996

997
    """
998
    body = {}
999
    _SetItemIf(body, disks is not None, "disks", disks)
1000
    _SetItemIf(body, nodes is not None, "nodes", nodes)
1001

    
1002
    query = []
1003
    _AppendReason(query, reason)
1004

    
1005
    return self._SendRequest(HTTP_POST,
1006
                             ("/%s/instances/%s/recreate-disks" %
1007
                              (GANETI_RAPI_VERSION, instance)), query, body)
1008

    
1009
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None,
1010
                       reason=None):
1011
    """Grows a disk of an instance.
1012

1013
    More details for parameters can be found in the RAPI documentation.
1014

1015
    @type instance: string
1016
    @param instance: Instance name
1017
    @type disk: integer
1018
    @param disk: Disk index
1019
    @type amount: integer
1020
    @param amount: Grow disk by this amount (MiB)
1021
    @type wait_for_sync: bool
1022
    @param wait_for_sync: Wait for disk to synchronize
1023
    @type reason: string
1024
    @param reason: the reason for executing this operation
1025
    @rtype: string
1026
    @return: job id
1027

1028
    """
1029
    body = {
1030
      "amount": amount,
1031
      }
1032

    
1033
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
1034

    
1035
    query = []
1036
    _AppendReason(query, reason)
1037

    
1038
    return self._SendRequest(HTTP_POST,
1039
                             ("/%s/instances/%s/disk/%s/grow" %
1040
                              (GANETI_RAPI_VERSION, instance, disk)),
1041
                             query, body)
1042

    
1043
  def GetInstanceTags(self, instance, reason=None):
1044
    """Gets tags for an instance.
1045

1046
    @type instance: str
1047
    @param instance: instance whose tags to return
1048
    @type reason: string
1049
    @param reason: the reason for executing this operation
1050

1051
    @rtype: list of str
1052
    @return: tags for the instance
1053

1054
    """
1055
    query = []
1056
    _AppendReason(query, reason)
1057
    return self._SendRequest(HTTP_GET,
1058
                             ("/%s/instances/%s/tags" %
1059
                              (GANETI_RAPI_VERSION, instance)), query, None)
1060

    
1061
  def AddInstanceTags(self, instance, tags, dry_run=False, reason=None):
1062
    """Adds tags to an instance.
1063

1064
    @type instance: str
1065
    @param instance: instance to add tags to
1066
    @type tags: list of str
1067
    @param tags: tags to add to the instance
1068
    @type dry_run: bool
1069
    @param dry_run: whether to perform a dry run
1070
    @type reason: string
1071
    @param reason: the reason for executing this operation
1072

1073
    @rtype: string
1074
    @return: job id
1075

1076
    """
1077
    query = [("tag", t) for t in tags]
1078
    _AppendDryRunIf(query, dry_run)
1079
    _AppendReason(query, reason)
1080

    
1081
    return self._SendRequest(HTTP_PUT,
1082
                             ("/%s/instances/%s/tags" %
1083
                              (GANETI_RAPI_VERSION, instance)), query, None)
1084

    
1085
  def DeleteInstanceTags(self, instance, tags, dry_run=False, reason=None):
1086
    """Deletes tags from an instance.
1087

1088
    @type instance: str
1089
    @param instance: instance to delete tags from
1090
    @type tags: list of str
1091
    @param tags: tags to delete
1092
    @type dry_run: bool
1093
    @param dry_run: whether to perform a dry run
1094
    @type reason: string
1095
    @param reason: the reason for executing this operation
1096
    @rtype: string
1097
    @return: job id
1098

1099
    """
1100
    query = [("tag", t) for t in tags]
1101
    _AppendDryRunIf(query, dry_run)
1102
    _AppendReason(query, reason)
1103

    
1104
    return self._SendRequest(HTTP_DELETE,
1105
                             ("/%s/instances/%s/tags" %
1106
                              (GANETI_RAPI_VERSION, instance)), query, None)
1107

    
1108
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
1109
                     dry_run=False, reason=None, **kwargs):
1110
    """Reboots an instance.
1111

1112
    @type instance: str
1113
    @param instance: instance to reboot
1114
    @type reboot_type: str
1115
    @param reboot_type: one of: hard, soft, full
1116
    @type ignore_secondaries: bool
1117
    @param ignore_secondaries: if True, ignores errors for the secondary node
1118
        while re-assembling disks (in hard-reboot mode only)
1119
    @type dry_run: bool
1120
    @param dry_run: whether to perform a dry run
1121
    @type reason: string
1122
    @param reason: the reason for the reboot
1123
    @rtype: string
1124
    @return: job id
1125

1126
    """
1127
    query = []
1128
    body = kwargs
1129

    
1130
    _AppendDryRunIf(query, dry_run)
1131
    _AppendIf(query, reboot_type, ("type", reboot_type))
1132
    _AppendIf(query, ignore_secondaries is not None,
1133
              ("ignore_secondaries", ignore_secondaries))
1134
    _AppendReason(query, reason)
1135

    
1136
    return self._SendRequest(HTTP_POST,
1137
                             ("/%s/instances/%s/reboot" %
1138
                              (GANETI_RAPI_VERSION, instance)), query, body)
1139

    
1140
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
1141
                       reason=None, **kwargs):
1142
    """Shuts down an instance.
1143

1144
    @type instance: str
1145
    @param instance: the instance to shut down
1146
    @type dry_run: bool
1147
    @param dry_run: whether to perform a dry run
1148
    @type no_remember: bool
1149
    @param no_remember: if true, will not record the state change
1150
    @type reason: string
1151
    @param reason: the reason for the shutdown
1152
    @rtype: string
1153
    @return: job id
1154

1155
    """
1156
    query = []
1157
    body = kwargs
1158

    
1159
    _AppendDryRunIf(query, dry_run)
1160
    _AppendIf(query, no_remember, ("no_remember", 1))
1161
    _AppendReason(query, reason)
1162

    
1163
    return self._SendRequest(HTTP_PUT,
1164
                             ("/%s/instances/%s/shutdown" %
1165
                              (GANETI_RAPI_VERSION, instance)), query, body)
1166

    
1167
  def StartupInstance(self, instance, dry_run=False, no_remember=False,
1168
                      reason=None):
1169
    """Starts up an instance.
1170

1171
    @type instance: str
1172
    @param instance: the instance to start up
1173
    @type dry_run: bool
1174
    @param dry_run: whether to perform a dry run
1175
    @type no_remember: bool
1176
    @param no_remember: if true, will not record the state change
1177
    @type reason: string
1178
    @param reason: the reason for the startup
1179
    @rtype: string
1180
    @return: job id
1181

1182
    """
1183
    query = []
1184
    _AppendDryRunIf(query, dry_run)
1185
    _AppendIf(query, no_remember, ("no_remember", 1))
1186
    _AppendReason(query, reason)
1187

    
1188
    return self._SendRequest(HTTP_PUT,
1189
                             ("/%s/instances/%s/startup" %
1190
                              (GANETI_RAPI_VERSION, instance)), query, None)
1191

    
1192
  def ReinstallInstance(self, instance, os=None, no_startup=False,
1193
                        osparams=None, reason=None):
1194
    """Reinstalls an instance.
1195

1196
    @type instance: str
1197
    @param instance: The instance to reinstall
1198
    @type os: str or None
1199
    @param os: The operating system to reinstall. If None, the instance's
1200
        current operating system will be installed again
1201
    @type no_startup: bool
1202
    @param no_startup: Whether to start the instance automatically
1203
    @type reason: string
1204
    @param reason: the reason for executing this operation
1205
    @rtype: string
1206
    @return: job id
1207

1208
    """
1209
    query = []
1210
    _AppendReason(query, reason)
1211

    
1212
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
1213
      body = {
1214
        "start": not no_startup,
1215
        }
1216
      _SetItemIf(body, os is not None, "os", os)
1217
      _SetItemIf(body, osparams is not None, "osparams", osparams)
1218
      return self._SendRequest(HTTP_POST,
1219
                               ("/%s/instances/%s/reinstall" %
1220
                                (GANETI_RAPI_VERSION, instance)), query, body)
1221

    
1222
    # Use old request format
1223
    if osparams:
1224
      raise GanetiApiError("Server does not support specifying OS parameters"
1225
                           " for instance reinstallation")
1226

    
1227
    query = []
1228
    _AppendIf(query, os, ("os", os))
1229
    _AppendIf(query, no_startup, ("nostartup", 1))
1230

    
1231
    return self._SendRequest(HTTP_POST,
1232
                             ("/%s/instances/%s/reinstall" %
1233
                              (GANETI_RAPI_VERSION, instance)), query, None)
1234

    
1235
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1236
                           remote_node=None, iallocator=None, reason=None):
1237
    """Replaces disks on an instance.
1238

1239
    @type instance: str
1240
    @param instance: instance whose disks to replace
1241
    @type disks: list of ints
1242
    @param disks: Indexes of disks to replace
1243
    @type mode: str
1244
    @param mode: replacement mode to use (defaults to replace_auto)
1245
    @type remote_node: str or None
1246
    @param remote_node: new secondary node to use (for use with
1247
        replace_new_secondary mode)
1248
    @type iallocator: str or None
1249
    @param iallocator: instance allocator plugin to use (for use with
1250
                       replace_auto mode)
1251
    @type reason: string
1252
    @param reason: the reason for executing this operation
1253

1254
    @rtype: string
1255
    @return: job id
1256

1257
    """
1258
    query = [
1259
      ("mode", mode),
1260
      ]
1261

    
1262
    # TODO: Convert to body parameters
1263

    
1264
    if disks is not None:
1265
      _AppendIf(query, True,
1266
                ("disks", ",".join(str(idx) for idx in disks)))
1267

    
1268
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1269
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1270
    _AppendReason(query, reason)
1271

    
1272
    return self._SendRequest(HTTP_POST,
1273
                             ("/%s/instances/%s/replace-disks" %
1274
                              (GANETI_RAPI_VERSION, instance)), query, None)
1275

    
1276
  def PrepareExport(self, instance, mode, reason=None):
1277
    """Prepares an instance for an export.
1278

1279
    @type instance: string
1280
    @param instance: Instance name
1281
    @type mode: string
1282
    @param mode: Export mode
1283
    @type reason: string
1284
    @param reason: the reason for executing this operation
1285
    @rtype: string
1286
    @return: Job ID
1287

1288
    """
1289
    query = [("mode", mode)]
1290
    _AppendReason(query, reason)
1291
    return self._SendRequest(HTTP_PUT,
1292
                             ("/%s/instances/%s/prepare-export" %
1293
                              (GANETI_RAPI_VERSION, instance)), query, None)
1294

    
1295
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1296
                     remove_instance=None,
1297
                     x509_key_name=None, destination_x509_ca=None, reason=None):
1298
    """Exports an instance.
1299

1300
    @type instance: string
1301
    @param instance: Instance name
1302
    @type mode: string
1303
    @param mode: Export mode
1304
    @type reason: string
1305
    @param reason: the reason for executing this operation
1306
    @rtype: string
1307
    @return: Job ID
1308

1309
    """
1310
    body = {
1311
      "destination": destination,
1312
      "mode": mode,
1313
      }
1314

    
1315
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1316
    _SetItemIf(body, remove_instance is not None,
1317
               "remove_instance", remove_instance)
1318
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1319
    _SetItemIf(body, destination_x509_ca is not None,
1320
               "destination_x509_ca", destination_x509_ca)
1321

    
1322
    query = []
1323
    _AppendReason(query, reason)
1324

    
1325
    return self._SendRequest(HTTP_PUT,
1326
                             ("/%s/instances/%s/export" %
1327
                              (GANETI_RAPI_VERSION, instance)), query, body)
1328

    
1329
  def MigrateInstance(self, instance, mode=None, cleanup=None,
1330
                      target_node=None, reason=None):
1331
    """Migrates an instance.
1332

1333
    @type instance: string
1334
    @param instance: Instance name
1335
    @type mode: string
1336
    @param mode: Migration mode
1337
    @type cleanup: bool
1338
    @param cleanup: Whether to clean up a previously failed migration
1339
    @type target_node: string
1340
    @param target_node: Target Node for externally mirrored instances
1341
    @type reason: string
1342
    @param reason: the reason for executing this operation
1343
    @rtype: string
1344
    @return: job id
1345

1346
    """
1347
    body = {}
1348
    _SetItemIf(body, mode is not None, "mode", mode)
1349
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1350
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1351

    
1352
    query = []
1353
    _AppendReason(query, reason)
1354

    
1355
    return self._SendRequest(HTTP_PUT,
1356
                             ("/%s/instances/%s/migrate" %
1357
                              (GANETI_RAPI_VERSION, instance)), query, body)
1358

    
1359
  def FailoverInstance(self, instance, iallocator=None,
1360
                       ignore_consistency=None, target_node=None, reason=None):
1361
    """Does a failover of an instance.
1362

1363
    @type instance: string
1364
    @param instance: Instance name
1365
    @type iallocator: string
1366
    @param iallocator: Iallocator for deciding the target node for
1367
      shared-storage instances
1368
    @type ignore_consistency: bool
1369
    @param ignore_consistency: Whether to ignore disk consistency
1370
    @type target_node: string
1371
    @param target_node: Target node for shared-storage instances
1372
    @type reason: string
1373
    @param reason: the reason for executing this operation
1374
    @rtype: string
1375
    @return: job id
1376

1377
    """
1378
    body = {}
1379
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1380
    _SetItemIf(body, ignore_consistency is not None,
1381
               "ignore_consistency", ignore_consistency)
1382
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1383

    
1384
    query = []
1385
    _AppendReason(query, reason)
1386

    
1387
    return self._SendRequest(HTTP_PUT,
1388
                             ("/%s/instances/%s/failover" %
1389
                              (GANETI_RAPI_VERSION, instance)), query, body)
1390

    
1391
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None,
1392
                     reason=None):
1393
    """Changes the name of an instance.
1394

1395
    @type instance: string
1396
    @param instance: Instance name
1397
    @type new_name: string
1398
    @param new_name: New instance name
1399
    @type ip_check: bool
1400
    @param ip_check: Whether to ensure instance's IP address is inactive
1401
    @type name_check: bool
1402
    @param name_check: Whether to ensure instance's name is resolvable
1403
    @type reason: string
1404
    @param reason: the reason for executing this operation
1405
    @rtype: string
1406
    @return: job id
1407

1408
    """
1409
    body = {
1410
      "new_name": new_name,
1411
      }
1412

    
1413
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1414
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1415

    
1416
    query = []
1417
    _AppendReason(query, reason)
1418

    
1419
    return self._SendRequest(HTTP_PUT,
1420
                             ("/%s/instances/%s/rename" %
1421
                              (GANETI_RAPI_VERSION, instance)), query, body)
1422

    
1423
  def GetInstanceConsole(self, instance, reason=None):
1424
    """Request information for connecting to instance's console.
1425

1426
    @type instance: string
1427
    @param instance: Instance name
1428
    @type reason: string
1429
    @param reason: the reason for executing this operation
1430
    @rtype: dict
1431
    @return: dictionary containing information about instance's console
1432

1433
    """
1434
    query = []
1435
    _AppendReason(query, reason)
1436
    return self._SendRequest(HTTP_GET,
1437
                             ("/%s/instances/%s/console" %
1438
                              (GANETI_RAPI_VERSION, instance)), query, None)
1439

    
1440
  def GetJobs(self, bulk=False):
1441
    """Gets all jobs for the cluster.
1442

1443
    @type bulk: bool
1444
    @param bulk: Whether to return detailed information about jobs.
1445
    @rtype: list of int
1446
    @return: List of job ids for the cluster or list of dicts with detailed
1447
             information about the jobs if bulk parameter was true.
1448

1449
    """
1450
    query = []
1451
    _AppendIf(query, bulk, ("bulk", 1))
1452

    
1453
    if bulk:
1454
      return self._SendRequest(HTTP_GET,
1455
                               "/%s/jobs" % GANETI_RAPI_VERSION,
1456
                               query, None)
1457
    else:
1458
      return [int(j["id"])
1459
              for j in self._SendRequest(HTTP_GET,
1460
                                         "/%s/jobs" % GANETI_RAPI_VERSION,
1461
                                         None, None)]
1462

    
1463
  def GetJobStatus(self, job_id):
1464
    """Gets the status of a job.
1465

1466
    @type job_id: string
1467
    @param job_id: job id whose status to query
1468

1469
    @rtype: dict
1470
    @return: job status
1471

1472
    """
1473
    return self._SendRequest(HTTP_GET,
1474
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1475
                             None, None)
1476

    
1477
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1478
    """Polls cluster for job status until completion.
1479

1480
    Completion is defined as any of the following states listed in
1481
    L{JOB_STATUS_FINALIZED}.
1482

1483
    @type job_id: string
1484
    @param job_id: job id to watch
1485
    @type period: int
1486
    @param period: how often to poll for status (optional, default 5s)
1487
    @type retries: int
1488
    @param retries: how many time to poll before giving up
1489
                    (optional, default -1 means unlimited)
1490

1491
    @rtype: bool
1492
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1493
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1494
      possible; L{WaitForJobChange} returns immediately after a job changed and
1495
      does not use polling
1496

1497
    """
1498
    while retries != 0:
1499
      job_result = self.GetJobStatus(job_id)
1500

    
1501
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1502
        return True
1503
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1504
        return False
1505

    
1506
      if period:
1507
        time.sleep(period)
1508

    
1509
      if retries > 0:
1510
        retries -= 1
1511

    
1512
    return False
1513

    
1514
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1515
    """Waits for job changes.
1516

1517
    @type job_id: string
1518
    @param job_id: Job ID for which to wait
1519
    @return: C{None} if no changes have been detected and a dict with two keys,
1520
      C{job_info} and C{log_entries} otherwise.
1521
    @rtype: dict
1522

1523
    """
1524
    body = {
1525
      "fields": fields,
1526
      "previous_job_info": prev_job_info,
1527
      "previous_log_serial": prev_log_serial,
1528
      }
1529

    
1530
    return self._SendRequest(HTTP_GET,
1531
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1532
                             None, body)
1533

    
1534
  def CancelJob(self, job_id, dry_run=False):
1535
    """Cancels a job.
1536

1537
    @type job_id: string
1538
    @param job_id: id of the job to delete
1539
    @type dry_run: bool
1540
    @param dry_run: whether to perform a dry run
1541
    @rtype: tuple
1542
    @return: tuple containing the result, and a message (bool, string)
1543

1544
    """
1545
    query = []
1546
    _AppendDryRunIf(query, dry_run)
1547

    
1548
    return self._SendRequest(HTTP_DELETE,
1549
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1550
                             query, None)
1551

    
1552
  def GetNodes(self, bulk=False, reason=None):
1553
    """Gets all nodes in the cluster.
1554

1555
    @type bulk: bool
1556
    @param bulk: whether to return all information about all instances
1557
    @type reason: string
1558
    @param reason: the reason for executing this operation
1559

1560
    @rtype: list of dict or str
1561
    @return: if bulk is true, info about nodes in the cluster,
1562
        else list of nodes in the cluster
1563

1564
    """
1565
    query = []
1566
    _AppendIf(query, bulk, ("bulk", 1))
1567
    _AppendReason(query, reason)
1568

    
1569
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1570
                              query, None)
1571
    if bulk:
1572
      return nodes
1573
    else:
1574
      return [n["id"] for n in nodes]
1575

    
1576
  def GetNode(self, node, reason=None):
1577
    """Gets information about a node.
1578

1579
    @type node: str
1580
    @param node: node whose info to return
1581
    @type reason: string
1582
    @param reason: the reason for executing this operation
1583

1584
    @rtype: dict
1585
    @return: info about the node
1586

1587
    """
1588
    query = []
1589
    _AppendReason(query, reason)
1590

    
1591
    return self._SendRequest(HTTP_GET,
1592
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1593
                             query, None)
1594

    
1595
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1596
                   dry_run=False, early_release=None,
1597
                   mode=None, accept_old=False, reason=None):
1598
    """Evacuates instances from a Ganeti node.
1599

1600
    @type node: str
1601
    @param node: node to evacuate
1602
    @type iallocator: str or None
1603
    @param iallocator: instance allocator to use
1604
    @type remote_node: str
1605
    @param remote_node: node to evaucate to
1606
    @type dry_run: bool
1607
    @param dry_run: whether to perform a dry run
1608
    @type early_release: bool
1609
    @param early_release: whether to enable parallelization
1610
    @type mode: string
1611
    @param mode: Node evacuation mode
1612
    @type accept_old: bool
1613
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1614
        results
1615
    @type reason: string
1616
    @param reason: the reason for executing this operation
1617

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

1624
    @raises GanetiApiError: if an iallocator and remote_node are both
1625
        specified
1626

1627
    """
1628
    if iallocator and remote_node:
1629
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1630

    
1631
    query = []
1632
    _AppendDryRunIf(query, dry_run)
1633
    _AppendReason(query, reason)
1634

    
1635
    if _NODE_EVAC_RES1 in self.GetFeatures():
1636
      # Server supports body parameters
1637
      body = {}
1638

    
1639
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1640
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1641
      _SetItemIf(body, early_release is not None,
1642
                 "early_release", early_release)
1643
      _SetItemIf(body, mode is not None, "mode", mode)
1644
    else:
1645
      # Pre-2.5 request format
1646
      body = None
1647

    
1648
      if not accept_old:
1649
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1650
                             " not accept old-style results (parameter"
1651
                             " accept_old)")
1652

    
1653
      # Pre-2.5 servers can only evacuate secondaries
1654
      if mode is not None and mode != NODE_EVAC_SEC:
1655
        raise GanetiApiError("Server can only evacuate secondary instances")
1656

    
1657
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1658
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1659
      _AppendIf(query, early_release, ("early_release", 1))
1660

    
1661
    return self._SendRequest(HTTP_POST,
1662
                             ("/%s/nodes/%s/evacuate" %
1663
                              (GANETI_RAPI_VERSION, node)), query, body)
1664

    
1665
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1666
                  target_node=None, reason=None):
1667
    """Migrates all primary instances from a node.
1668

1669
    @type node: str
1670
    @param node: node to migrate
1671
    @type mode: string
1672
    @param mode: if passed, it will overwrite the live migration type,
1673
        otherwise the hypervisor default will be used
1674
    @type dry_run: bool
1675
    @param dry_run: whether to perform a dry run
1676
    @type iallocator: string
1677
    @param iallocator: instance allocator to use
1678
    @type target_node: string
1679
    @param target_node: Target node for shared-storage instances
1680
    @type reason: string
1681
    @param reason: the reason for executing this operation
1682

1683
    @rtype: string
1684
    @return: job id
1685

1686
    """
1687
    query = []
1688
    _AppendDryRunIf(query, dry_run)
1689
    _AppendReason(query, reason)
1690

    
1691
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1692
      body = {}
1693

    
1694
      _SetItemIf(body, mode is not None, "mode", mode)
1695
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1696
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1697

    
1698
      assert len(query) <= 1
1699

    
1700
      return self._SendRequest(HTTP_POST,
1701
                               ("/%s/nodes/%s/migrate" %
1702
                                (GANETI_RAPI_VERSION, node)), query, body)
1703
    else:
1704
      # Use old request format
1705
      if target_node is not None:
1706
        raise GanetiApiError("Server does not support specifying target node"
1707
                             " for node migration")
1708

    
1709
      _AppendIf(query, mode is not None, ("mode", mode))
1710

    
1711
      return self._SendRequest(HTTP_POST,
1712
                               ("/%s/nodes/%s/migrate" %
1713
                                (GANETI_RAPI_VERSION, node)), query, None)
1714

    
1715
  def GetNodeRole(self, node, reason=None):
1716
    """Gets the current role for a node.
1717

1718
    @type node: str
1719
    @param node: node whose role to return
1720
    @type reason: string
1721
    @param reason: the reason for executing this operation
1722

1723
    @rtype: str
1724
    @return: the current role for a node
1725

1726
    """
1727
    query = []
1728
    _AppendReason(query, reason)
1729

    
1730
    return self._SendRequest(HTTP_GET,
1731
                             ("/%s/nodes/%s/role" %
1732
                              (GANETI_RAPI_VERSION, node)), query, None)
1733

    
1734
  def SetNodeRole(self, node, role, force=False, auto_promote=None,
1735
                  reason=None):
1736
    """Sets the role for a node.
1737

1738
    @type node: str
1739
    @param node: the node whose role to set
1740
    @type role: str
1741
    @param role: the role to set for the node
1742
    @type force: bool
1743
    @param force: whether to force the role change
1744
    @type auto_promote: bool
1745
    @param auto_promote: Whether node(s) should be promoted to master candidate
1746
                         if necessary
1747
    @type reason: string
1748
    @param reason: the reason for executing this operation
1749

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

1753
    """
1754
    query = []
1755
    _AppendForceIf(query, force)
1756
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1757
    _AppendReason(query, reason)
1758

    
1759
    return self._SendRequest(HTTP_PUT,
1760
                             ("/%s/nodes/%s/role" %
1761
                              (GANETI_RAPI_VERSION, node)), query, role)
1762

    
1763
  def PowercycleNode(self, node, force=False, reason=None):
1764
    """Powercycles a node.
1765

1766
    @type node: string
1767
    @param node: Node name
1768
    @type force: bool
1769
    @param force: Whether to force the operation
1770
    @type reason: string
1771
    @param reason: the reason for executing this operation
1772
    @rtype: string
1773
    @return: job id
1774

1775
    """
1776
    query = []
1777
    _AppendForceIf(query, force)
1778
    _AppendReason(query, reason)
1779

    
1780
    return self._SendRequest(HTTP_POST,
1781
                             ("/%s/nodes/%s/powercycle" %
1782
                              (GANETI_RAPI_VERSION, node)), query, None)
1783

    
1784
  def ModifyNode(self, node, reason=None, **kwargs):
1785
    """Modifies a node.
1786

1787
    More details for parameters can be found in the RAPI documentation.
1788

1789
    @type node: string
1790
    @param node: Node name
1791
    @type reason: string
1792
    @param reason: the reason for executing this operation
1793
    @rtype: string
1794
    @return: job id
1795

1796
    """
1797
    query = []
1798
    _AppendReason(query, reason)
1799

    
1800
    return self._SendRequest(HTTP_POST,
1801
                             ("/%s/nodes/%s/modify" %
1802
                              (GANETI_RAPI_VERSION, node)), query, kwargs)
1803

    
1804
  def GetNodeStorageUnits(self, node, storage_type, output_fields, reason=None):
1805
    """Gets the storage units for a node.
1806

1807
    @type node: str
1808
    @param node: the node whose storage units to return
1809
    @type storage_type: str
1810
    @param storage_type: storage type whose units to return
1811
    @type output_fields: str
1812
    @param output_fields: storage type fields to return
1813
    @type reason: string
1814
    @param reason: the reason for executing this operation
1815

1816
    @rtype: string
1817
    @return: job id where results can be retrieved
1818

1819
    """
1820
    query = [
1821
      ("storage_type", storage_type),
1822
      ("output_fields", output_fields),
1823
      ]
1824
    _AppendReason(query, reason)
1825

    
1826
    return self._SendRequest(HTTP_GET,
1827
                             ("/%s/nodes/%s/storage" %
1828
                              (GANETI_RAPI_VERSION, node)), query, None)
1829

    
1830
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None,
1831
                             reason=None):
1832
    """Modifies parameters of storage units on the node.
1833

1834
    @type node: str
1835
    @param node: node whose storage units to modify
1836
    @type storage_type: str
1837
    @param storage_type: storage type whose units to modify
1838
    @type name: str
1839
    @param name: name of the storage unit
1840
    @type allocatable: bool or None
1841
    @param allocatable: Whether to set the "allocatable" flag on the storage
1842
                        unit (None=no modification, True=set, False=unset)
1843
    @type reason: string
1844
    @param reason: the reason for executing this operation
1845

1846
    @rtype: string
1847
    @return: job id
1848

1849
    """
1850
    query = [
1851
      ("storage_type", storage_type),
1852
      ("name", name),
1853
      ]
1854

    
1855
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1856
    _AppendReason(query, reason)
1857

    
1858
    return self._SendRequest(HTTP_PUT,
1859
                             ("/%s/nodes/%s/storage/modify" %
1860
                              (GANETI_RAPI_VERSION, node)), query, None)
1861

    
1862
  def RepairNodeStorageUnits(self, node, storage_type, name, reason=None):
1863
    """Repairs a storage unit on the node.
1864

1865
    @type node: str
1866
    @param node: node whose storage units to repair
1867
    @type storage_type: str
1868
    @param storage_type: storage type to repair
1869
    @type name: str
1870
    @param name: name of the storage unit to repair
1871
    @type reason: string
1872
    @param reason: the reason for executing this operation
1873

1874
    @rtype: string
1875
    @return: job id
1876

1877
    """
1878
    query = [
1879
      ("storage_type", storage_type),
1880
      ("name", name),
1881
      ]
1882
    _AppendReason(query, reason)
1883

    
1884
    return self._SendRequest(HTTP_PUT,
1885
                             ("/%s/nodes/%s/storage/repair" %
1886
                              (GANETI_RAPI_VERSION, node)), query, None)
1887

    
1888
  def GetNodeTags(self, node, reason=None):
1889
    """Gets the tags for a node.
1890

1891
    @type node: str
1892
    @param node: node whose tags to return
1893
    @type reason: string
1894
    @param reason: the reason for executing this operation
1895

1896
    @rtype: list of str
1897
    @return: tags for the node
1898

1899
    """
1900
    query = []
1901
    _AppendReason(query, reason)
1902

    
1903
    return self._SendRequest(HTTP_GET,
1904
                             ("/%s/nodes/%s/tags" %
1905
                              (GANETI_RAPI_VERSION, node)), query, None)
1906

    
1907
  def AddNodeTags(self, node, tags, dry_run=False, reason=None):
1908
    """Adds tags to a node.
1909

1910
    @type node: str
1911
    @param node: node to add tags to
1912
    @type tags: list of str
1913
    @param tags: tags to add to the node
1914
    @type dry_run: bool
1915
    @param dry_run: whether to perform a dry run
1916
    @type reason: string
1917
    @param reason: the reason for executing this operation
1918

1919
    @rtype: string
1920
    @return: job id
1921

1922
    """
1923
    query = [("tag", t) for t in tags]
1924
    _AppendDryRunIf(query, dry_run)
1925
    _AppendReason(query, reason)
1926

    
1927
    return self._SendRequest(HTTP_PUT,
1928
                             ("/%s/nodes/%s/tags" %
1929
                              (GANETI_RAPI_VERSION, node)), query, tags)
1930

    
1931
  def DeleteNodeTags(self, node, tags, dry_run=False, reason=None):
1932
    """Delete tags from a node.
1933

1934
    @type node: str
1935
    @param node: node to remove tags from
1936
    @type tags: list of str
1937
    @param tags: tags to remove from the node
1938
    @type dry_run: bool
1939
    @param dry_run: whether to perform a dry run
1940
    @type reason: string
1941
    @param reason: the reason for executing this operation
1942

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

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

    
1951
    return self._SendRequest(HTTP_DELETE,
1952
                             ("/%s/nodes/%s/tags" %
1953
                              (GANETI_RAPI_VERSION, node)), query, None)
1954

    
1955
  def GetNetworks(self, bulk=False, reason=None):
1956
    """Gets all networks in the cluster.
1957

1958
    @type bulk: bool
1959
    @param bulk: whether to return all information about the networks
1960

1961
    @rtype: list of dict or str
1962
    @return: if bulk is true, a list of dictionaries with info about all
1963
        networks in the cluster, else a list of names of those networks
1964

1965
    """
1966
    query = []
1967
    _AppendIf(query, bulk, ("bulk", 1))
1968
    _AppendReason(query, reason)
1969

    
1970
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1971
                                 query, None)
1972
    if bulk:
1973
      return networks
1974
    else:
1975
      return [n["name"] for n in networks]
1976

    
1977
  def GetNetwork(self, network, reason=None):
1978
    """Gets information about a network.
1979

1980
    @type network: str
1981
    @param network: name of the network whose info to return
1982
    @type reason: string
1983
    @param reason: the reason for executing this operation
1984

1985
    @rtype: dict
1986
    @return: info about the network
1987

1988
    """
1989
    query = []
1990
    _AppendReason(query, reason)
1991

    
1992
    return self._SendRequest(HTTP_GET,
1993
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1994
                             query, None)
1995

    
1996
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1997
                    gateway6=None, mac_prefix=None,
1998
                    add_reserved_ips=None, tags=None, dry_run=False,
1999
                    reason=None):
2000
    """Creates a new network.
2001

2002
    @type network_name: str
2003
    @param network_name: the name of network to create
2004
    @type dry_run: bool
2005
    @param dry_run: whether to peform a dry run
2006
    @type reason: string
2007
    @param reason: the reason for executing this operation
2008

2009
    @rtype: string
2010
    @return: job id
2011

2012
    """
2013
    query = []
2014
    _AppendDryRunIf(query, dry_run)
2015
    _AppendReason(query, reason)
2016

    
2017
    if add_reserved_ips:
2018
      add_reserved_ips = add_reserved_ips.split(",")
2019

    
2020
    if tags:
2021
      tags = tags.split(",")
2022

    
2023
    body = {
2024
      "network_name": network_name,
2025
      "gateway": gateway,
2026
      "network": network,
2027
      "gateway6": gateway6,
2028
      "network6": network6,
2029
      "mac_prefix": mac_prefix,
2030
      "add_reserved_ips": add_reserved_ips,
2031
      "tags": tags,
2032
      }
2033

    
2034
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
2035
                             query, body)
2036

    
2037
  def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False,
2038
                     reason=None):
2039
    """Connects a Network to a NodeGroup with the given netparams
2040

2041
    """
2042
    body = {
2043
      "group_name": group_name,
2044
      "network_mode": mode,
2045
      "network_link": link,
2046
      }
2047

    
2048
    query = []
2049
    _AppendDryRunIf(query, dry_run)
2050
    _AppendReason(query, reason)
2051

    
2052
    return self._SendRequest(HTTP_PUT,
2053
                             ("/%s/networks/%s/connect" %
2054
                             (GANETI_RAPI_VERSION, network_name)), query, body)
2055

    
2056
  def DisconnectNetwork(self, network_name, group_name, dry_run=False,
2057
                        reason=None):
2058
    """Connects a Network to a NodeGroup with the given netparams
2059

2060
    """
2061
    body = {
2062
      "group_name": group_name,
2063
      }
2064

    
2065
    query = []
2066
    _AppendDryRunIf(query, dry_run)
2067
    _AppendReason(query, reason)
2068

    
2069
    return self._SendRequest(HTTP_PUT,
2070
                             ("/%s/networks/%s/disconnect" %
2071
                             (GANETI_RAPI_VERSION, network_name)), query, body)
2072

    
2073
  def ModifyNetwork(self, network, reason=None, **kwargs):
2074
    """Modifies a network.
2075

2076
    More details for parameters can be found in the RAPI documentation.
2077

2078
    @type network: string
2079
    @param network: Network name
2080
    @type reason: string
2081
    @param reason: the reason for executing this operation
2082
    @rtype: string
2083
    @return: job id
2084

2085
    """
2086
    query = []
2087
    _AppendReason(query, reason)
2088

    
2089
    return self._SendRequest(HTTP_PUT,
2090
                             ("/%s/networks/%s/modify" %
2091
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
2092

    
2093
  def DeleteNetwork(self, network, dry_run=False, reason=None):
2094
    """Deletes a network.
2095

2096
    @type network: str
2097
    @param network: the network to delete
2098
    @type dry_run: bool
2099
    @param dry_run: whether to peform a dry run
2100
    @type reason: string
2101
    @param reason: the reason for executing this operation
2102

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

2106
    """
2107
    query = []
2108
    _AppendDryRunIf(query, dry_run)
2109
    _AppendReason(query, reason)
2110

    
2111
    return self._SendRequest(HTTP_DELETE,
2112
                             ("/%s/networks/%s" %
2113
                              (GANETI_RAPI_VERSION, network)), query, None)
2114

    
2115
  def GetNetworkTags(self, network, reason=None):
2116
    """Gets tags for a network.
2117

2118
    @type network: string
2119
    @param network: Node group whose tags to return
2120
    @type reason: string
2121
    @param reason: the reason for executing this operation
2122

2123
    @rtype: list of strings
2124
    @return: tags for the network
2125

2126
    """
2127
    query = []
2128
    _AppendReason(query, reason)
2129

    
2130
    return self._SendRequest(HTTP_GET,
2131
                             ("/%s/networks/%s/tags" %
2132
                              (GANETI_RAPI_VERSION, network)), query, None)
2133

    
2134
  def AddNetworkTags(self, network, tags, dry_run=False, reason=None):
2135
    """Adds tags to a network.
2136

2137
    @type network: str
2138
    @param network: network to add tags to
2139
    @type tags: list of string
2140
    @param tags: tags to add to the network
2141
    @type dry_run: bool
2142
    @param dry_run: whether to perform a dry run
2143
    @type reason: string
2144
    @param reason: the reason for executing this operation
2145

2146
    @rtype: string
2147
    @return: job id
2148

2149
    """
2150
    query = [("tag", t) for t in tags]
2151
    _AppendDryRunIf(query, dry_run)
2152
    _AppendReason(query, reason)
2153

    
2154
    return self._SendRequest(HTTP_PUT,
2155
                             ("/%s/networks/%s/tags" %
2156
                              (GANETI_RAPI_VERSION, network)), query, None)
2157

    
2158
  def DeleteNetworkTags(self, network, tags, dry_run=False, reason=None):
2159
    """Deletes tags from a network.
2160

2161
    @type network: str
2162
    @param network: network to delete tags from
2163
    @type tags: list of string
2164
    @param tags: tags to delete
2165
    @type dry_run: bool
2166
    @param dry_run: whether to perform a dry run
2167
    @type reason: string
2168
    @param reason: the reason for executing this operation
2169
    @rtype: string
2170
    @return: job id
2171

2172
    """
2173
    query = [("tag", t) for t in tags]
2174
    _AppendDryRunIf(query, dry_run)
2175
    _AppendReason(query, reason)
2176

    
2177
    return self._SendRequest(HTTP_DELETE,
2178
                             ("/%s/networks/%s/tags" %
2179
                              (GANETI_RAPI_VERSION, network)), query, None)
2180

    
2181
  def GetGroups(self, bulk=False, reason=None):
2182
    """Gets all node groups in the cluster.
2183

2184
    @type bulk: bool
2185
    @param bulk: whether to return all information about the groups
2186
    @type reason: string
2187
    @param reason: the reason for executing this operation
2188

2189
    @rtype: list of dict or str
2190
    @return: if bulk is true, a list of dictionaries with info about all node
2191
        groups in the cluster, else a list of names of those node groups
2192

2193
    """
2194
    query = []
2195
    _AppendIf(query, bulk, ("bulk", 1))
2196
    _AppendReason(query, reason)
2197

    
2198
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
2199
                               query, None)
2200
    if bulk:
2201
      return groups
2202
    else:
2203
      return [g["name"] for g in groups]
2204

    
2205
  def GetGroup(self, group, reason=None):
2206
    """Gets information about a node group.
2207

2208
    @type group: str
2209
    @param group: name of the node group whose info to return
2210
    @type reason: string
2211
    @param reason: the reason for executing this operation
2212

2213
    @rtype: dict
2214
    @return: info about the node group
2215

2216
    """
2217
    query = []
2218
    _AppendReason(query, reason)
2219

    
2220
    return self._SendRequest(HTTP_GET,
2221
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
2222
                             query, None)
2223

    
2224
  def CreateGroup(self, name, alloc_policy=None, dry_run=False, reason=None):
2225
    """Creates a new node group.
2226

2227
    @type name: str
2228
    @param name: the name of node group to create
2229
    @type alloc_policy: str
2230
    @param alloc_policy: the desired allocation policy for the group, if any
2231
    @type dry_run: bool
2232
    @param dry_run: whether to peform a dry run
2233
    @type reason: string
2234
    @param reason: the reason for executing this operation
2235

2236
    @rtype: string
2237
    @return: job id
2238

2239
    """
2240
    query = []
2241
    _AppendDryRunIf(query, dry_run)
2242
    _AppendReason(query, reason)
2243

    
2244
    body = {
2245
      "name": name,
2246
      "alloc_policy": alloc_policy,
2247
      }
2248

    
2249
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
2250
                             query, body)
2251

    
2252
  def ModifyGroup(self, group, reason=None, **kwargs):
2253
    """Modifies a node group.
2254

2255
    More details for parameters can be found in the RAPI documentation.
2256

2257
    @type group: string
2258
    @param group: Node group name
2259
    @type reason: string
2260
    @param reason: the reason for executing this operation
2261
    @rtype: string
2262
    @return: job id
2263

2264
    """
2265
    query = []
2266
    _AppendReason(query, reason)
2267

    
2268
    return self._SendRequest(HTTP_PUT,
2269
                             ("/%s/groups/%s/modify" %
2270
                              (GANETI_RAPI_VERSION, group)), query, kwargs)
2271

    
2272
  def DeleteGroup(self, group, dry_run=False, reason=None):
2273
    """Deletes a node group.
2274

2275
    @type group: str
2276
    @param group: the node group to delete
2277
    @type dry_run: bool
2278
    @param dry_run: whether to peform a dry run
2279
    @type reason: string
2280
    @param reason: the reason for executing this operation
2281

2282
    @rtype: string
2283
    @return: job id
2284

2285
    """
2286
    query = []
2287
    _AppendDryRunIf(query, dry_run)
2288
    _AppendReason(query, reason)
2289

    
2290
    return self._SendRequest(HTTP_DELETE,
2291
                             ("/%s/groups/%s" %
2292
                              (GANETI_RAPI_VERSION, group)), query, None)
2293

    
2294
  def RenameGroup(self, group, new_name, reason=None):
2295
    """Changes the name of a node group.
2296

2297
    @type group: string
2298
    @param group: Node group name
2299
    @type new_name: string
2300
    @param new_name: New node group name
2301
    @type reason: string
2302
    @param reason: the reason for executing this operation
2303

2304
    @rtype: string
2305
    @return: job id
2306

2307
    """
2308
    body = {
2309
      "new_name": new_name,
2310
      }
2311

    
2312
    query = []
2313
    _AppendReason(query, reason)
2314

    
2315
    return self._SendRequest(HTTP_PUT,
2316
                             ("/%s/groups/%s/rename" %
2317
                              (GANETI_RAPI_VERSION, group)), query, body)
2318

    
2319
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False,
2320
                       reason=None):
2321
    """Assigns nodes to a group.
2322

2323
    @type group: string
2324
    @param group: Node group name
2325
    @type nodes: list of strings
2326
    @param nodes: List of nodes to assign to the group
2327
    @type reason: string
2328
    @param reason: the reason for executing this operation
2329

2330
    @rtype: string
2331
    @return: job id
2332

2333
    """
2334
    query = []
2335
    _AppendForceIf(query, force)
2336
    _AppendDryRunIf(query, dry_run)
2337
    _AppendReason(query, reason)
2338

    
2339
    body = {
2340
      "nodes": nodes,
2341
      }
2342

    
2343
    return self._SendRequest(HTTP_PUT,
2344
                             ("/%s/groups/%s/assign-nodes" %
2345
                             (GANETI_RAPI_VERSION, group)), query, body)
2346

    
2347
  def GetGroupTags(self, group, reason=None):
2348
    """Gets tags for a node group.
2349

2350
    @type group: string
2351
    @param group: Node group whose tags to return
2352
    @type reason: string
2353
    @param reason: the reason for executing this operation
2354

2355
    @rtype: list of strings
2356
    @return: tags for the group
2357

2358
    """
2359
    query = []
2360
    _AppendReason(query, reason)
2361

    
2362
    return self._SendRequest(HTTP_GET,
2363
                             ("/%s/groups/%s/tags" %
2364
                              (GANETI_RAPI_VERSION, group)), query, None)
2365

    
2366
  def AddGroupTags(self, group, tags, dry_run=False, reason=None):
2367
    """Adds tags to a node group.
2368

2369
    @type group: str
2370
    @param group: group to add tags to
2371
    @type tags: list of string
2372
    @param tags: tags to add to the group
2373
    @type dry_run: bool
2374
    @param dry_run: whether to perform a dry run
2375
    @type reason: string
2376
    @param reason: the reason for executing this operation
2377

2378
    @rtype: string
2379
    @return: job id
2380

2381
    """
2382
    query = [("tag", t) for t in tags]
2383
    _AppendDryRunIf(query, dry_run)
2384
    _AppendReason(query, reason)
2385

    
2386
    return self._SendRequest(HTTP_PUT,
2387
                             ("/%s/groups/%s/tags" %
2388
                              (GANETI_RAPI_VERSION, group)), query, None)
2389

    
2390
  def DeleteGroupTags(self, group, tags, dry_run=False, reason=None):
2391
    """Deletes tags from a node group.
2392

2393
    @type group: str
2394
    @param group: group to delete tags from
2395
    @type tags: list of string
2396
    @param tags: tags to delete
2397
    @type dry_run: bool
2398
    @param dry_run: whether to perform a dry run
2399
    @type reason: string
2400
    @param reason: the reason for executing this operation
2401
    @rtype: string
2402
    @return: job id
2403

2404
    """
2405
    query = [("tag", t) for t in tags]
2406
    _AppendDryRunIf(query, dry_run)
2407
    _AppendReason(query, reason)
2408

    
2409
    return self._SendRequest(HTTP_DELETE,
2410
                             ("/%s/groups/%s/tags" %
2411
                              (GANETI_RAPI_VERSION, group)), query, None)
2412

    
2413
  def Query(self, what, fields, qfilter=None, reason=None):
2414
    """Retrieves information about resources.
2415

2416
    @type what: string
2417
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2418
    @type fields: list of string
2419
    @param fields: Requested fields
2420
    @type qfilter: None or list
2421
    @param qfilter: Query filter
2422
    @type reason: string
2423
    @param reason: the reason for executing this operation
2424

2425
    @rtype: string
2426
    @return: job id
2427

2428
    """
2429
    query = []
2430
    _AppendReason(query, reason)
2431

    
2432
    body = {
2433
      "fields": fields,
2434
      }
2435

    
2436
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2437
    # TODO: remove "filter" after 2.7
2438
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
2439

    
2440
    return self._SendRequest(HTTP_PUT,
2441
                             ("/%s/query/%s" %
2442
                              (GANETI_RAPI_VERSION, what)), query, body)
2443

    
2444
  def QueryFields(self, what, fields=None, reason=None):
2445
    """Retrieves available fields for a resource.
2446

2447
    @type what: string
2448
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2449
    @type fields: list of string
2450
    @param fields: Requested fields
2451
    @type reason: string
2452
    @param reason: the reason for executing this operation
2453

2454
    @rtype: string
2455
    @return: job id
2456

2457
    """
2458
    query = []
2459
    _AppendReason(query, reason)
2460

    
2461
    if fields is not None:
2462
      _AppendIf(query, True, ("fields", ",".join(fields)))
2463

    
2464
    return self._SendRequest(HTTP_GET,
2465
                             ("/%s/query/%s/fields" %
2466
                              (GANETI_RAPI_VERSION, what)), query, None)