Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / rapi.py @ 1ad47ca5

History | View | Annotate | Download (60.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010, 2011 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Ganeti RAPI client.
23

24
@attention: To use the RAPI client, the application B{must} call
25
            C{pycurl.global_init} during initialization and
26
            C{pycurl.global_cleanup} before exiting the process. This is very
27
            important in multi-threaded programs. See curl_global_init(3) and
28
            curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
29
            can be used.
30

31
"""
32

    
33
# No Ganeti-specific modules should be imported. The RAPI client is supposed to
34
# be standalone.
35

    
36
import logging
37
import simplejson
38
import socket
39
import urllib
40
import threading
41
import pycurl
42
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_FINALIZED = frozenset([
84
  JOB_STATUS_CANCELED,
85
  JOB_STATUS_SUCCESS,
86
  JOB_STATUS_ERROR,
87
  ])
88
JOB_STATUS_ALL = frozenset([
89
  JOB_STATUS_QUEUED,
90
  JOB_STATUS_WAITING,
91
  JOB_STATUS_CANCELING,
92
  JOB_STATUS_RUNNING,
93
  ]) | JOB_STATUS_FINALIZED
94

    
95
# Legacy name
96
JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
97

    
98
# Internal constants
99
_REQ_DATA_VERSION_FIELD = "__version__"
100
_QPARAM_DRY_RUN = "dry-run"
101
_QPARAM_FORCE = "force"
102

    
103
# Feature strings
104
INST_CREATE_REQV1 = "instance-create-reqv1"
105
INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
106
NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
107
NODE_EVAC_RES1 = "node-evac-res1"
108

    
109
# Old feature constant names in case they're references by users of this module
110
_INST_CREATE_REQV1 = INST_CREATE_REQV1
111
_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
112
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
113
_NODE_EVAC_RES1 = NODE_EVAC_RES1
114

    
115
# Older pycURL versions don't have all error constants
116
try:
117
  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
118
  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
119
except AttributeError:
120
  _CURLE_SSL_CACERT = 60
121
  _CURLE_SSL_CACERT_BADFILE = 77
122

    
123
_CURL_SSL_CERT_ERRORS = frozenset([
124
  _CURLE_SSL_CACERT,
125
  _CURLE_SSL_CACERT_BADFILE,
126
  ])
127

    
128

    
129
class Error(Exception):
130
  """Base error class for this module.
131

132
  """
133
  pass
134

    
135

    
136
class GanetiApiError(Error):
137
  """Generic error raised from Ganeti API.
138

139
  """
140
  def __init__(self, msg, code=None):
141
    Error.__init__(self, msg)
142
    self.code = code
143

    
144

    
145
class CertificateError(GanetiApiError):
146
  """Raised when a problem is found with the SSL certificate.
147

148
  """
149
  pass
150

    
151

    
152
def _AppendIf(container, condition, value):
153
  """Appends to a list if a condition evaluates to truth.
154

155
  """
156
  if condition:
157
    container.append(value)
158

    
159
  return condition
160

    
161

    
162
def _AppendDryRunIf(container, condition):
163
  """Appends a "dry-run" parameter if a condition evaluates to truth.
164

165
  """
166
  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
167

    
168

    
169
def _AppendForceIf(container, condition):
170
  """Appends a "force" parameter if a condition evaluates to truth.
171

172
  """
173
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
174

    
175

    
176
def _SetItemIf(container, condition, item, value):
177
  """Sets an item if a condition evaluates to truth.
178

179
  """
180
  if condition:
181
    container[item] = value
182

    
183
  return condition
184

    
185

    
186
def UsesRapiClient(fn):
187
  """Decorator for code using RAPI client to initialize pycURL.
188

189
  """
190
  def wrapper(*args, **kwargs):
191
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
192
    # one thread running. This check is just a safety measure -- it doesn't
193
    # cover all cases.
194
    assert threading.activeCount() == 1, \
195
           "Found active threads when initializing pycURL"
196

    
197
    pycurl.global_init(pycurl.GLOBAL_ALL)
198
    try:
199
      return fn(*args, **kwargs)
200
    finally:
201
      pycurl.global_cleanup()
202

    
203
  return wrapper
204

    
205

    
206
def GenericCurlConfig(verbose=False, use_signal=False,
207
                      use_curl_cabundle=False, cafile=None, capath=None,
208
                      proxy=None, verify_hostname=False,
209
                      connect_timeout=None, timeout=None,
210
                      _pycurl_version_fn=pycurl.version_info):
211
  """Curl configuration function generator.
212

213
  @type verbose: bool
214
  @param verbose: Whether to set cURL to verbose mode
215
  @type use_signal: bool
216
  @param use_signal: Whether to allow cURL to use signals
217
  @type use_curl_cabundle: bool
218
  @param use_curl_cabundle: Whether to use cURL's default CA bundle
219
  @type cafile: string
220
  @param cafile: In which file we can find the certificates
221
  @type capath: string
222
  @param capath: In which directory we can find the certificates
223
  @type proxy: string
224
  @param proxy: Proxy to use, None for default behaviour and empty string for
225
                disabling proxies (see curl_easy_setopt(3))
226
  @type verify_hostname: bool
227
  @param verify_hostname: Whether to verify the remote peer certificate's
228
                          commonName
229
  @type connect_timeout: number
230
  @param connect_timeout: Timeout for establishing connection in seconds
231
  @type timeout: number
232
  @param timeout: Timeout for complete transfer in seconds (see
233
                  curl_easy_setopt(3)).
234

235
  """
236
  if use_curl_cabundle and (cafile or capath):
237
    raise Error("Can not use default CA bundle when CA file or path is set")
238

    
239
  def _ConfigCurl(curl, logger):
240
    """Configures a cURL object
241

242
    @type curl: pycurl.Curl
243
    @param curl: cURL object
244

245
    """
246
    logger.debug("Using cURL version %s", pycurl.version)
247

    
248
    # pycurl.version_info returns a tuple with information about the used
249
    # version of libcurl. Item 5 is the SSL library linked to it.
250
    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
251
    # 0, '1.2.3.3', ...)
252
    sslver = _pycurl_version_fn()[5]
253
    if not sslver:
254
      raise Error("No SSL support in cURL")
255

    
256
    lcsslver = sslver.lower()
257
    if lcsslver.startswith("openssl/"):
258
      pass
259
    elif lcsslver.startswith("nss/"):
260
      # TODO: investigate compatibility beyond a simple test
261
      pass
262
    elif lcsslver.startswith("gnutls/"):
263
      if capath:
264
        raise Error("cURL linked against GnuTLS has no support for a"
265
                    " CA path (%s)" % (pycurl.version, ))
266
    else:
267
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
268
                                sslver)
269

    
270
    curl.setopt(pycurl.VERBOSE, verbose)
271
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
272

    
273
    # Whether to verify remote peer's CN
274
    if verify_hostname:
275
      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
276
      # certificate must indicate that the server is the server to which you
277
      # meant to connect, or the connection fails. [...] When the value is 1,
278
      # the certificate must contain a Common Name field, but it doesn't matter
279
      # what name it says. [...]"
280
      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
281
    else:
282
      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
283

    
284
    if cafile or capath or use_curl_cabundle:
285
      # Require certificates to be checked
286
      curl.setopt(pycurl.SSL_VERIFYPEER, True)
287
      if cafile:
288
        curl.setopt(pycurl.CAINFO, str(cafile))
289
      if capath:
290
        curl.setopt(pycurl.CAPATH, str(capath))
291
      # Not changing anything for using default CA bundle
292
    else:
293
      # Disable SSL certificate verification
294
      curl.setopt(pycurl.SSL_VERIFYPEER, False)
295

    
296
    if proxy is not None:
297
      curl.setopt(pycurl.PROXY, str(proxy))
298

    
299
    # Timeouts
300
    if connect_timeout is not None:
301
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
302
    if timeout is not None:
303
      curl.setopt(pycurl.TIMEOUT, timeout)
304

    
305
  return _ConfigCurl
306

    
307

    
308
class GanetiRapiClient(object): # pylint: disable=R0904
309
  """Ganeti RAPI client.
310

311
  """
312
  USER_AGENT = "Ganeti RAPI Client"
313
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
314

    
315
  def __init__(self, host, port=GANETI_RAPI_PORT,
316
               username=None, password=None, logger=logging,
317
               curl_config_fn=None, curl_factory=None):
318
    """Initializes this class.
319

320
    @type host: string
321
    @param host: the ganeti cluster master to interact with
322
    @type port: int
323
    @param port: the port on which the RAPI is running (default is 5080)
324
    @type username: string
325
    @param username: the username to connect with
326
    @type password: string
327
    @param password: the password to connect with
328
    @type curl_config_fn: callable
329
    @param curl_config_fn: Function to configure C{pycurl.Curl} object
330
    @param logger: Logging object
331

332
    """
333
    self._username = username
334
    self._password = password
335
    self._logger = logger
336
    self._curl_config_fn = curl_config_fn
337
    self._curl_factory = curl_factory
338

    
339
    try:
340
      socket.inet_pton(socket.AF_INET6, host)
341
      address = "[%s]:%s" % (host, port)
342
    except socket.error:
343
      address = "%s:%s" % (host, port)
344

    
345
    self._base_url = "https://%s" % address
346

    
347
    if username is not None:
348
      if password is None:
349
        raise Error("Password not specified")
350
    elif password:
351
      raise Error("Specified password without username")
352

    
353
  def _CreateCurl(self):
354
    """Creates a cURL object.
355

356
    """
357
    # Create pycURL object if no factory is provided
358
    if self._curl_factory:
359
      curl = self._curl_factory()
360
    else:
361
      curl = pycurl.Curl()
362

    
363
    # Default cURL settings
364
    curl.setopt(pycurl.VERBOSE, False)
365
    curl.setopt(pycurl.FOLLOWLOCATION, False)
366
    curl.setopt(pycurl.MAXREDIRS, 5)
367
    curl.setopt(pycurl.NOSIGNAL, True)
368
    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
369
    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
370
    curl.setopt(pycurl.SSL_VERIFYPEER, False)
371
    curl.setopt(pycurl.HTTPHEADER, [
372
      "Accept: %s" % HTTP_APP_JSON,
373
      "Content-type: %s" % HTTP_APP_JSON,
374
      ])
375

    
376
    assert ((self._username is None and self._password is None) ^
377
            (self._username is not None and self._password is not None))
378

    
379
    if self._username:
380
      # Setup authentication
381
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
382
      curl.setopt(pycurl.USERPWD,
383
                  str("%s:%s" % (self._username, self._password)))
384

    
385
    # Call external configuration function
386
    if self._curl_config_fn:
387
      self._curl_config_fn(curl, self._logger)
388

    
389
    return curl
390

    
391
  @staticmethod
392
  def _EncodeQuery(query):
393
    """Encode query values for RAPI URL.
394

395
    @type query: list of two-tuples
396
    @param query: Query arguments
397
    @rtype: list
398
    @return: Query list with encoded values
399

400
    """
401
    result = []
402

    
403
    for name, value in query:
404
      if value is None:
405
        result.append((name, ""))
406

    
407
      elif isinstance(value, bool):
408
        # Boolean values must be encoded as 0 or 1
409
        result.append((name, int(value)))
410

    
411
      elif isinstance(value, (list, tuple, dict)):
412
        raise ValueError("Invalid query data type %r" % type(value).__name__)
413

    
414
      else:
415
        result.append((name, value))
416

    
417
    return result
418

    
419
  def _SendRequest(self, method, path, query, content):
420
    """Sends an HTTP request.
421

422
    This constructs a full URL, encodes and decodes HTTP bodies, and
423
    handles invalid responses in a pythonic way.
424

425
    @type method: string
426
    @param method: HTTP method to use
427
    @type path: string
428
    @param path: HTTP URL path
429
    @type query: list of two-tuples
430
    @param query: query arguments to pass to urllib.urlencode
431
    @type content: str or None
432
    @param content: HTTP body content
433

434
    @rtype: str
435
    @return: JSON-Decoded response
436

437
    @raises CertificateError: If an invalid SSL certificate is found
438
    @raises GanetiApiError: If an invalid response is returned
439

440
    """
441
    assert path.startswith("/")
442

    
443
    curl = self._CreateCurl()
444

    
445
    if content is not None:
446
      encoded_content = self._json_encoder.encode(content)
447
    else:
448
      encoded_content = ""
449

    
450
    # Build URL
451
    urlparts = [self._base_url, path]
452
    if query:
453
      urlparts.append("?")
454
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
455

    
456
    url = "".join(urlparts)
457

    
458
    self._logger.debug("Sending request %s %s (content=%r)",
459
                       method, url, encoded_content)
460

    
461
    # Buffer for response
462
    encoded_resp_body = StringIO()
463

    
464
    # Configure cURL
465
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
466
    curl.setopt(pycurl.URL, str(url))
467
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
468
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
469

    
470
    try:
471
      # Send request and wait for response
472
      try:
473
        curl.perform()
474
      except pycurl.error, err:
475
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
476
          raise CertificateError("SSL certificate error %s" % err,
477
                                 code=err.args[0])
478

    
479
        raise GanetiApiError(str(err), code=err.args[0])
480
    finally:
481
      # Reset settings to not keep references to large objects in memory
482
      # between requests
483
      curl.setopt(pycurl.POSTFIELDS, "")
484
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
485

    
486
    # Get HTTP response code
487
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
488

    
489
    # Was anything written to the response buffer?
490
    if encoded_resp_body.tell():
491
      response_content = simplejson.loads(encoded_resp_body.getvalue())
492
    else:
493
      response_content = None
494

    
495
    if http_code != HTTP_OK:
496
      if isinstance(response_content, dict):
497
        msg = ("%s %s: %s" %
498
               (response_content["code"],
499
                response_content["message"],
500
                response_content["explain"]))
501
      else:
502
        msg = str(response_content)
503

    
504
      raise GanetiApiError(msg, code=http_code)
505

    
506
    return response_content
507

    
508
  def GetVersion(self):
509
    """Gets the Remote API version running on the cluster.
510

511
    @rtype: int
512
    @return: Ganeti Remote API version
513

514
    """
515
    return self._SendRequest(HTTP_GET, "/version", None, None)
516

    
517
  def GetFeatures(self):
518
    """Gets the list of optional features supported by RAPI server.
519

520
    @rtype: list
521
    @return: List of optional features
522

523
    """
524
    try:
525
      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
526
                               None, None)
527
    except GanetiApiError, err:
528
      # Older RAPI servers don't support this resource
529
      if err.code == HTTP_NOT_FOUND:
530
        return []
531

    
532
      raise
533

    
534
  def GetOperatingSystems(self):
535
    """Gets the Operating Systems running in the Ganeti cluster.
536

537
    @rtype: list of str
538
    @return: operating systems
539

540
    """
541
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
542
                             None, None)
543

    
544
  def GetInfo(self):
545
    """Gets info about the cluster.
546

547
    @rtype: dict
548
    @return: information about the cluster
549

550
    """
551
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
552
                             None, None)
553

    
554
  def RedistributeConfig(self):
555
    """Tells the cluster to redistribute its configuration files.
556

557
    @rtype: string
558
    @return: job id
559

560
    """
561
    return self._SendRequest(HTTP_PUT,
562
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
563
                             None, None)
564

    
565
  def ModifyCluster(self, **kwargs):
566
    """Modifies cluster parameters.
567

568
    More details for parameters can be found in the RAPI documentation.
569

570
    @rtype: string
571
    @return: job id
572

573
    """
574
    body = kwargs
575

    
576
    return self._SendRequest(HTTP_PUT,
577
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
578

    
579
  def GetClusterTags(self):
580
    """Gets the cluster tags.
581

582
    @rtype: list of str
583
    @return: cluster tags
584

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

    
589
  def AddClusterTags(self, tags, dry_run=False):
590
    """Adds tags to the cluster.
591

592
    @type tags: list of str
593
    @param tags: tags to add to the cluster
594
    @type dry_run: bool
595
    @param dry_run: whether to perform a dry run
596

597
    @rtype: string
598
    @return: job id
599

600
    """
601
    query = [("tag", t) for t in tags]
602
    _AppendDryRunIf(query, dry_run)
603

    
604
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
605
                             query, None)
