Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 065be3f0

History | View | Annotate | Download (54.6 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
_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"])
101
_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
102
_INST_CREATE_V0_PARAMS = frozenset([
103
  "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
104
  "hypervisor", "file_storage_dir", "file_driver", "dry_run",
105
  ])
106
_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
107
_QPARAM_DRY_RUN = "dry-run"
108
_QPARAM_FORCE = "force"
109

    
110
# Feature strings
111
INST_CREATE_REQV1 = "instance-create-reqv1"
112
INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
113
NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
114
NODE_EVAC_RES1 = "node-evac-res1"
115

    
116
# Old feature constant names in case they're references by users of this module
117
_INST_CREATE_REQV1 = INST_CREATE_REQV1
118
_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
119
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
120
_NODE_EVAC_RES1 = NODE_EVAC_RES1
121

    
122
# Older pycURL versions don't have all error constants
123
try:
124
  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
125
  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
126
except AttributeError:
127
  _CURLE_SSL_CACERT = 60
128
  _CURLE_SSL_CACERT_BADFILE = 77
129

    
130
_CURL_SSL_CERT_ERRORS = frozenset([
131
  _CURLE_SSL_CACERT,
132
  _CURLE_SSL_CACERT_BADFILE,
133
  ])
134

    
135

    
136
class Error(Exception):
137
  """Base error class for this module.
138

139
  """
140
  pass
141

    
142

    
143
class CertificateError(Error):
144
  """Raised when a problem is found with the SSL certificate.
145

146
  """
147
  pass
148

    
149

    
150
class GanetiApiError(Error):
151
  """Generic error raised from Ganeti API.
152

153
  """
154
  def __init__(self, msg, code=None):
155
    Error.__init__(self, msg)
156
    self.code = code
157

    
158

    
159
def _AppendIf(container, condition, value):
160
  """Appends to a list if a condition evaluates to truth.
161

162
  """
163
  if condition:
164
    container.append(value)
165

    
166
  return condition
167

    
168

    
169
def _AppendDryRunIf(container, condition):
170
  """Appends a "dry-run" parameter if a condition evaluates to truth.
171

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

    
175

    
176
def _AppendForceIf(container, condition):
177
  """Appends a "force" parameter if a condition evaluates to truth.
178

179
  """
180
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
181

    
182

    
183
def _SetItemIf(container, condition, item, value):
184
  """Sets an item if a condition evaluates to truth.
185

186
  """
187
  if condition:
188
    container[item] = value
189

    
190
  return condition
191

    
192

    
193
def UsesRapiClient(fn):
194
  """Decorator for code using RAPI client to initialize pycURL.
195

196
  """
197
  def wrapper(*args, **kwargs):
198
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
199
    # one thread running. This check is just a safety measure -- it doesn't
200
    # cover all cases.
201
    assert threading.activeCount() == 1, \
202
           "Found active threads when initializing pycURL"
203

    
204
    pycurl.global_init(pycurl.GLOBAL_ALL)
205
    try:
206
      return fn(*args, **kwargs)
207
    finally:
208
      pycurl.global_cleanup()
209

    
210
  return wrapper
211

    
212

    
213
def GenericCurlConfig(verbose=False, use_signal=False,
214
                      use_curl_cabundle=False, cafile=None, capath=None,
215
                      proxy=None, verify_hostname=False,
216
                      connect_timeout=None, timeout=None,
217
                      _pycurl_version_fn=pycurl.version_info):
218
  """Curl configuration function generator.
219

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

242
  """
243
  if use_curl_cabundle and (cafile or capath):
244
    raise Error("Can not use default CA bundle when CA file or path is set")
245

    
246
  def _ConfigCurl(curl, logger):
247
    """Configures a cURL object
248

249
    @type curl: pycurl.Curl
250
    @param curl: cURL object
251

252
    """
253
    logger.debug("Using cURL version %s", pycurl.version)
254

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

    
263
    lcsslver = sslver.lower()
264
    if lcsslver.startswith("openssl/"):
265
      pass
266
    elif lcsslver.startswith("gnutls/"):
267
      if capath:
268
        raise Error("cURL linked against GnuTLS has no support for a"
269
                    " CA path (%s)" % (pycurl.version, ))
270
    else:
271
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
272
                                sslver)
273

    
274
    curl.setopt(pycurl.VERBOSE, verbose)
275
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
276

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

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

    
300
    if proxy is not None:
301
      curl.setopt(pycurl.PROXY, str(proxy))
302

    
303
    # Timeouts
304
    if connect_timeout is not None:
305
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
306
    if timeout is not None:
307
      curl.setopt(pycurl.TIMEOUT, timeout)
308

    
309
  return _ConfigCurl
310

    
311

    
312
class GanetiRapiClient(object): # pylint: disable=R0904
313
  """Ganeti RAPI client.
314

315
  """
316
  USER_AGENT = "Ganeti RAPI Client"
317
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
318

    
319
  def __init__(self, host, port=GANETI_RAPI_PORT,
320
               username=None, password=None, logger=logging,
321
               curl_config_fn=None, curl_factory=None):
322
    """Initializes this class.
323

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

336
    """
337
    self._username = username
338
    self._password = password
339
    self._logger = logger
340
    self._curl_config_fn = curl_config_fn
341
    self._curl_factory = curl_factory
342

    
343
    try:
344
      socket.inet_pton(socket.AF_INET6, host)
345
      address = "[%s]:%s" % (host, port)
346
    except socket.error:
347
      address = "%s:%s" % (host, port)
348

    
349
    self._base_url = "https://%s" % address
350

    
351
    if username is not None:
352
      if password is None:
353
        raise Error("Password not specified")
354
    elif password:
355
      raise Error("Specified password without username")
356

    
357
  def _CreateCurl(self):
358
    """Creates a cURL object.
359

360
    """
361
    # Create pycURL object if no factory is provided
362
    if self._curl_factory:
363
      curl = self._curl_factory()
364
    else:
365
      curl = pycurl.Curl()
366

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

    
380
    assert ((self._username is None and self._password is None) ^
381
            (self._username is not None and self._password is not None))
382

    
383
    if self._username:
384
      # Setup authentication
385
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
386
      curl.setopt(pycurl.USERPWD,
387
                  str("%s:%s" % (self._username, self._password)))
388

    
389
    # Call external configuration function
390
    if self._curl_config_fn:
391
      self._curl_config_fn(curl, self._logger)
392

    
393
    return curl
394

    
395
  @staticmethod
396
  def _EncodeQuery(query):
397
    """Encode query values for RAPI URL.
398

399
    @type query: list of two-tuples
400
    @param query: Query arguments
401
    @rtype: list
402
    @return: Query list with encoded values
403

404
    """
405
    result = []
406

    
407
    for name, value in query:
408
      if value is None:
409
        result.append((name, ""))
410

    
411
      elif isinstance(value, bool):
412
        # Boolean values must be encoded as 0 or 1
413
        result.append((name, int(value)))
414

    
415
      elif isinstance(value, (list, tuple, dict)):
416
        raise ValueError("Invalid query data type %r" % type(value).__name__)
417

    
418
      else:
419
        result.append((name, value))
420

    
421
    return result
422

    
423
  def _SendRequest(self, method, path, query, content):
424
    """Sends an HTTP request.
425

426
    This constructs a full URL, encodes and decodes HTTP bodies, and
427
    handles invalid responses in a pythonic way.
428

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

438
    @rtype: str
439
    @return: JSON-Decoded response
440

441
    @raises CertificateError: If an invalid SSL certificate is found
442
    @raises GanetiApiError: If an invalid response is returned
443

444
    """
445
    assert path.startswith("/")
446

    
447
    curl = self._CreateCurl()
448

    
449
    if content is not None:
450
      encoded_content = self._json_encoder.encode(content)
451
    else:
452
      encoded_content = ""
453

    
454
    # Build URL
455
    urlparts = [self._base_url, path]
456
    if query:
457
      urlparts.append("?")
458
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
459

    
460
    url = "".join(urlparts)
461

    
462
    self._logger.debug("Sending request %s %s (content=%r)",
463
                       method, url, encoded_content)
464

    
465
    # Buffer for response
466
    encoded_resp_body = StringIO()
467

    
468
    # Configure cURL
469
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
470
    curl.setopt(pycurl.URL, str(url))
471
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
472
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
473

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

    
482
        raise GanetiApiError(str(err))
483
    finally:
484
      # Reset settings to not keep references to large objects in memory
485
      # between requests
486
      curl.setopt(pycurl.POSTFIELDS, "")
487
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
488

    
489
    # Get HTTP response code
490
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
491

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

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

    
507
      raise GanetiApiError(msg, code=http_code)
508

    
509
    return response_content
510

    
511
  def GetVersion(self):
512
    """Gets the Remote API version running on the cluster.
513

514
    @rtype: int
515
    @return: Ganeti Remote API version
516

517
    """
518
    return self._SendRequest(HTTP_GET, "/version", None, None)
519

    
520
  def GetFeatures(self):
521
    """Gets the list of optional features supported by RAPI server.
522

523
    @rtype: list
524
    @return: List of optional features
525

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

    
535
      raise
536

    
537
  def GetOperatingSystems(self):
538
    """Gets the Operating Systems running in the Ganeti cluster.
539

540
    @rtype: list of str
541
    @return: operating systems
542

543
    """
544
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
545
                             None, None)