606

    
607
  def DeleteClusterTags(self, tags, dry_run=False):
608
    """Deletes tags from the cluster.
609

610
    @type tags: list of str
611
    @param tags: tags to delete
612
    @type dry_run: bool
613
    @param dry_run: whether to perform a dry run
614
    @rtype: string
615
    @return: job id
616

617
    """
618
    query = [("tag", t) for t in tags]
619
    _AppendDryRunIf(query, dry_run)
620

    
621
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
622
                             query, None)
623

    
624
  def GetInstances(self, bulk=False):
625
    """Gets information about instances on the cluster.
626

627
    @type bulk: bool
628
    @param bulk: whether to return all information about all instances
629

630
    @rtype: list of dict or list of str
631
    @return: if bulk is True, info about the instances, else a list of instances
632

633
    """
634
    query = []
635
    _AppendIf(query, bulk, ("bulk", 1))
636

    
637
    instances = self._SendRequest(HTTP_GET,
638
                                  "/%s/instances" % GANETI_RAPI_VERSION,
639
                                  query, None)
640
    if bulk:
641
      return instances
642
    else:
643
      return [i["id"] for i in instances]
644

    
645
  def GetInstance(self, instance):
646
    """Gets information about an instance.
647

648
    @type instance: str
649
    @param instance: instance whose info to return
650

651
    @rtype: dict
652
    @return: info about the instance
653

654
    """
655
    return self._SendRequest(HTTP_GET,
656
                             ("/%s/instances/%s" %
657
                              (GANETI_RAPI_VERSION, instance)), None, None)
658

    
659
  def GetInstanceInfo(self, instance, static=None):
660
    """Gets information about an instance.
661

662
    @type instance: string
663
    @param instance: Instance name
664
    @rtype: string
665
    @return: Job ID
666

667
    """
668
    if static is not None:
669
      query = [("static", static)]
670
    else:
671
      query = None
672

    
673
    return self._SendRequest(HTTP_GET,
674
                             ("/%s/instances/%s/info" %
675
                              (GANETI_RAPI_VERSION, instance)), query, None)
676

    
677
  def CreateInstance(self, mode, name, disk_template, disks, nics,
678
                     **kwargs):
679
    """Creates a new instance.
680

681
    More details for parameters can be found in the RAPI documentation.
682

683
    @type mode: string
684
    @param mode: Instance creation mode
685
    @type name: string
686
    @param name: Hostname of the instance to create
687
    @type disk_template: string
688
    @param disk_template: Disk template for instance (e.g. plain, diskless,
689
                          file, or drbd)
690
    @type disks: list of dicts
691
    @param disks: List of disk definitions
692
    @type nics: list of dicts
693
    @param nics: List of NIC definitions
694
    @type dry_run: bool
695
    @keyword dry_run: whether to perform a dry run
696

697
    @rtype: string
698
    @return: job id
699

700
    """
701
    query = []
702

    
703
    _AppendDryRunIf(query, kwargs.get("dry_run"))
704

    
705
    if _INST_CREATE_REQV1 in self.GetFeatures():
706
      # All required fields for request data version 1
707
      body = {
708
        _REQ_DATA_VERSION_FIELD: 1,
709
        "mode": mode,
710
        "name": name,
711
        "disk_template": disk_template,
712
        "disks": disks,
713
        "nics": nics,
714
        }
715

    
716
      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
717
      if conflicts:
718
        raise GanetiApiError("Required fields can not be specified as"
719
                             " keywords: %s" % ", ".join(conflicts))
720

    
721
      body.update((key, value) for key, value in kwargs.iteritems()
722
                  if key != "dry_run")
723
    else:
724
      raise GanetiApiError("Server does not support new-style (version 1)"
725
                           " instance creation requests")
726

    
727
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
728
                             query, body)
729

    
730
  def DeleteInstance(self, instance, dry_run=False):
731
    """Deletes an instance.
732

733
    @type instance: str
734
    @param instance: the instance to delete
735

736
    @rtype: string
737
    @return: job id
738

739
    """
740
    query = []
741
    _AppendDryRunIf(query, dry_run)
742

    
743
    return self._SendRequest(HTTP_DELETE,
744
                             ("/%s/instances/%s" %
745
                              (GANETI_RAPI_VERSION, instance)), query, None)
746

    
747
  def ModifyInstance(self, instance, **kwargs):
748
    """Modifies an instance.
749

750
    More details for parameters can be found in the RAPI documentation.
751

752
    @type instance: string
753
    @param instance: Instance name
754
    @rtype: string
755
    @return: job id
756

757
    """
758
    body = kwargs
759

    
760
    return self._SendRequest(HTTP_PUT,
761
                             ("/%s/instances/%s/modify" %
762
                              (GANETI_RAPI_VERSION, instance)), None, body)
763

    
764
  def ActivateInstanceDisks(self, instance, ignore_size=None):
765
    """Activates an instance's disks.
766

767
    @type instance: string
768
    @param instance: Instance name
769
    @type ignore_size: bool
770
    @param ignore_size: Whether to ignore recorded size
771
    @rtype: string
772
    @return: job id
773

774
    """
775
    query = []
776
    _AppendIf(query, ignore_size, ("ignore_size", 1))
777

    
778
    return self._SendRequest(HTTP_PUT,
779
                             ("/%s/instances/%s/activate-disks" %
780
                              (GANETI_RAPI_VERSION, instance)), query, None)
781

    
782
  def DeactivateInstanceDisks(self, instance):
783
    """Deactivates an instance's disks.
784

785
    @type instance: string
786
    @param instance: Instance name
787
    @rtype: string
788
    @return: job id
789

790
    """
791
    return self._SendRequest(HTTP_PUT,
792
                             ("/%s/instances/%s/deactivate-disks" %
793
                              (GANETI_RAPI_VERSION, instance)), None, None)
794

    
795
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
796
    """Recreate an instance's disks.
797

798
    @type instance: string
799
    @param instance: Instance name
800
    @type disks: list of int
801
    @param disks: List of disk indexes
802
    @type nodes: list of string
803
    @param nodes: New instance nodes, if relocation is desired
804
    @rtype: string
805
    @return: job id
806

807
    """
808
    body = {}
809
    _SetItemIf(body, disks is not None, "disks", disks)
810
    _SetItemIf(body, nodes is not None, "nodes", nodes)
811

    
812
    return self._SendRequest(HTTP_POST,
813
                             ("/%s/instances/%s/recreate-disks" %
814
                              (GANETI_RAPI_VERSION, instance)), None, body)
815

    
816
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
817
    """Grows a disk of an instance.
818

819
    More details for parameters can be found in the RAPI documentation.
820

821
    @type instance: string
822
    @param instance: Instance name
823
    @type disk: integer
824
    @param disk: Disk index
825
    @type amount: integer
826
    @param amount: Grow disk by this amount (MiB)
827
    @type wait_for_sync: bool
828
    @param wait_for_sync: Wait for disk to synchronize
829
    @rtype: string
830
    @return: job id
831

832
    """
833
    body = {
834
      "amount": amount,
835
      }