546

    
547
  def GetInfo(self):
548
    """Gets info about the cluster.
549

550
    @rtype: dict
551
    @return: information about the cluster
552

553
    """
554
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
555
                             None, None)
556

    
557
  def RedistributeConfig(self):
558
    """Tells the cluster to redistribute its configuration files.
559

560
    @rtype: string
561
    @return: job id
562

563
    """
564
    return self._SendRequest(HTTP_PUT,
565
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
566
                             None, None)
567

    
568
  def ModifyCluster(self, **kwargs):
569
    """Modifies cluster parameters.
570

571
    More details for parameters can be found in the RAPI documentation.
572

573
    @rtype: string
574
    @return: job id
575

576
    """
577
    body = kwargs
578

    
579
    return self._SendRequest(HTTP_PUT,
580
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
581

    
582
  def GetClusterTags(self):
583
    """Gets the cluster tags.
584

585
    @rtype: list of str
586
    @return: cluster tags
587

588
    """
589
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
590
                             None, None)
591

    
592
  def AddClusterTags(self, tags, dry_run=False):
593
    """Adds tags to the cluster.
594

595
    @type tags: list of str
596
    @param tags: tags to add to the cluster
597
    @type dry_run: bool
598
    @param dry_run: whether to perform a dry run
599

600
    @rtype: string
601
    @return: job id
602

603
    """