836

    
837
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
838

    
839
    return self._SendRequest(HTTP_POST,
840
                             ("/%s/instances/%s/disk/%s/grow" %
841
                              (GANETI_RAPI_VERSION, instance, disk)),
842
                             None, body)
843

    
844
  def GetInstanceTags(self, instance):
845
    """Gets tags for an instance.
846

847
    @type instance: str
848
    @param instance: instance whose tags to return
849

850
    @rtype: list of str
851
    @return: tags for the instance
852

853
    """
854
    return self._SendRequest(HTTP_GET,
855
                             ("/%s/instances/%s/tags" %
856
                              (GANETI_RAPI_VERSION, instance)), None, None)
857

    
858
  def AddInstanceTags(self, instance, tags, dry_run=False):
859
    """Adds tags to an instance.
860

861
    @type instance: str
862
    @param instance: instance to add tags to
863
    @type tags: list of str
864
    @param tags: tags to add to the instance
865
    @type dry_run: bool
866
    @param dry_run: whether to perform a dry run
867

868
    @rtype: string
869
    @return: job id
870

871
    """
872
    query = [("tag", t) for t in tags]
873
    _AppendDryRunIf(query, dry_run)
874

    
875
    return self._SendRequest(HTTP_PUT,
876
                             ("/%s/instances/%s/tags" %
877
                              (GANETI_RAPI_VERSION, instance)), query, None)
878

    
879
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
880
    """Deletes tags from an instance.
881

882
    @type instance: str
883
    @param instance: instance to delete tags from
884
    @type tags: list of str
885
    @param tags: tags to delete
886
    @type dry_run: bool
887
    @param dry_run: whether to perform a dry run
888
    @rtype: string
889
    @return: job id
890

891
    """
892
    query = [("tag", t) for t in tags]
893
    _AppendDryRunIf(query, dry_run)
894

    
895
    return self._SendRequest(HTTP_DELETE,
896
                             ("/%s/instances/%s/tags" %
897
                              (GANETI_RAPI_VERSION, instance)), query, None)
898

    
899
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
900
                     dry_run=False):
901
    """Reboots an instance.
902

903
    @type instance: str
904
    @param instance: instance to rebot
905
    @type reboot_type: str
906
    @param reboot_type: one of: hard, soft, full
907
    @type ignore_secondaries: bool
908
    @param ignore_secondaries: if True, ignores errors for the secondary node
909
        while re-assembling disks (in hard-reboot mode only)
910
    @type dry_run: bool
911
    @param dry_run: whether to perform a dry run
912
    @rtype: string
913
    @return: job id
914

915
    """
916
    query = []
917
    _AppendDryRunIf(query, dry_run)
918
    _AppendIf(query, reboot_type, ("type", reboot_type))
919
    _AppendIf(query, ignore_secondaries is not None,
920
              ("ignore_secondaries", ignore_secondaries))
921

    
922
    return self._SendRequest(HTTP_POST,
923
                             ("/%s/instances/%s/reboot" %
924
                              (GANETI_RAPI_VERSION, instance)), query, None)
925

    
926
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
927
    """Shuts down an instance.
928

929
    @type instance: str
930
    @param instance: the instance to shut down
931
    @type dry_run: bool
932
    @param dry_run: whether to perform a dry run
933
    @type no_remember: bool
934
    @param no_remember: if true, will not record the state change
935
    @rtype: string
936
    @return: job id
937

938
    """
939
    query = []
940
    _AppendDryRunIf(query, dry_run)
941
    _AppendIf(query, no_remember, ("no-remember", 1))
942

    
943
    return self._SendRequest(HTTP_PUT,
944
                             ("/%s/instances/%s/shutdown" %
945
                              (GANETI_RAPI_VERSION, instance)), query, None)
946

    
947
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
948
    """Starts up an instance.
949

950
    @type instance: str
951
    @param instance: the instance to start up
952
    @type dry_run: bool
953
    @param dry_run: whether to perform a dry run
954
    @type no_remember: bool
955
    @param no_remember: if true, will not record the state change
956
    @rtype: string
957
    @return: job id
958

959
    """
960
    query = []
961
    _AppendDryRunIf(query, dry_run)
962
    _AppendIf(query, no_remember, ("no-remember", 1))
963

    
964
    return self._SendRequest(HTTP_PUT,
965
                             ("/%s/instances/%s/startup" %
966
                              (GANETI_RAPI_VERSION, instance)), query, None)
967

    
968
  def ReinstallInstance(self, instance, os=None, no_startup=False,
969
                        osparams=None):
970
    """Reinstalls an instance.
971

972
    @type instance: str
973
    @param instance: The instance to reinstall
974
    @type os: str or None
975
    @param os: The operating system to reinstall. If None, the instance's
976
        current operating system will be installed again
977
    @type no_startup: bool
978
    @param no_startup: Whether to start the instance automatically
979
    @rtype: string
980
    @return: job id
981

982
    """
983
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
984
      body = {
985
        "start": not no_startup,
986
        }
987
      _SetItemIf(body, os is not None, "os", os)
988
      _SetItemIf(body, osparams is not None, "osparams", osparams)
989
      return self._SendRequest(HTTP_POST,
990
                               ("/%s/instances/%s/reinstall" %
991
                                (GANETI_RAPI_VERSION, instance)), None, body)
992

    
993
    # Use old request format
994
    if osparams:
995
      raise GanetiApiError("Server does not support specifying OS parameters"
996
                           " for instance reinstallation")
997

    
998
    query = []
999
    _AppendIf(query, os, ("os", os))
1000
    _AppendIf(query, no_startup, ("nostartup", 1))
1001

    
1002
    return self._SendRequest(HTTP_POST,
1003
                             ("/%s/instances/%s/reinstall" %
1004
                              (GANETI_RAPI_VERSION, instance)), query, None)
1005

    
1006
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1007
                           remote_node=None, iallocator=None):
1008
    """Replaces disks on an instance.
1009

1010
    @type instance: str
1011
    @param instance: instance whose disks to replace
1012
    @type disks: list of ints
1013
    @param disks: Indexes of disks to replace
1014
    @type mode: str
1015
    @param mode: replacement mode to use (defaults to replace_auto)
1016
    @type remote_node: str or None
1017
    @param remote_node: new secondary node to use (for use with
1018
        replace_new_secondary mode)
1019
    @type iallocator: str or None
1020
    @param iallocator: instance allocator plugin to use (for use with
1021
                       replace_auto mode)
1022

1023
    @rtype: string
1024
    @return: job id
1025

1026
    """
1027
    query = [
1028
      ("mode", mode),
1029
      ]
1030

    
1031
    # TODO: Convert to body parameters
1032

    
1033
    if disks is not None:
1034
      _AppendIf(query, True,
1035
                ("disks", ",".join(str(idx) for idx in disks)))
1036

    
1037
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1038
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1039

    
1040
    return self._SendRequest(HTTP_POST,
1041
                             ("/%s/instances/%s/replace-disks" %
1042
                              (GANETI_RAPI_VERSION, instance)), query, None)
1043

    
1044
  def PrepareExport(self, instance, mode):
1045
    """Prepares an instance for an export.
1046

1047
    @type instance: string
1048
    @param instance: Instance name
1049
    @type mode: string
1050
    @param mode: Export mode
1051
    @rtype: string
1052
    @return: Job ID
1053

1054
    """
1055
    query = [("mode", mode)]
1056
    return self._SendRequest(HTTP_PUT,
1057
                             ("/%s/instances/%s/prepare-export" %
1058
                              (GANETI_RAPI_VERSION, instance)), query, None)
1059

    
1060
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1061
                     remove_instance=None,
1062
                     x509_key_name=None, destination_x509_ca=None):