604
    query = [("tag", t) for t in tags]
605
    _AppendDryRunIf(query, dry_run)
606

    
607
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
608
                             query, None)
609

    
610
  def DeleteClusterTags(self, tags, dry_run=False):
611
    """Deletes tags from the cluster.
612

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

620
    """
621
    query = [("tag", t) for t in tags]
622
    _AppendDryRunIf(query, dry_run)
623

    
624
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
625
                             query, None)
626

    
627
  def GetInstances(self, bulk=False):
628
    """Gets information about instances on the cluster.
629

630
    @type bulk: bool
631
    @param bulk: whether to return all information about all instances
632

633
    @rtype: list of dict or list of str
634
    @return: if bulk is True, info about the instances, else a list of instances
635

636
    """
637
    query = []
638
    _AppendIf(query, bulk, ("bulk", 1))
639

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

    
648
  def GetInstance(self, instance):
649
    """Gets information about an instance.
650

651
    @type instance: str
652
    @param instance: instance whose info to return
653

654
    @rtype: dict
655
    @return: info about the instance
656

657
    """
658
    return self._SendRequest(HTTP_GET,
659
                             ("/%s/instances/%s" %
660
                              (GANETI_RAPI_VERSION, instance)), None, None)
661

    
662
  def GetInstanceInfo(self, instance, static=None):
663
    """Gets information about an instance.
664

665
    @type instance: string
666
    @param instance: Instance name
667
    @rtype: string
668
    @return: Job ID
669

670
    """
671
    if static is not None:
672
      query = [("static", static)]
673
    else:
674
      query = None
675

    
676
    return self._SendRequest(HTTP_GET,
677
                             ("/%s/instances/%s/info" %
678
                              (GANETI_RAPI_VERSION, instance)), query, None)
679

    
680
  def CreateInstance(self, mode, name, disk_template, disks, nics,
681
                     **kwargs):
682
    """Creates a new instance.
683

684
    More details for parameters can be found in the RAPI documentation.
685

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

700
    @rtype: string
701
    @return: job id
702

703
    """
704
    query = []
705

    
706
    _AppendDryRunIf(query, kwargs.get("dry_run"))
707

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

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

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

    
730
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
731
                             query, body)
732

    
733
  def DeleteInstance(self, instance, dry_run=False):
734
    """Deletes an instance.
735

736
    @type instance: str
737
    @param instance: the instance to delete
738

739
    @rtype: string
740
    @return: job id
741

742
    """
743
    query = []
744
    _AppendDryRunIf(query, dry_run)
745

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

    
750
  def ModifyInstance(self, instance, **kwargs):
751
    """Modifies an instance.
752

753
    More details for parameters can be found in the RAPI documentation.
754

755
    @type instance: string
756
    @param instance: Instance name
757
    @rtype: string
758
    @return: job id
759

760
    """
761
    body = kwargs
762

    
763
    return self._SendRequest(HTTP_PUT,
764
                             ("/%s/instances/%s/modify" %
765
                              (GANETI_RAPI_VERSION, instance)), None, body)
766

    
767
  def ActivateInstanceDisks(self, instance, ignore_size=None):
768
    """Activates an instance's disks.
769

770
    @type instance: string
771
    @param instance: Instance name
772
    @type ignore_size: bool
773
    @param ignore_size: Whether to ignore recorded size
774
    @rtype: string
775
    @return: job id
776

777
    """
778
    query = []
779
    _AppendIf(query, ignore_size, ("ignore_size", 1))
780

    
781
    return self._SendRequest(HTTP_PUT,
782
                             ("/%s/instances/%s/activate-disks" %
783
                              (GANETI_RAPI_VERSION, instance)), query, None)
784

    
785
  def DeactivateInstanceDisks(self, instance):
786
    """Deactivates an instance's disks.
787

788
    @type instance: string
789
    @param instance: Instance name
790
    @rtype: string
791
    @return: job id
792

793
    """
794
    return self._SendRequest(HTTP_PUT,
795
                             ("/%s/instances/%s/deactivate-disks" %
796
                              (GANETI_RAPI_VERSION, instance)), None, None)
797

    
798
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
799
    """Recreate an instance's disks.
800

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

810
    """
811
    body = {}
812
    _SetItemIf(body, disks is not None, "disks", disks)
813
    _SetItemIf(body, nodes is not None, "nodes", nodes)
814

    
815
    return self._SendRequest(HTTP_POST,
816
                             ("/%s/instances/%s/recreate-disks" %
817
                              (GANETI_RAPI_VERSION, instance)), None, body)
818

    
819
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
820
    """Grows a disk of an instance.
821

822
    More details for parameters can be found in the RAPI documentation.
823

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

835
    """
836
    body = {
837
      "amount": amount,
838
      }
839

    
840
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
841

    
842
    return self._SendRequest(HTTP_POST,
843
                             ("/%s/instances/%s/disk/%s/grow" %
844
                              (GANETI_RAPI_VERSION, instance, disk)),
845
                             None, body)
846

    
847
  def GetInstanceTags(self, instance):
848
    """Gets tags for an instance.
849

850
    @type instance: str
851
    @param instance: instance whose tags to return
852

853
    @rtype: list of str
854
    @return: tags for the instance
855

856
    """
857
    return self._SendRequest(HTTP_GET,
858
                             ("/%s/instances/%s/tags" %
859
                              (GANETI_RAPI_VERSION, instance)), None, None)
860

    
861
  def AddInstanceTags(self, instance, tags, dry_run=False):
862
    """Adds tags to an instance.
863

864
    @type instance: str
865
    @param instance: instance to add tags to
866
    @type tags: list of str
867
    @param tags: tags to add to the instance
868
    @type dry_run: bool
869
    @param dry_run: whether to perform a dry run
870

871
    @rtype: string
872
    @return: job id
873

874
    """
875
    query = [("tag", t) for t in tags]
876
    _AppendDryRunIf(query, dry_run)
877

    
878
    return self._SendRequest(HTTP_PUT,
879
                             ("/%s/instances/%s/tags" %
880
                              (GANETI_RAPI_VERSION, instance)), query, None)
881

    
882
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
883
    """Deletes tags from an instance.
884

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

894
    """
895
    query = [("tag", t) for t in tags]
896
    _AppendDryRunIf(query, dry_run)
897

    
898
    return self._SendRequest(HTTP_DELETE,
899
                             ("/%s/instances/%s/tags" %
900
                              (GANETI_RAPI_VERSION, instance)), query, None)
901

    
902
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
903
                     dry_run=False):
904
    """Reboots an instance.
905

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

918
    """
919
    query = []
920
    _AppendDryRunIf(query, dry_run)
921
    _AppendIf(query, reboot_type, ("type", reboot_type))
922
    _AppendIf(query, ignore_secondaries is not None,
923
              ("ignore_secondaries", ignore_secondaries))
924

    
925
    return self._SendRequest(HTTP_POST,
926
                             ("/%s/instances/%s/reboot" %
927
                              (GANETI_RAPI_VERSION, instance)), query, None)
928

    
929
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False):
930
    """Shuts down an instance.
931

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

941
    """
942
    query = []
943
    _AppendDryRunIf(query, dry_run)
944
    _AppendIf(query, no_remember, ("no-remember", 1))
945

    
946
    return self._SendRequest(HTTP_PUT,
947
                             ("/%s/instances/%s/shutdown" %
948
                              (GANETI_RAPI_VERSION, instance)), query, None)
949

    
950
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
951
    """Starts up an instance.
952

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

962
    """
963
    query = []
964
    _AppendDryRunIf(query, dry_run)
965
    _AppendIf(query, no_remember, ("no-remember", 1))
966

    
967
    return self._SendRequest(HTTP_PUT,
968
                             ("/%s/instances/%s/startup" %
969
                              (GANETI_RAPI_VERSION, instance)), query, None)
970

    
971
  def ReinstallInstance(self, instance, os=None, no_startup=False,
972
                        osparams=None):
973
    """Reinstalls an instance.
974

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

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

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

    
1001
    query = []
1002
    _AppendIf(query, os, ("os", os))
1003
    _AppendIf(query, no_startup, ("nostartup", 1))
1004

    
1005
    return self._SendRequest(HTTP_POST,
1006
                             ("/%s/instances/%s/reinstall" %
1007
                              (GANETI_RAPI_VERSION, instance)), query, None)
1008

    
1009
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1010
                           remote_node=None, iallocator=None):
1011
    """Replaces disks on an instance.
1012

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

1026
    @rtype: string
1027
    @return: job id
1028

1029
    """
1030
    query = [
1031
      ("mode", mode),
1032
      ]
1033

    
1034
    # TODO: Convert to body parameters
1035

    
1036
    if disks is not None:
1037
      _AppendIf(query, True,
1038
                ("disks", ",".join(str(idx) for idx in disks)))
1039

    
1040
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
1041
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
1042

    
1043
    return self._SendRequest(HTTP_POST,
1044
                             ("/%s/instances/%s/replace-disks" %
1045
                              (GANETI_RAPI_VERSION, instance)), query, None)
1046

    
1047
  def PrepareExport(self, instance, mode):
1048
    """Prepares an instance for an export.