1063
    """Exports an instance.
1064

1065
    @type instance: string
1066
    @param instance: Instance name
1067
    @type mode: string
1068
    @param mode: Export mode
1069
    @rtype: string
1070
    @return: Job ID
1071

1072
    """
1073
    body = {
1074
      "destination": destination,
1075
      "mode": mode,
1076
      }
1077

    
1078
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1079
    _SetItemIf(body, remove_instance is not None,
1080
               "remove_instance", remove_instance)
1081
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1082
    _SetItemIf(body, destination_x509_ca is not None,
1083
               "destination_x509_ca", destination_x509_ca)
1084

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

    
1089
  def MigrateInstance(self, instance, mode=None, cleanup=None):
1090
    """Migrates an instance.
1091

1092
    @type instance: string
1093
    @param instance: Instance name
1094
    @type mode: string
1095
    @param mode: Migration mode
1096
    @type cleanup: bool
1097
    @param cleanup: Whether to clean up a previously failed migration
1098
    @rtype: string
1099
    @return: job id
1100

1101
    """
1102
    body = {}
1103
    _SetItemIf(body, mode is not None, "mode", mode)
1104
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1105

    
1106
    return self._SendRequest(HTTP_PUT,
1107
                             ("/%s/instances/%s/migrate" %
1108
                              (GANETI_RAPI_VERSION, instance)), None, body)
1109

    
1110
  def FailoverInstance(self, instance, iallocator=None,
1111
                       ignore_consistency=None, target_node=None):
1112
    """Does a failover of an instance.
1113

1114
    @type instance: string
1115
    @param instance: Instance name
1116
    @type iallocator: string
1117
    @param iallocator: Iallocator for deciding the target node for
1118
      shared-storage instances
1119
    @type ignore_consistency: bool
1120
    @param ignore_consistency: Whether to ignore disk consistency
1121
    @type target_node: string
1122
    @param target_node: Target node for shared-storage instances
1123
    @rtype: string
1124
    @return: job id
1125

1126
    """
1127
    body = {}
1128
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1129
    _SetItemIf(body, ignore_consistency is not None,
1130
               "ignore_consistency", ignore_consistency)
1131
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1132

    
1133
    return self._SendRequest(HTTP_PUT,
1134
                             ("/%s/instances/%s/failover" %
1135
                              (GANETI_RAPI_VERSION, instance)), None, body)
1136

    
1137
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1138
    """Changes the name of an instance.
1139

1140
    @type instance: string
1141
    @param instance: Instance name
1142
    @type new_name: string
1143
    @param new_name: New instance name
1144
    @type ip_check: bool
1145
    @param ip_check: Whether to ensure instance's IP address is inactive
1146
    @type name_check: bool
1147
    @param name_check: Whether to ensure instance's name is resolvable
1148
    @rtype: string
1149
    @return: job id
1150

1151
    """
1152
    body = {
1153
      "new_name": new_name,
1154
      }
1155

    
1156
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1157
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1158

    
1159
    return self._SendRequest(HTTP_PUT,
1160
                             ("/%s/instances/%s/rename" %
1161
                              (GANETI_RAPI_VERSION, instance)), None, body)
1162

    
1163
  def GetInstanceConsole(self, instance):
1164
    """Request information for connecting to instance's console.
1165

1166
    @type instance: string
1167
    @param instance: Instance name
1168
    @rtype: dict
1169
    @return: dictionary containing information about instance's console
1170

1171
    """
1172
    return self._SendRequest(HTTP_GET,
1173
                             ("/%s/instances/%s/console" %
1174
                              (GANETI_RAPI_VERSION, instance)), None, None)
1175

    
1176
  def GetJobs(self):
1177
    """Gets all jobs for the cluster.
1178

1179
    @rtype: list of int
1180
    @return: job ids for the cluster
1181

1182
    """
1183
    return [int(j["id"])
1184
            for j in self._SendRequest(HTTP_GET,
1185
                                       "/%s/jobs" % GANETI_RAPI_VERSION,
1186
                                       None, None)]
1187

    
1188
  def GetJobStatus(self, job_id):
1189
    """Gets the status of a job.
1190

1191
    @type job_id: string
1192
    @param job_id: job id whose status to query
1193

1194
    @rtype: dict
1195
    @return: job status
1196

1197
    """
1198
    return self._SendRequest(HTTP_GET,
1199
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1200
                             None, None)
1201

    
1202
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1203
    """Polls cluster for job status until completion.
1204

1205
    Completion is defined as any of the following states listed in
1206
    L{JOB_STATUS_FINALIZED}.
1207

1208
    @type job_id: string
1209
    @param job_id: job id to watch
1210
    @type period: int
1211
    @param period: how often to poll for status (optional, default 5s)
1212
    @type retries: int
1213
    @param retries: how many time to poll before giving up
1214
                    (optional, default -1 means unlimited)
1215

1216
    @rtype: bool
1217
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1218
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1219
      possible; L{WaitForJobChange} returns immediately after a job changed and
1220
      does not use polling
1221

1222
    """
1223
    while retries != 0:
1224
      job_result = self.GetJobStatus(job_id)
1225

    
1226
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1227
        return True
1228
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1229
        return False
1230

    
1231
      if period:
1232
        time.sleep(period)
1233

    
1234
      if retries > 0:
1235
        retries -= 1
1236

    
1237
    return False
1238

    
1239
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1240
    """Waits for job changes.
1241

1242
    @type job_id: string
1243
    @param job_id: Job ID for which to wait
1244
    @return: C{None} if no changes have been detected and a dict with two keys,
1245
      C{job_info} and C{log_entries} otherwise.
1246
    @rtype: dict
1247

1248
    """
1249
    body = {
1250
      "fields": fields,
1251
      "previous_job_info": prev_job_info,
1252
      "previous_log_serial": prev_log_serial,
1253
      }
1254

    
1255
    return self._SendRequest(HTTP_GET,
1256
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1257
                             None, body)
1258

    
1259
  def CancelJob(self, job_id, dry_run=False):
1260
    """Cancels a job.
1261

1262
    @type job_id: string
1263
    @param job_id: id of the job to delete
1264
    @type dry_run: bool
1265
    @param dry_run: whether to perform a dry run
1266
    @rtype: tuple
1267
    @return: tuple containing the result, and a message (bool, string)
1268

1269
    """
1270
    query = []
1271
    _AppendDryRunIf(query, dry_run)
1272

    
1273
    return self._SendRequest(HTTP_DELETE,
1274
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1275
                             query, None)
1276

    
1277
  def GetNodes(self, bulk=False):
1278
    """Gets all nodes in the cluster.
1279

1280
    @type bulk: bool
1281
    @param bulk: whether to return all information about all instances
1282

1283
    @rtype: list of dict or str
1284
    @return: if bulk is true, info about nodes in the cluster,
1285
        else list of nodes in the cluster
1286

1287
    """
1288
    query = []
1289
    _AppendIf(query, bulk, ("bulk", 1))
1290

    
1291
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1292
                              query, None)
1293
    if bulk:
1294
      return nodes
1295
    else:
1296
      return [n["id"] for n in nodes]
1297

    
1298
  def GetNode(self, node):
1299
    """Gets information about a node.
1300

1301
    @type node: str
1302
    @param node: node whose info to return
1303

1304
    @rtype: dict
1305
    @return: info about the node
1306

1307
    """
1308
    return self._SendRequest(HTTP_GET,
1309
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1310
                             None, None)
1311

    
1312
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1313
                   dry_run=False, early_release=None,
1314
                   mode=None, accept_old=False):
1315
    """Evacuates instances from a Ganeti node.
1316

1317
    @type node: str
1318
    @param node: node to evacuate
1319
    @type iallocator: str or None
1320
    @param iallocator: instance allocator to use
1321
    @type remote_node: str
1322
    @param remote_node: node to evaucate to
1323
    @type dry_run: bool
1324
    @param dry_run: whether to perform a dry run
1325
    @type early_release: bool
1326
    @param early_release: whether to enable parallelization
1327
    @type mode: string
1328
    @param mode: Node evacuation mode
1329
    @type accept_old: bool
1330
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1331
        results
1332

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

1339
    @raises GanetiApiError: if an iallocator and remote_node are both
1340
        specified
1341

1342
    """
1343
    if iallocator and remote_node:
1344
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1345

    
1346
    query = []
1347
    _AppendDryRunIf(query, dry_run)
1348

    
1349
    if _NODE_EVAC_RES1 in self.GetFeatures():
1350
      # Server supports body parameters
1351
      body = {}
1352

    
1353
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1354
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1355
      _SetItemIf(body, early_release is not None,
1356
                 "early_release", early_release)
1357
      _SetItemIf(body, mode is not None, "mode", mode)
1358
    else:
1359
      # Pre-2.5 request format
1360
      body = None
1361

    
1362
      if not accept_old:
1363
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1364
                             " not accept old-style results (parameter"
1365
                             " accept_old)")
1366

    
1367
      # Pre-2.5 servers can only evacuate secondaries
1368
      if mode is not None and mode != NODE_EVAC_SEC:
1369
        raise GanetiApiError("Server can only evacuate secondary instances")
1370

    
1371
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1372
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1373
      _AppendIf(query, early_release, ("early_release", 1))
1374

    
1375
    return self._SendRequest(HTTP_POST,
1376
                             ("/%s/nodes/%s/evacuate" %
1377
                              (GANETI_RAPI_VERSION, node)), query, body)
1378

    
1379
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1380
                  target_node=None):
1381
    """Migrates all primary instances from a node.
1382

1383
    @type node: str
1384
    @param node: node to migrate
1385
    @type mode: string
1386
    @param mode: if passed, it will overwrite the live migration type,
1387
        otherwise the hypervisor default will be used
1388
    @type dry_run: bool
1389
    @param dry_run: whether to perform a dry run
1390
    @type iallocator: string
1391
    @param iallocator: instance allocator to use
1392
    @type target_node: string
1393
    @param target_node: Target node for shared-storage instances
1394

1395
    @rtype: string
1396
    @return: job id
1397

1398
    """
1399
    query = []
1400
    _AppendDryRunIf(query, dry_run)
1401

    
1402
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1403
      body = {}
1404

    
1405
      _SetItemIf(body, mode is not None, "mode", mode)
1406
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1407
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1408

    
1409
      assert len(query) <= 1
1410

    
1411
      return self._SendRequest(HTTP_POST,
1412
                               ("/%s/nodes/%s/migrate" %
1413
                                (GANETI_RAPI_VERSION, node)), query, body)
1414
    else:
1415
      # Use old request format
1416
      if target_node is not None:
1417
        raise GanetiApiError("Server does not support specifying target node"
1418
                             " for node migration")
1419

    
1420
      _AppendIf(query, mode is not None, ("mode", mode))
1421

    
1422
      return self._SendRequest(HTTP_POST,
1423
                               ("/%s/nodes/%s/migrate" %
1424
                                (GANETI_RAPI_VERSION, node)), query, None)
1425

    
1426
  def GetNodeRole(self, node):
1427
    """Gets the current role for a node.
1428

1429
    @type node: str
1430
    @param node: node whose role to return
1431

1432
    @rtype: str
1433
    @return: the current role for a node
1434

1435
    """
1436
    return self._SendRequest(HTTP_GET,
1437
                             ("/%s/nodes/%s/role" %
1438
                              (GANETI_RAPI_VERSION, node)), None, None)
1439

    
1440
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1441
    """Sets the role for a node.
1442

1443
    @type node: str
1444
    @param node: the node whose role to set
1445
    @type role: str
1446
    @param role: the role to set for the node
1447
    @type force: bool
1448
    @param force: whether to force the role change
1449
    @type auto_promote: bool
1450
    @param auto_promote: Whether node(s) should be promoted to master candidate
1451
                         if necessary
1452

1453
    @rtype: string
1454
    @return: job id
1455

1456
    """
1457
    query = []
1458
    _AppendForceIf(query, force)
1459
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1460

    
1461
    return self._SendRequest(HTTP_PUT,
1462
                             ("/%s/nodes/%s/role" %
1463
                              (GANETI_RAPI_VERSION, node)), query, role)
1464

    
1465
  def PowercycleNode(self, node, force=False):
1466
    """Powercycles a node.
1467

1468
    @type node: string
1469
    @param node: Node name
1470
    @type force: bool
1471
    @param force: Whether to force the operation
1472
    @rtype: string
1473
    @return: job id
1474

1475
    """
1476
    query = []
1477
    _AppendForceIf(query, force)
1478

    
1479
    return self._SendRequest(HTTP_POST,
1480
                             ("/%s/nodes/%s/powercycle" %
1481
                              (GANETI_RAPI_VERSION, node)), query, None)
1482

    
1483
  def ModifyNode(self, node, **kwargs):
1484
    """Modifies a node.
1485

1486
    More details for parameters can be found in the RAPI documentation.
1487

1488
    @type node: string
1489
    @param node: Node name
1490
    @rtype: string
1491
    @return: job id
1492

1493
    """