1049

1050
    @type instance: string
1051
    @param instance: Instance name
1052
    @type mode: string
1053
    @param mode: Export mode
1054
    @rtype: string
1055
    @return: Job ID
1056

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

    
1063
  def ExportInstance(self, instance, mode, destination, shutdown=None,
1064
                     remove_instance=None,
1065
                     x509_key_name=None, destination_x509_ca=None):
1066
    """Exports an instance.
1067

1068
    @type instance: string
1069
    @param instance: Instance name
1070
    @type mode: string
1071
    @param mode: Export mode
1072
    @rtype: string
1073
    @return: Job ID
1074

1075
    """
1076
    body = {
1077
      "destination": destination,
1078
      "mode": mode,
1079
      }
1080

    
1081
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
1082
    _SetItemIf(body, remove_instance is not None,
1083
               "remove_instance", remove_instance)
1084
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
1085
    _SetItemIf(body, destination_x509_ca is not None,
1086
               "destination_x509_ca", destination_x509_ca)
1087

    
1088
    return self._SendRequest(HTTP_PUT,
1089
                             ("/%s/instances/%s/export" %
1090
                              (GANETI_RAPI_VERSION, instance)), None, body)
1091

    
1092
  def MigrateInstance(self, instance, mode=None, cleanup=None):
1093
    """Migrates an instance.
1094

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

1104
    """
1105
    body = {}
1106
    _SetItemIf(body, mode is not None, "mode", mode)
1107
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
1108

    
1109
    return self._SendRequest(HTTP_PUT,
1110
                             ("/%s/instances/%s/migrate" %
1111
                              (GANETI_RAPI_VERSION, instance)), None, body)
1112

    
1113
  def FailoverInstance(self, instance, iallocator=None,
1114
                       ignore_consistency=None, target_node=None):
1115
    """Does a failover of an instance.
1116

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

1129
    """
1130
    body = {}
1131
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1132
    _SetItemIf(body, ignore_consistency is not None,
1133
               "ignore_consistency", ignore_consistency)
1134
    _SetItemIf(body, target_node is not None, "target_node", target_node)
1135

    
1136
    return self._SendRequest(HTTP_PUT,
1137
                             ("/%s/instances/%s/failover" %
1138
                              (GANETI_RAPI_VERSION, instance)), None, body)
1139

    
1140
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
1141
    """Changes the name of an instance.
1142

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

1154
    """
1155
    body = {
1156
      "new_name": new_name,
1157
      }
1158

    
1159
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
1160
    _SetItemIf(body, name_check is not None, "name_check", name_check)
1161

    
1162
    return self._SendRequest(HTTP_PUT,
1163
                             ("/%s/instances/%s/rename" %
1164
                              (GANETI_RAPI_VERSION, instance)), None, body)
1165

    
1166
  def GetInstanceConsole(self, instance):
1167
    """Request information for connecting to instance's console.
1168

1169
    @type instance: string
1170
    @param instance: Instance name
1171
    @rtype: dict
1172
    @return: dictionary containing information about instance's console
1173

1174
    """
1175
    return self._SendRequest(HTTP_GET,
1176
                             ("/%s/instances/%s/console" %
1177
                              (GANETI_RAPI_VERSION, instance)), None, None)
1178

    
1179
  def GetJobs(self):
1180
    """Gets all jobs for the cluster.
1181

1182
    @rtype: list of int
1183
    @return: job ids for the cluster
1184

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

    
1191
  def GetJobStatus(self, job_id):
1192
    """Gets the status of a job.
1193

1194
    @type job_id: string
1195
    @param job_id: job id whose status to query
1196

1197
    @rtype: dict
1198
    @return: job status
1199

1200
    """
1201
    return self._SendRequest(HTTP_GET,
1202
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1203
                             None, None)
1204

    
1205
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
1206
    """Polls cluster for job status until completion.
1207

1208
    Completion is defined as any of the following states listed in
1209
    L{JOB_STATUS_FINALIZED}.
1210

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

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

1225
    """
1226
    while retries != 0:
1227
      job_result = self.GetJobStatus(job_id)
1228

    
1229
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1230
        return True
1231
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1232
        return False
1233

    
1234
      if period:
1235
        time.sleep(period)
1236

    
1237
      if retries > 0:
1238
        retries -= 1
1239

    
1240
    return False
1241

    
1242
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1243
    """Waits for job changes.
1244

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

1251
    """
1252
    body = {
1253
      "fields": fields,
1254
      "previous_job_info": prev_job_info,
1255
      "previous_log_serial": prev_log_serial,
1256
      }
1257

    
1258
    return self._SendRequest(HTTP_GET,
1259
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1260
                             None, body)
1261

    
1262
  def CancelJob(self, job_id, dry_run=False):
1263
    """Cancels a job.
1264

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

1272
    """
1273
    query = []
1274
    _AppendDryRunIf(query, dry_run)
1275

    
1276
    return self._SendRequest(HTTP_DELETE,
1277
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1278
                             query, None)
1279

    
1280
  def GetNodes(self, bulk=False):
1281
    """Gets all nodes in the cluster.
1282

1283
    @type bulk: bool
1284
    @param bulk: whether to return all information about all instances
1285

1286
    @rtype: list of dict or str
1287
    @return: if bulk is true, info about nodes in the cluster,
1288
        else list of nodes in the cluster
1289

1290
    """
1291
    query = []
1292
    _AppendIf(query, bulk, ("bulk", 1))
1293

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

    
1301
  def GetNode(self, node):
1302
    """Gets information about a node.
1303

1304
    @type node: str
1305
    @param node: node whose info to return
1306

1307
    @rtype: dict
1308
    @return: info about the node
1309

1310
    """
1311
    return self._SendRequest(HTTP_GET,
1312
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1313
                             None, None)
1314

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

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

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

1342
    @raises GanetiApiError: if an iallocator and remote_node are both
1343
        specified
1344

1345
    """
1346
    if iallocator and remote_node:
1347
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1348

    
1349
    query = []
1350
    _AppendDryRunIf(query, dry_run)
1351

    
1352
    if _NODE_EVAC_RES1 in self.GetFeatures():
1353
      # Server supports body parameters
1354
      body = {}
1355

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

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

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

    
1374
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1375
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1376
      _AppendIf(query, early_release, ("early_release", 1))
1377

    
1378
    return self._SendRequest(HTTP_POST,
1379
                             ("/%s/nodes/%s/evacuate" %
1380
                              (GANETI_RAPI_VERSION, node)), query, body)
1381

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

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

1398
    @rtype: string
1399
    @return: job id
1400

1401
    """
1402
    query = []
1403
    _AppendDryRunIf(query, dry_run)
1404

    
1405
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1406
      body = {}
1407

    
1408
      _SetItemIf(body, mode is not None, "mode", mode)
1409
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1410
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1411

    
1412
      assert len(query) <= 1
1413

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

    
1423
      _AppendIf(query, mode is not None, ("mode", mode))
1424

    
1425
      return self._SendRequest(HTTP_POST,
1426
                               ("/%s/nodes/%s/migrate" %
1427
                                (GANETI_RAPI_VERSION, node)), query, None)
1428

    
1429
  def GetNodeRole(self, node):
1430
    """Gets the current role for a node.
1431

1432
    @type node: str
1433
    @param node: node whose role to return
1434

1435
    @rtype: str
1436
    @return: the current role for a node
1437

1438
    """
1439
    return self._SendRequest(HTTP_GET,
1440
                             ("/%s/nodes/%s/role" %
1441
                              (GANETI_RAPI_VERSION, node)), None, None)
1442

    
1443
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1444
    """Sets the role for a node.
1445

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

1456
    @rtype: string
1457
    @return: job id
1458

1459
    """
1460
    query = []
1461
    _AppendForceIf(query, force)
1462
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1463

    
1464
    return self._SendRequest(HTTP_PUT,
1465
                             ("/%s/nodes/%s/role" %
1466
                              (GANETI_RAPI_VERSION, node)), query, role)
1467

    
1468
  def PowercycleNode(self, node, force=False):
1469
    """Powercycles a node.
1470

1471
    @type node: string
1472
    @param node: Node name
1473
    @type force: bool
1474
    @param force: Whether to force the operation
1475
    @rtype: string
1476
    @return: job id
1477

1478
    """
1479
    query = []
1480
    _AppendForceIf(query, force)
1481

    
1482
    return self._SendRequest(HTTP_POST,
1483
                             ("/%s/nodes/%s/powercycle" %
1484
                              (GANETI_RAPI_VERSION, node)), query, None)
1485

    
1486
  def ModifyNode(self, node, **kwargs):
1487
    """Modifies a node.
1488

1489
    More details for parameters can be found in the RAPI documentation.
1490

1491
    @type node: string
1492
    @param node: Node name
1493
    @rtype: string
1494
    @return: job id
1495

1496
    """
1497
    return self._SendRequest(HTTP_POST,
1498
                             ("/%s/nodes/%s/modify" %
1499
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1500

    
1501
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1502
    """Gets the storage units for a node.
1503

1504
    @type node: str
1505
    @param node: the node whose storage units to return
1506
    @type storage_type: str
1507
    @param storage_type: storage type whose units to return
1508
    @type output_fields: str
1509
    @param output_fields: storage type fields to return
1510

1511
    @rtype: string
1512
    @return: job id where results can be retrieved
1513

1514
    """
1515
    query = [
1516
      ("storage_type", storage_type),
1517
      ("output_fields", output_fields),
1518
      ]
1519

    
1520
    return self._SendRequest(HTTP_GET,
1521
                             ("/%s/nodes/%s/storage" %
1522
                              (GANETI_RAPI_VERSION, node)), query, None)
1523

    
1524
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1525
    """Modifies parameters of storage units on the node.
1526

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

1537
    @rtype: string
1538
    @return: job id
1539