1494
    return self._SendRequest(HTTP_POST,
1495
                             ("/%s/nodes/%s/modify" %
1496
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1497

    
1498
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1499
    """Gets the storage units for a node.
1500

1501
    @type node: str
1502
    @param node: the node whose storage units to return
1503
    @type storage_type: str
1504
    @param storage_type: storage type whose units to return
1505
    @type output_fields: str
1506
    @param output_fields: storage type fields to return
1507

1508
    @rtype: string
1509
    @return: job id where results can be retrieved
1510

1511
    """
1512
    query = [
1513
      ("storage_type", storage_type),
1514
      ("output_fields", output_fields),
1515
      ]
1516

    
1517
    return self._SendRequest(HTTP_GET,
1518
                             ("/%s/nodes/%s/storage" %
1519
                              (GANETI_RAPI_VERSION, node)), query, None)
1520

    
1521
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1522
    """Modifies parameters of storage units on the node.
1523

1524
    @type node: str
1525
    @param node: node whose storage units to modify
1526
    @type storage_type: str
1527
    @param storage_type: storage type whose units to modify
1528
    @type name: str
1529
    @param name: name of the storage unit
1530
    @type allocatable: bool or None
1531
    @param allocatable: Whether to set the "allocatable" flag on the storage
1532
                        unit (None=no modification, True=set, False=unset)
1533

1534
    @rtype: string
1535
    @return: job id
1536

1537
    """
1538
    query = [
1539
      ("storage_type", storage_type),
1540
      ("name", name),
1541
      ]
1542

    
1543
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1544

    
1545
    return self._SendRequest(HTTP_PUT,
1546
                             ("/%s/nodes/%s/storage/modify" %
1547
                              (GANETI_RAPI_VERSION, node)), query, None)
1548

    
1549
  def RepairNodeStorageUnits(self, node, storage_type, name):
1550
    """Repairs a storage unit on the node.
1551

1552
    @type node: str
1553
    @param node: node whose storage units to repair
1554
    @type storage_type: str
1555
    @param storage_type: storage type to repair
1556
    @type name: str
1557
    @param name: name of the storage unit to repair
1558

1559
    @rtype: string
1560
    @return: job id
1561

1562
    """
1563
    query = [
1564
      ("storage_type", storage_type),
1565
      ("name", name),
1566
      ]
1567

    
1568
    return self._SendRequest(HTTP_PUT,
1569
                             ("/%s/nodes/%s/storage/repair" %
1570
                              (GANETI_RAPI_VERSION, node)), query, None)
1571

    
1572
  def GetNodeTags(self, node):
1573
    """Gets the tags for a node.
1574

1575
    @type node: str
1576
    @param node: node whose tags to return
1577

1578
    @rtype: list of str
1579
    @return: tags for the node
1580

1581
    """
1582
    return self._SendRequest(HTTP_GET,
1583
                             ("/%s/nodes/%s/tags" %
1584
                              (GANETI_RAPI_VERSION, node)), None, None)
1585

    
1586
  def AddNodeTags(self, node, tags, dry_run=False):
1587
    """Adds tags to a node.
1588

1589
    @type node: str
1590
    @param node: node to add tags to
1591
    @type tags: list of str
1592
    @param tags: tags to add to the node
1593
    @type dry_run: bool
1594
    @param dry_run: whether to perform a dry run
1595

1596
    @rtype: string
1597
    @return: job id
1598

1599
    """
1600
    query = [("tag", t) for t in tags]
1601
    _AppendDryRunIf(query, dry_run)
1602

    
1603
    return self._SendRequest(HTTP_PUT,
1604
                             ("/%s/nodes/%s/tags" %
1605
                              (GANETI_RAPI_VERSION, node)), query, tags)
1606

    
1607
  def DeleteNodeTags(self, node, tags, dry_run=False):
1608
    """Delete tags from a node.
1609

1610
    @type node: str
1611
    @param node: node to remove tags from
1612
    @type tags: list of str
1613
    @param tags: tags to remove from the node
1614
    @type dry_run: bool
1615
    @param dry_run: whether to perform a dry run
1616

1617
    @rtype: string
1618
    @return: job id
1619

1620
    """
1621
    query = [("tag", t) for t in tags]
1622
    _AppendDryRunIf(query, dry_run)
1623

    
1624
    return self._SendRequest(HTTP_DELETE,
1625
                             ("/%s/nodes/%s/tags" %
1626
                              (GANETI_RAPI_VERSION, node)), query, None)
1627

    
1628
  def GetNetworks(self, bulk=False):
1629
    """Gets all networks in the cluster.
1630

1631
    @type bulk: bool
1632
    @param bulk: whether to return all information about the networks
1633

1634
    @rtype: list of dict or str
1635
    @return: if bulk is true, a list of dictionaries with info about all
1636
        networks in the cluster, else a list of names of those networks
1637

1638
    """
1639
    query = []
1640
    _AppendIf(query, bulk, ("bulk", 1))
1641

    
1642
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1643
                               query, None)
1644
    if bulk:
1645
      return networks
1646
    else:
1647
      return [n["name"] for n in networks]
1648

    
1649
  def GetNetwork(self, network):
1650
    """Gets information about a network.
1651

1652
    @type group: str
1653
    @param group: name of the network whose info to return
1654

1655
    @rtype: dict
1656
    @return: info about the network
1657

1658
    """
1659
    return self._SendRequest(HTTP_GET,
1660
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1661
                             None, None)
1662

    
1663
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1664
                    gateway6=None, mac_prefix=None, network_type=None,
1665
                    add_reserved_ips=None, tags=[],
1666
                    conflicts_check=False, dry_run=False):
1667
    """Creates a new network.
1668

1669
    @type name: str
1670
    @param name: the name of network to create
1671
    @type dry_run: bool
1672
    @param dry_run: whether to peform a dry run
1673

1674
    @rtype: string
1675
    @return: job id
1676

1677
    """
1678
    query = []
1679
    _AppendDryRunIf(query, dry_run)
1680

    
1681
    body = {
1682
      "network_name": network_name,
1683
      "gateway": gateway,
1684
      "network": network,
1685
      "gateway6": gateway6,
1686
      "network6": network6,
1687
      "mac_prefix": mac_prefix,
1688
      "network_type": network_type,
1689
      "add_reserved_ips": add_reserved_ips,
1690
      "conflicts_check": conflicts_check,
1691
      "tags": tags,
1692
      }
1693

    
1694
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1695
                             query, body)
1696

    
1697
  def ConnectNetwork(self, network_name, group_name, mode, link,
1698
                     conflicts_check=False, depends=None, dry_run=False):
1699
    """Connects a Network to a NodeGroup with the given netparams
1700

1701
    """
1702
    body = {
1703
      "group_name": group_name,
1704
      "network_mode": mode,
1705
      "network_link": link,
1706
      "conflicts_check": conflicts_check,
1707
      }
1708

    
1709
    if depends:
1710
      body['depends'] = []
1711
      for d in depends:
1712
        body['depends'].append([d, ["success"]])
1713

    
1714
    query = []
1715
    _AppendDryRunIf(query, dry_run)