1540
    """
1541
    query = [
1542
      ("storage_type", storage_type),
1543
      ("name", name),
1544
      ]
1545

    
1546
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1547

    
1548
    return self._SendRequest(HTTP_PUT,
1549
                             ("/%s/nodes/%s/storage/modify" %
1550
                              (GANETI_RAPI_VERSION, node)), query, None)
1551

    
1552
  def RepairNodeStorageUnits(self, node, storage_type, name):
1553
    """Repairs a storage unit on the node.
1554

1555
    @type node: str
1556
    @param node: node whose storage units to repair
1557
    @type storage_type: str
1558
    @param storage_type: storage type to repair
1559
    @type name: str
1560
    @param name: name of the storage unit to repair
1561

1562
    @rtype: string
1563
    @return: job id
1564

1565
    """
1566
    query = [
1567
      ("storage_type", storage_type),
1568
      ("name", name),
1569
      ]
1570

    
1571
    return self._SendRequest(HTTP_PUT,
1572
                             ("/%s/nodes/%s/storage/repair" %
1573
                              (GANETI_RAPI_VERSION, node)), query, None)
1574

    
1575
  def GetNodeTags(self, node):
1576
    """Gets the tags for a node.
1577

1578
    @type node: str
1579
    @param node: node whose tags to return
1580

1581
    @rtype: list of str
1582
    @return: tags for the node
1583

1584
    """
1585
    return self._SendRequest(HTTP_GET,
1586
                             ("/%s/nodes/%s/tags" %
1587
                              (GANETI_RAPI_VERSION, node)), None, None)
1588

    
1589
  def AddNodeTags(self, node, tags, dry_run=False):
1590
    """Adds tags to a node.
1591

1592
    @type node: str
1593
    @param node: node to add tags to
1594
    @type tags: list of str
1595
    @param tags: tags to add to the node
1596
    @type dry_run: bool
1597
    @param dry_run: whether to perform a dry run
1598

1599
    @rtype: string
1600
    @return: job id
1601

1602
    """
1603
    query = [("tag", t) for t in tags]
1604
    _AppendDryRunIf(query, dry_run)
1605

    
1606
    return self._SendRequest(HTTP_PUT,
1607
                             ("/%s/nodes/%s/tags" %
1608
                              (GANETI_RAPI_VERSION, node)), query, tags)
1609

    
1610
  def DeleteNodeTags(self, node, tags, dry_run=False):
1611
    """Delete tags from a node.
1612

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

1620
    @rtype: string
1621
    @return: job id
1622

1623
    """
1624
    query = [("tag", t) for t in tags]
1625
    _AppendDryRunIf(query, dry_run)
1626

    
1627
    return self._SendRequest(HTTP_DELETE,
1628
                             ("/%s/nodes/%s/tags" %
1629
                              (GANETI_RAPI_VERSION, node)), query, None)
1630

    
1631
  def GetGroups(self, bulk=False):
1632
    """Gets all node groups in the cluster.
1633

1634
    @type bulk: bool
1635
    @param bulk: whether to return all information about the groups
1636

1637
    @rtype: list of dict or str
1638
    @return: if bulk is true, a list of dictionaries with info about all node
1639
        groups in the cluster, else a list of names of those node groups
1640

1641
    """
1642
    query = []
1643
    _AppendIf(query, bulk, ("bulk", 1))
1644

    
1645
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1646
                               query, None)
1647
    if bulk:
1648
      return groups
1649
    else:
1650
      return [g["name"] for g in groups]
1651

    
1652
  def GetGroup(self, group):
1653
    """Gets information about a node group.
1654

1655
    @type group: str
1656
    @param group: name of the node group whose info to return
1657

1658
    @rtype: dict
1659
    @return: info about the node group
1660

1661
    """
1662
    return self._SendRequest(HTTP_GET,
1663
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1664
                             None, None)
1665

    
1666
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1667
    """Creates a new node group.
1668

1669
    @type name: str
1670
    @param name: the name of node group to create
1671
    @type alloc_policy: str
1672
    @param alloc_policy: the desired allocation policy for the group, if any
1673
    @type dry_run: bool
1674
    @param dry_run: whether to peform a dry run
1675

1676
    @rtype: string
1677
    @return: job id
1678

1679
    """
1680
    query = []
1681
    _AppendDryRunIf(query, dry_run)
1682

    
1683
    body = {
1684
      "name": name,
1685
      "alloc_policy": alloc_policy
1686
      }
1687

    
1688
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1689
                             query, body)
1690

    
1691
  def ModifyGroup(self, group, **kwargs):
1692
    """Modifies a node group.
1693

1694
    More details for parameters can be found in the RAPI documentation.
1695

1696
    @type group: string
1697
    @param group: Node group name
1698
    @rtype: string
1699
    @return: job id
1700

1701
    """
1702
    return self._SendRequest(HTTP_PUT,
1703
                             ("/%s/groups/%s/modify" %
1704
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1705

    
1706
  def DeleteGroup(self, group, dry_run=False):
1707
    """Deletes a node group.
1708

1709
    @type group: str
1710
    @param group: the node group to delete