1716

    
1717
    return self._SendRequest(HTTP_PUT,
1718
                             ("/%s/networks/%s/connect" %
1719
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1720

    
1721
  def DisconnectNetwork(self, network_name, group_name,
1722
                        depends=None, dry_run=False):
1723
    """Connects a Network to a NodeGroup with the given netparams
1724

1725
    """
1726
    body = {
1727
      "group_name": group_name
1728
      }
1729

    
1730
    if depends:
1731
      body['depends'] = []
1732
      for d in depends:
1733
        body['depends'].append([d, ["success"]])
1734

    
1735
    query = []
1736
    _AppendDryRunIf(query, dry_run)
1737

    
1738
    return self._SendRequest(HTTP_PUT,
1739
                             ("/%s/networks/%s/disconnect" %
1740
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1741

    
1742

    
1743
  def ModifyNetwork(self, network, **kwargs):
1744
    """Modifies a network.
1745

1746
    More details for parameters can be found in the RAPI documentation.
1747

1748
    @type network: string
1749
    @param network: Network name
1750
    @rtype: string
1751
    @return: job id
1752

1753
    """
1754
    return self._SendRequest(HTTP_PUT,
1755
                             ("/%s/networks/%s/modify" %
1756
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
1757

    
1758
  def DeleteNetwork(self, network, depends=None, dry_run=False):
1759
    """Deletes a network.
1760

1761
    @type group: str
1762
    @param group: the network to delete
1763
    @type dry_run: bool
1764
    @param dry_run: whether to peform a dry run
1765

1766
    @rtype: string
1767
    @return: job id
1768

1769
    """
1770
    body = {}
1771
    if depends:
1772
      body['depends'] = []
1773
      for d in depends:
1774
        body['depends'].append([d, ["success"]])
1775

    
1776
    query = []
1777
    _AppendDryRunIf(query, dry_run)
1778

    
1779
    return self._SendRequest(HTTP_DELETE,
1780
                             ("/%s/networks/%s" %
1781
                              (GANETI_RAPI_VERSION, network)), query, body)
1782

    
1783
  def GetNetworkTags(self, network):
1784
    """Gets tags for a network.
1785

1786
    @type network: string
1787
    @param network: Node group whose tags to return
1788

1789
    @rtype: list of strings
1790
    @return: tags for the network
1791

1792
    """
1793
    return self._SendRequest(HTTP_GET,
1794
                             ("/%s/networks/%s/tags" %
1795
                              (GANETI_RAPI_VERSION, network)), None, None)
1796

    
1797
  def AddNetworkTags(self, network, tags, dry_run=False):
1798
    """Adds tags to a network.
1799

1800
    @type network: str
1801
    @param network: network to add tags to
1802
    @type tags: list of string
1803
    @param tags: tags to add to the network
1804
    @type dry_run: bool
1805
    @param dry_run: whether to perform a dry run
1806

1807
    @rtype: string
1808
    @return: job id
1809

1810
    """
1811
    query = [("tag", t) for t in tags]
1812
    _AppendDryRunIf(query, dry_run)
1813

    
1814
    return self._SendRequest(HTTP_PUT,
1815
                             ("/%s/networks/%s/tags" %
1816
                              (GANETI_RAPI_VERSION, network)), query, None)
1817

    
1818
  def DeleteNetworkTags(self, network, tags, dry_run=False):
1819
    """Deletes tags from a network.
1820

1821
    @type network: str
1822
    @param network: network to delete tags from
1823
    @type tags: list of string
1824
    @param tags: tags to delete
1825
    @type dry_run: bool
1826
    @param dry_run: whether to perform a dry run
1827
    @rtype: string
1828
    @return: job id
1829

1830
    """
1831
    query = [("tag", t) for t in tags]
1832
    _AppendDryRunIf(query, dry_run)
1833

    
1834
    return self._SendRequest(HTTP_DELETE,
1835
                             ("/%s/networks/%s/tags" %
1836
                              (GANETI_RAPI_VERSION, network)), query, None)
1837

    
1838

    
1839
  def GetGroups(self, bulk=False):
1840
    """Gets all node groups in the cluster.
1841

1842
    @type bulk: bool
1843
    @param bulk: whether to return all information about the groups
1844

1845
    @rtype: list of dict or str
1846
    @return: if bulk is true, a list of dictionaries with info about all node
1847
        groups in the cluster, else a list of names of those node groups
1848

1849
    """
1850
    query = []
1851
    _AppendIf(query, bulk, ("bulk", 1))
1852

    
1853
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1854
                               query, None)
1855
    if bulk:
1856
      return groups
1857
    else:
1858
      return [g["name"] for g in groups]
1859

    
1860
  def GetGroup(self, group):
1861
    """Gets information about a node group.
1862

1863
    @type group: str
1864
    @param group: name of the node group whose info to return
1865

1866
    @rtype: dict
1867
    @return: info about the node group
1868

1869
    """
1870
    return self._SendRequest(HTTP_GET,
1871
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1872
                             None, None)
1873

    
1874
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1875
    """Creates a new node group.
1876

1877
    @type name: str
1878
    @param name: the name of node group to create
1879
    @type alloc_policy: str
1880
    @param alloc_policy: the desired allocation policy for the group, if any
1881
    @type dry_run: bool
1882
    @param dry_run: whether to peform a dry run
1883

1884
    @rtype: string
1885
    @return: job id
1886

1887
    """
1888
    query = []
1889
    _AppendDryRunIf(query, dry_run)
1890

    
1891
    body = {
1892
      "name": name,
1893
      "alloc_policy": alloc_policy
1894
      }
1895

    
1896
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1897
                             query, body)
1898

    
1899
  def ModifyGroup(self, group, **kwargs):
1900
    """Modifies a node group.
1901

1902
    More details for parameters can be found in the RAPI documentation.
1903

1904
    @type group: string
1905
    @param group: Node group name
1906
    @rtype: string
1907
    @return: job id
1908

1909
    """
1910
    return self._SendRequest(HTTP_PUT,
1911
                             ("/%s/groups/%s/modify" %
1912
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1913

    
1914
  def DeleteGroup(self, group, dry_run=False):
1915
    """Deletes a node group.
1916

1917
    @type group: str
1918
    @param group: the node group to delete
1919
    @type dry_run: bool
1920
    @param dry_run: whether to peform a dry run
1921

1922
    @rtype: string
1923
    @return: job id
1924

1925
    """
1926
    query = []
1927
    _AppendDryRunIf(query, dry_run)
1928

    
1929
    return self._SendRequest(HTTP_DELETE,
1930
                             ("/%s/groups/%s" %
1931
                              (GANETI_RAPI_VERSION, group)), query, None)
1932

    
1933
  def RenameGroup(self, group, new_name):
1934
    """Changes the name of a node group.
1935

1936
    @type group: string
1937
    @param group: Node group name
1938
    @type new_name: string
1939
    @param new_name: New node group name
1940

1941
    @rtype: string
1942
    @return: job id
1943

1944
    """
1945
    body = {
1946
      "new_name": new_name,
1947
      }
1948

    
1949
    return self._SendRequest(HTTP_PUT,
1950
                             ("/%s/groups/%s/rename" %
1951
                              (GANETI_RAPI_VERSION, group)), None, body)
1952

    
1953
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1954
    """Assigns nodes to a group.
1955

1956
    @type group: string
1957
    @param group: Node gropu name
1958
    @type nodes: list of strings
1959
    @param nodes: List of nodes to assign to the group
1960

1961
    @rtype: string
1962
    @return: job id
1963

1964
    """
1965
    query = []
1966
    _AppendForceIf(query, force)
1967
    _AppendDryRunIf(query, dry_run)
1968

    
1969
    body = {
1970
      "nodes": nodes,
1971
      }
1972

    
1973
    return self._SendRequest(HTTP_PUT,
1974
                             ("/%s/groups/%s/assign-nodes" %
1975
                             (GANETI_RAPI_VERSION, group)), query, body)
1976

    
1977
  def GetGroupTags(self, group):
1978
    """Gets tags for a node group.
1979

1980
    @type group: string
1981
    @param group: Node group whose tags to return
1982

1983
    @rtype: list of strings
1984
    @return: tags for the group
1985

1986
    """
1987
    return self._SendRequest(HTTP_GET,
1988
                             ("/%s/groups/%s/tags" %
1989
                              (GANETI_RAPI_VERSION, group)), None, None)
1990

    
1991
  def AddGroupTags(self, group, tags, dry_run=False):
1992
    """Adds tags to a node group.
1993

1994
    @type group: str
1995
    @param group: group to add tags to
1996
    @type tags: list of string
1997
    @param tags: tags to add to the group
1998
    @type dry_run: bool
1999
    @param dry_run: whether to perform a dry run
2000

2001
    @rtype: string
2002
    @return: job id
2003

2004
    """
2005
    query = [("tag", t) for t in tags]
2006
    _AppendDryRunIf(query, dry_run)
2007

    
2008
    return self._SendRequest(HTTP_PUT,
2009
                             ("/%s/groups/%s/tags" %
2010
                              (GANETI_RAPI_VERSION, group)), query, None)
2011

    
2012
  def DeleteGroupTags(self, group, tags, dry_run=False):
2013
    """Deletes tags from a node group.
2014

2015
    @type group: str
2016
    @param group: group to delete tags from
2017
    @type tags: list of string
2018
    @param tags: tags to delete
2019
    @type dry_run: bool
2020
    @param dry_run: whether to perform a dry run
2021
    @rtype: string
2022
    @return: job id
2023

2024
    """
2025
    query = [("tag", t) for t in tags]
2026
    _AppendDryRunIf(query, dry_run)
2027

    
2028
    return self._SendRequest(HTTP_DELETE,
2029
                             ("/%s/groups/%s/tags" %
2030
                              (GANETI_RAPI_VERSION, group)), query, None)
2031

    
2032
  def Query(self, what, fields, qfilter=None):
2033
    """Retrieves information about resources.
2034

2035
    @type what: string
2036
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2037
    @type fields: list of string
2038
    @param fields: Requested fields
2039
    @type qfilter: None or list
2040
    @param qfilter: Query filter
2041

2042
    @rtype: string
2043
    @return: job id
2044

2045
    """
2046
    body = {
2047
      "fields": fields,
2048
      }
2049

    
2050
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
2051
    # TODO: remove "filter" after 2.7
2052
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
2053

    
2054
    return self._SendRequest(HTTP_PUT,
2055
                             ("/%s/query/%s" %
2056
                              (GANETI_RAPI_VERSION, what)), None, body)
2057

    
2058
  def QueryFields(self, what, fields=None):
2059
    """Retrieves available fields for a resource.
2060

2061
    @type what: string
2062
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
2063
    @type fields: list of string
2064
    @param fields: Requested fields
2065

2066
    @rtype: string
2067
    @return: job id
2068

2069
    """
2070
    query = []
2071

    
2072
    if fields is not None:
2073
      _AppendIf(query, True, ("fields", ",".join(fields)))
2074

    
2075
    return self._SendRequest(HTTP_GET,
2076
                             ("/%s/query/%s/fields" %
2077
                              (GANETI_RAPI_VERSION, what)), query, None)