1711
    @type dry_run: bool
1712
    @param dry_run: whether to peform a dry run
1713

1714
    @rtype: string
1715
    @return: job id
1716

1717
    """
1718
    query = []
1719
    _AppendDryRunIf(query, dry_run)
1720

    
1721
    return self._SendRequest(HTTP_DELETE,
1722
                             ("/%s/groups/%s" %
1723
                              (GANETI_RAPI_VERSION, group)), query, None)
1724

    
1725
  def RenameGroup(self, group, new_name):
1726
    """Changes the name of a node group.
1727

1728
    @type group: string
1729
    @param group: Node group name
1730
    @type new_name: string
1731
    @param new_name: New node group name
1732

1733
    @rtype: string
1734
    @return: job id
1735

1736
    """
1737
    body = {
1738
      "new_name": new_name,
1739
      }
1740

    
1741
    return self._SendRequest(HTTP_PUT,
1742
                             ("/%s/groups/%s/rename" %
1743
                              (GANETI_RAPI_VERSION, group)), None, body)
1744

    
1745
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1746
    """Assigns nodes to a group.
1747

1748
    @type group: string
1749
    @param group: Node gropu name
1750
    @type nodes: list of strings
1751
    @param nodes: List of nodes to assign to the group
1752

1753
    @rtype: string
1754
    @return: job id
1755

1756
    """
1757
    query = []
1758
    _AppendForceIf(query, force)
1759
    _AppendDryRunIf(query, dry_run)
1760

    
1761
    body = {
1762
      "nodes": nodes,
1763
      }
1764

    
1765
    return self._SendRequest(HTTP_PUT,
1766
                             ("/%s/groups/%s/assign-nodes" %
1767
                             (GANETI_RAPI_VERSION, group)), query, body)
1768

    
1769
  def GetGroupTags(self, group):
1770
    """Gets tags for a node group.
1771

1772
    @type group: string
1773
    @param group: Node group whose tags to return
1774

1775
    @rtype: list of strings
1776
    @return: tags for the group
1777

1778
    """
1779
    return self._SendRequest(HTTP_GET,
1780
                             ("/%s/groups/%s/tags" %
1781
                              (GANETI_RAPI_VERSION, group)), None, None)
1782

    
1783
  def AddGroupTags(self, group, tags, dry_run=False):
1784
    """Adds tags to a node group.
1785

1786
    @type group: str
1787
    @param group: group to add tags to
1788
    @type tags: list of string
1789
    @param tags: tags to add to the group
1790
    @type dry_run: bool
1791
    @param dry_run: whether to perform a dry run
1792

1793
    @rtype: string
1794
    @return: job id
1795

1796
    """
1797
    query = [("tag", t) for t in tags]
1798
    _AppendDryRunIf(query, dry_run)
1799

    
1800
    return self._SendRequest(HTTP_PUT,
1801
                             ("/%s/groups/%s/tags" %
1802
                              (GANETI_RAPI_VERSION, group)), query, None)
1803

    
1804
  def DeleteGroupTags(self, group, tags, dry_run=False):
1805
    """Deletes tags from a node group.
1806

1807
    @type group: str
1808
    @param group: group to delete tags from
1809
    @type tags: list of string
1810
    @param tags: tags to delete
1811
    @type dry_run: bool
1812
    @param dry_run: whether to perform a dry run
1813
    @rtype: string
1814
    @return: job id
1815

1816
    """
1817
    query = [("tag", t) for t in tags]
1818
    _AppendDryRunIf(query, dry_run)
1819

    
1820
    return self._SendRequest(HTTP_DELETE,
1821
                             ("/%s/groups/%s/tags" %
1822
                              (GANETI_RAPI_VERSION, group)), query, None)
1823

    
1824
  def Query(self, what, fields, qfilter=None):
1825
    """Retrieves information about resources.
1826

1827
    @type what: string
1828
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1829
    @type fields: list of string
1830
    @param fields: Requested fields
1831
    @type qfilter: None or list
1832
    @param qfilter: Query filter
1833

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

1837
    """
1838
    body = {
1839
      "fields": fields,
1840
      }
1841

    
1842
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
1843
    # TODO: remove "filter" after 2.7
1844
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
1845

    
1846
    return self._SendRequest(HTTP_PUT,
1847
                             ("/%s/query/%s" %
1848
                              (GANETI_RAPI_VERSION, what)), None, body)
1849

    
1850
  def QueryFields(self, what, fields=None):
1851
    """Retrieves available fields for a resource.
1852

1853
    @type what: string
1854
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1855
    @type fields: list of string
1856
    @param fields: Requested fields
1857

1858
    @rtype: string
1859
    @return: job id
1860

1861
    """
1862
    query = []
1863

    
1864
    if fields is not None:
1865
      _AppendIf(query, True, ("fields", ",".join(fields)))
1866

    
1867
    return self._SendRequest(HTTP_GET,
1868
                             ("/%s/query/%s/fields" %
1869
                              (GANETI_RAPI_VERSION, what)), query, None)