Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ bfc2002f

History | View | Annotate | Download (29.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010 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
import httplib
25
import urllib2
26
import logging
27
import simplejson
28
import socket
29
import urllib
30
import OpenSSL
31
import distutils.version
32

    
33

    
34
GANETI_RAPI_PORT = 5080
35
GANETI_RAPI_VERSION = 2
36

    
37
HTTP_DELETE = "DELETE"
38
HTTP_GET = "GET"
39
HTTP_PUT = "PUT"
40
HTTP_POST = "POST"
41
HTTP_OK = 200
42
HTTP_APP_JSON = "application/json"
43

    
44
REPLACE_DISK_PRI = "replace_on_primary"
45
REPLACE_DISK_SECONDARY = "replace_on_secondary"
46
REPLACE_DISK_CHG = "replace_new_secondary"
47
REPLACE_DISK_AUTO = "replace_auto"
48
VALID_NODE_ROLES = frozenset([
49
  "drained", "master", "master-candidate", "offline", "regular",
50
  ])
51

    
52

    
53
class Error(Exception):
54
  """Base error class for this module.
55

56
  """
57
  pass
58

    
59

    
60
class CertificateError(Error):
61
  """Raised when a problem is found with the SSL certificate.
62

63
  """
64
  pass
65

    
66

    
67
class GanetiApiError(Error):
68
  """Generic error raised from Ganeti API.
69

70
  """
71
  def __init__(self, msg, code=None):
72
    Error.__init__(self, msg)
73
    self.code = code
74

    
75

    
76
class InvalidNodeRole(Error):
77
  """Raised when an invalid node role is used.
78

79
  """
80
  pass
81

    
82

    
83
def FormatX509Name(x509_name):
84
  """Formats an X509 name.
85

86
  @type x509_name: OpenSSL.crypto.X509Name
87

88
  """
89
  try:
90
    # Only supported in pyOpenSSL 0.7 and above
91
    get_components_fn = x509_name.get_components
92
  except AttributeError:
93
    return repr(x509_name)
94
  else:
95
    return "".join("/%s=%s" % (name, value)
96
                   for name, value in get_components_fn())
97

    
98

    
99
class CertAuthorityVerify:
100
  """Certificate verificator for SSL context.
101

102
  Configures SSL context to verify server's certificate.
103

104
  """
105
  _CAPATH_MINVERSION = "0.9"
106
  _DEFVFYPATHS_MINVERSION = "0.9"
107

    
108
  _PYOPENSSL_VERSION = OpenSSL.__version__
109
  _PARSED_PYOPENSSL_VERSION = distutils.version.LooseVersion(_PYOPENSSL_VERSION)
110

    
111
  _SUPPORT_CAPATH = (_PARSED_PYOPENSSL_VERSION >= _CAPATH_MINVERSION)
112
  _SUPPORT_DEFVFYPATHS = (_PARSED_PYOPENSSL_VERSION >= _DEFVFYPATHS_MINVERSION)
113

    
114
  def __init__(self, cafile=None, capath=None, use_default_verify_paths=False):
115
    """Initializes this class.
116

117
    @type cafile: string
118
    @param cafile: In which file we can find the certificates
119
    @type capath: string
120
    @param capath: In which directory we can find the certificates
121
    @type use_default_verify_paths: bool
122
    @param use_default_verify_paths: Whether the platform provided CA
123
                                     certificates are to be used for
124
                                     verification purposes
125

126
    """
127
    self._cafile = cafile
128
    self._capath = capath
129
    self._use_default_verify_paths = use_default_verify_paths
130

    
131
    if self._capath is not None and not self._SUPPORT_CAPATH:
132
      raise Error(("PyOpenSSL %s has no support for a CA directory,"
133
                   " version %s or above is required") %
134
                  (self._PYOPENSSL_VERSION, self._CAPATH_MINVERSION))
135

    
136
    if self._use_default_verify_paths and not self._SUPPORT_DEFVFYPATHS:
137
      raise Error(("PyOpenSSL %s has no support for using default verification"
138
                   " paths, version %s or above is required") %
139
                  (self._PYOPENSSL_VERSION, self._DEFVFYPATHS_MINVERSION))
140

    
141
  @staticmethod
142
  def _VerifySslCertCb(logger, _, cert, errnum, errdepth, ok):
143
    """Callback for SSL certificate verification.
144

145
    @param logger: Logging object
146

147
    """
148
    if ok:
149
      log_fn = logger.debug
150
    else:
151
      log_fn = logger.error
152

    
153
    log_fn("Verifying SSL certificate at depth %s, subject '%s', issuer '%s'",
154
           errdepth, FormatX509Name(cert.get_subject()),
155
           FormatX509Name(cert.get_issuer()))
156

    
157
    if not ok:
158
      try:
159
        # Only supported in pyOpenSSL 0.7 and above
160
        # pylint: disable-msg=E1101
161
        fn = OpenSSL.crypto.X509_verify_cert_error_string
162
      except AttributeError:
163
        errmsg = ""
164
      else:
165
        errmsg = ":%s" % fn(errnum)
166

    
167
      logger.error("verify error:num=%s%s", errnum, errmsg)
168

    
169
    return ok
170

    
171
  def __call__(self, ctx, logger):
172
    """Configures an SSL context to verify certificates.
173

174
    @type ctx: OpenSSL.SSL.Context
175
    @param ctx: SSL context
176

177
    """
178
    if self._use_default_verify_paths:
179
      ctx.set_default_verify_paths()
180

    
181
    if self._cafile or self._capath:
182
      if self._SUPPORT_CAPATH:
183
        ctx.load_verify_locations(self._cafile, self._capath)
184
      else:
185
        ctx.load_verify_locations(self._cafile)
186

    
187
    ctx.set_verify(OpenSSL.SSL.VERIFY_PEER,
188
                   lambda conn, cert, errnum, errdepth, ok: \
189
                     self._VerifySslCertCb(logger, conn, cert,
190
                                           errnum, errdepth, ok))
191

    
192

    
193
class _HTTPSConnectionOpenSSL(httplib.HTTPSConnection):
194
  """HTTPS Connection handler that verifies the SSL certificate.
195

196
  """
197
  def __init__(self, *args, **kwargs):
198
    """Initializes this class.
199

200
    """
201
    httplib.HTTPSConnection.__init__(self, *args, **kwargs)
202
    self._logger = None
203
    self._config_ssl_verification = None
204

    
205
  def Setup(self, logger, config_ssl_verification):
206
    """Sets the SSL verification config function.
207

208
    @param logger: Logging object
209
    @type config_ssl_verification: callable
210

211
    """
212
    assert self._logger is None
213
    assert self._config_ssl_verification is None
214

    
215
    self._logger = logger
216
    self._config_ssl_verification = config_ssl_verification
217

    
218
  def connect(self):
219
    """Connect to the server specified when the object was created.
220

221
    This ensures that SSL certificates are verified.
222

223
    """
224
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
225

    
226
    ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
227
    ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
228

    
229
    if self._config_ssl_verification:
230
      self._config_ssl_verification(ctx, self._logger)
231

    
232
    ssl = OpenSSL.SSL.Connection(ctx, sock)
233
    ssl.connect((self.host, self.port))
234

    
235
    self.sock = httplib.FakeSocket(sock, ssl)
236

    
237

    
238
class _HTTPSHandler(urllib2.HTTPSHandler):
239
  def __init__(self, logger, config_ssl_verification):
240
    """Initializes this class.
241

242
    @param logger: Logging object
243
    @type config_ssl_verification: callable
244
    @param config_ssl_verification: Function to configure SSL context for
245
                                    certificate verification
246

247
    """
248
    urllib2.HTTPSHandler.__init__(self)
249
    self._logger = logger
250
    self._config_ssl_verification = config_ssl_verification
251

    
252
  def _CreateHttpsConnection(self, *args, **kwargs):
253
    """Wrapper around L{_HTTPSConnectionOpenSSL} to add SSL verification.
254

255
    This wrapper is necessary provide a compatible API to urllib2.
256

257
    """
258
    conn = _HTTPSConnectionOpenSSL(*args, **kwargs)
259
    conn.Setup(self._logger, self._config_ssl_verification)
260
    return conn
261

    
262
  def https_open(self, req):
263
    """Creates HTTPS connection.
264

265
    Called by urllib2.
266

267
    """
268
    return self.do_open(self._CreateHttpsConnection, req)
269

    
270

    
271
class _RapiRequest(urllib2.Request):
272
  def __init__(self, method, url, headers, data):
273
    """Initializes this class.
274

275
    """
276
    urllib2.Request.__init__(self, url, data=data, headers=headers)
277
    self._method = method
278

    
279
  def get_method(self):
280
    """Returns the HTTP request method.
281

282
    """
283
    return self._method
284

    
285

    
286
class GanetiRapiClient(object):
287
  """Ganeti RAPI client.
288

289
  """
290
  USER_AGENT = "Ganeti RAPI Client"
291
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
292

    
293
  def __init__(self, host, port=GANETI_RAPI_PORT,
294
               username=None, password=None,
295
               config_ssl_verification=None, ignore_proxy=False,
296
               logger=logging):
297
    """Constructor.
298

299
    @type host: string
300
    @param host: the ganeti cluster master to interact with
301
    @type port: int
302
    @param port: the port on which the RAPI is running (default is 5080)
303
    @type username: string
304
    @param username: the username to connect with
305
    @type password: string
306
    @param password: the password to connect with
307
    @type config_ssl_verification: callable
308
    @param config_ssl_verification: Function to configure SSL context for
309
                                    certificate verification
310
    @type ignore_proxy: bool
311
    @param ignore_proxy: Whether to ignore proxy settings
312
    @param logger: Logging object
313

314
    """
315
    self._host = host
316
    self._port = port
317
    self._logger = logger
318

    
319
    self._base_url = "https://%s:%s" % (host, port)
320

    
321
    handlers = [_HTTPSHandler(self._logger, config_ssl_verification)]
322

    
323
    if username is not None:
324
      pwmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
325
      pwmgr.add_password(None, self._base_url, username, password)
326
      handlers.append(urllib2.HTTPBasicAuthHandler(pwmgr))
327
    elif password:
328
      raise Error("Specified password without username")
329

    
330
    if ignore_proxy:
331
      handlers.append(urllib2.ProxyHandler({}))
332

    
333
    self._http = urllib2.build_opener(*handlers) # pylint: disable-msg=W0142
334

    
335
    self._headers = {
336
      "Accept": HTTP_APP_JSON,
337
      "Content-type": HTTP_APP_JSON,
338
      "User-Agent": self.USER_AGENT,
339
      }
340

    
341
  @staticmethod
342
  def _EncodeQuery(query):
343
    """Encode query values for RAPI URL.
344

345
    @type query: list of two-tuples
346
    @param query: Query arguments
347
    @rtype: list
348
    @return: Query list with encoded values
349

350
    """
351
    result = []
352

    
353
    for name, value in query:
354
      if value is None:
355
        result.append((name, ""))
356

    
357
      elif isinstance(value, bool):
358
        # Boolean values must be encoded as 0 or 1
359
        result.append((name, int(value)))
360

    
361
      elif isinstance(value, (list, tuple, dict)):
362
        raise ValueError("Invalid query data type %r" % type(value).__name__)
363

    
364
      else:
365
        result.append((name, value))
366

    
367
    return result
368

    
369
  def _SendRequest(self, method, path, query, content):
370
    """Sends an HTTP request.
371

372
    This constructs a full URL, encodes and decodes HTTP bodies, and
373
    handles invalid responses in a pythonic way.
374

375
    @type method: string
376
    @param method: HTTP method to use
377
    @type path: string
378
    @param path: HTTP URL path
379
    @type query: list of two-tuples
380
    @param query: query arguments to pass to urllib.urlencode
381
    @type content: str or None
382
    @param content: HTTP body content
383

384
    @rtype: str
385
    @return: JSON-Decoded response
386

387
    @raises CertificateError: If an invalid SSL certificate is found
388
    @raises GanetiApiError: If an invalid response is returned
389

390
    """
391
    assert path.startswith("/")
392

    
393
    if content:
394
      encoded_content = self._json_encoder.encode(content)
395
    else:
396
      encoded_content = None
397

    
398
    # Build URL
399
    url = [self._base_url, path]
400
    if query:
401
      url.append("?")
402
      url.append(urllib.urlencode(self._EncodeQuery(query)))
403

    
404
    req = _RapiRequest(method, "".join(url), self._headers, encoded_content)
405

    
406
    try:
407
      resp = self._http.open(req)
408
      encoded_response_content = resp.read()
409
    except (OpenSSL.SSL.Error, OpenSSL.crypto.Error), err:
410
      raise CertificateError("SSL issue: %s" % err)
411

    
412
    if encoded_response_content:
413
      response_content = simplejson.loads(encoded_response_content)
414
    else:
415
      response_content = None
416

    
417
    # TODO: Are there other status codes that are valid? (redirect?)
418
    if resp.code != HTTP_OK:
419
      if isinstance(response_content, dict):
420
        msg = ("%s %s: %s" %
421
               (response_content["code"],
422
                response_content["message"],
423
                response_content["explain"]))
424
      else:
425
        msg = str(response_content)
426

    
427
      raise GanetiApiError(msg, code=resp.code)
428

    
429
    return response_content
430

    
431
  def GetVersion(self):
432
    """Gets the Remote API version running on the cluster.
433

434
    @rtype: int
435
    @return: Ganeti Remote API version
436

437
    """
438
    return self._SendRequest(HTTP_GET, "/version", None, None)
439

    
440
  def GetOperatingSystems(self):
441
    """Gets the Operating Systems running in the Ganeti cluster.
442

443
    @rtype: list of str
444
    @return: operating systems
445

446
    """
447
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
448
                             None, None)
449

    
450
  def GetInfo(self):
451
    """Gets info about the cluster.
452

453
    @rtype: dict
454
    @return: information about the cluster
455

456
    """
457
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
458
                             None, None)
459

    
460
  def GetClusterTags(self):
461
    """Gets the cluster tags.
462

463
    @rtype: list of str
464
    @return: cluster tags
465

466
    """
467
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
468
                             None, None)
469

    
470
  def AddClusterTags(self, tags, dry_run=False):
471
    """Adds tags to the cluster.
472

473
    @type tags: list of str
474
    @param tags: tags to add to the cluster
475
    @type dry_run: bool
476
    @param dry_run: whether to perform a dry run
477

478
    @rtype: int
479
    @return: job id
480

481
    """
482
    query = [("tag", t) for t in tags]
483
    if dry_run:
484
      query.append(("dry-run", 1))
485

    
486
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
487
                             query, None)
488

    
489
  def DeleteClusterTags(self, tags, dry_run=False):
490
    """Deletes tags from the cluster.
491

492
    @type tags: list of str
493
    @param tags: tags to delete
494
    @type dry_run: bool
495
    @param dry_run: whether to perform a dry run
496

497
    """
498
    query = [("tag", t) for t in tags]
499
    if dry_run:
500
      query.append(("dry-run", 1))
501

    
502
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
503
                             query, None)
504

    
505
  def GetInstances(self, bulk=False):
506
    """Gets information about instances on the cluster.
507

508
    @type bulk: bool
509
    @param bulk: whether to return all information about all instances
510

511
    @rtype: list of dict or list of str
512
    @return: if bulk is True, info about the instances, else a list of instances
513

514
    """
515
    query = []
516
    if bulk:
517
      query.append(("bulk", 1))
518

    
519
    instances = self._SendRequest(HTTP_GET,
520
                                  "/%s/instances" % GANETI_RAPI_VERSION,
521
                                  query, None)
522
    if bulk:
523
      return instances
524
    else:
525
      return [i["id"] for i in instances]
526

    
527
  def GetInstanceInfo(self, instance):
528
    """Gets information about an instance.
529

530
    @type instance: str
531
    @param instance: instance whose info to return
532

533
    @rtype: dict
534
    @return: info about the instance
535

536
    """
537
    return self._SendRequest(HTTP_GET,
538
                             ("/%s/instances/%s" %
539
                              (GANETI_RAPI_VERSION, instance)), None, None)
540

    
541
  def CreateInstance(self, dry_run=False):
542
    """Creates a new instance.
543

544
    @type dry_run: bool
545
    @param dry_run: whether to perform a dry run
546

547
    @rtype: int
548
    @return: job id
549

550
    """
551
    # TODO: Pass arguments needed to actually create an instance.
552
    query = []
553
    if dry_run:
554
      query.append(("dry-run", 1))
555

    
556
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
557
                             query, None)
558

    
559
  def DeleteInstance(self, instance, dry_run=False):
560
    """Deletes an instance.
561

562
    @type instance: str
563
    @param instance: the instance to delete
564

565
    @rtype: int
566
    @return: job id
567

568
    """
569
    query = []
570
    if dry_run:
571
      query.append(("dry-run", 1))
572

    
573
    return self._SendRequest(HTTP_DELETE,
574
                             ("/%s/instances/%s" %
575
                              (GANETI_RAPI_VERSION, instance)), query, None)
576

    
577
  def GetInstanceTags(self, instance):
578
    """Gets tags for an instance.
579

580
    @type instance: str
581
    @param instance: instance whose tags to return
582

583
    @rtype: list of str
584
    @return: tags for the instance
585

586
    """
587
    return self._SendRequest(HTTP_GET,
588
                             ("/%s/instances/%s/tags" %
589
                              (GANETI_RAPI_VERSION, instance)), None, None)
590

    
591
  def AddInstanceTags(self, instance, tags, dry_run=False):
592
    """Adds tags to an instance.
593

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

601
    @rtype: int
602
    @return: job id
603

604
    """
605
    query = [("tag", t) for t in tags]
606
    if dry_run:
607
      query.append(("dry-run", 1))
608

    
609
    return self._SendRequest(HTTP_PUT,
610
                             ("/%s/instances/%s/tags" %
611
                              (GANETI_RAPI_VERSION, instance)), query, None)
612

    
613
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
614
    """Deletes tags from an instance.
615

616
    @type instance: str
617
    @param instance: instance to delete tags from
618
    @type tags: list of str
619
    @param tags: tags to delete
620
    @type dry_run: bool
621
    @param dry_run: whether to perform a dry run
622

623
    """
624
    query = [("tag", t) for t in tags]
625
    if dry_run:
626
      query.append(("dry-run", 1))
627

    
628
    return self._SendRequest(HTTP_DELETE,
629
                             ("/%s/instances/%s/tags" %
630
                              (GANETI_RAPI_VERSION, instance)), query, None)
631

    
632
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
633
                     dry_run=False):
634
    """Reboots an instance.
635

636
    @type instance: str
637
    @param instance: instance to rebot
638
    @type reboot_type: str
639
    @param reboot_type: one of: hard, soft, full
640
    @type ignore_secondaries: bool
641
    @param ignore_secondaries: if True, ignores errors for the secondary node
642
        while re-assembling disks (in hard-reboot mode only)
643
    @type dry_run: bool
644
    @param dry_run: whether to perform a dry run
645

646
    """
647
    query = []
648
    if reboot_type:
649
      query.append(("type", reboot_type))
650
    if ignore_secondaries is not None:
651
      query.append(("ignore_secondaries", ignore_secondaries))
652
    if dry_run:
653
      query.append(("dry-run", 1))
654

    
655
    return self._SendRequest(HTTP_POST,
656
                             ("/%s/instances/%s/reboot" %
657
                              (GANETI_RAPI_VERSION, instance)), query, None)
658

    
659
  def ShutdownInstance(self, instance, dry_run=False):
660
    """Shuts down an instance.
661

662
    @type instance: str
663
    @param instance: the instance to shut down
664
    @type dry_run: bool
665
    @param dry_run: whether to perform a dry run
666

667
    """
668
    query = []
669
    if dry_run:
670
      query.append(("dry-run", 1))
671

    
672
    return self._SendRequest(HTTP_PUT,
673
                             ("/%s/instances/%s/shutdown" %
674
                              (GANETI_RAPI_VERSION, instance)), query, None)
675

    
676
  def StartupInstance(self, instance, dry_run=False):
677
    """Starts up an instance.
678

679
    @type instance: str
680
    @param instance: the instance to start up
681
    @type dry_run: bool
682
    @param dry_run: whether to perform a dry run
683

684
    """
685
    query = []
686
    if dry_run:
687
      query.append(("dry-run", 1))
688

    
689
    return self._SendRequest(HTTP_PUT,
690
                             ("/%s/instances/%s/startup" %
691
                              (GANETI_RAPI_VERSION, instance)), query, None)
692

    
693
  def ReinstallInstance(self, instance, os, no_startup=False):
694
    """Reinstalls an instance.
695

696
    @type instance: str
697
    @param instance: the instance to reinstall
698
    @type os: str
699
    @param os: the os to reinstall
700
    @type no_startup: bool
701
    @param no_startup: whether to start the instance automatically
702

703
    """
704
    query = [("os", os)]
705
    if no_startup:
706
      query.append(("nostartup", 1))
707
    return self._SendRequest(HTTP_POST,
708
                             ("/%s/instances/%s/reinstall" %
709
                              (GANETI_RAPI_VERSION, instance)), query, None)
710

    
711
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
712
                           remote_node=None, iallocator=None, dry_run=False):
713
    """Replaces disks on an instance.
714

715
    @type instance: str
716
    @param instance: instance whose disks to replace
717
    @type disks: list of ints
718
    @param disks: Indexes of disks to replace
719
    @type mode: str
720
    @param mode: replacement mode to use (defaults to replace_auto)
721
    @type remote_node: str or None
722
    @param remote_node: new secondary node to use (for use with
723
        replace_new_secondary mode)
724
    @type iallocator: str or None
725
    @param iallocator: instance allocator plugin to use (for use with
726
                       replace_auto mode)
727
    @type dry_run: bool
728
    @param dry_run: whether to perform a dry run
729

730
    @rtype: int
731
    @return: job id
732

733
    """
734
    query = [
735
      ("mode", mode),
736
      ]
737

    
738
    if disks:
739
      query.append(("disks", ",".join(str(idx) for idx in disks)))
740

    
741
    if remote_node:
742
      query.append(("remote_node", remote_node))
743

    
744
    if iallocator:
745
      query.append(("iallocator", iallocator))
746

    
747
    if dry_run:
748
      query.append(("dry-run", 1))
749

    
750
    return self._SendRequest(HTTP_POST,
751
                             ("/%s/instances/%s/replace-disks" %
752
                              (GANETI_RAPI_VERSION, instance)), query, None)
753

    
754
  def GetJobs(self):
755
    """Gets all jobs for the cluster.
756

757
    @rtype: list of int
758
    @return: job ids for the cluster
759

760
    """
761
    return [int(j["id"])
762
            for j in self._SendRequest(HTTP_GET,
763
                                       "/%s/jobs" % GANETI_RAPI_VERSION,
764
                                       None, None)]
765

    
766
  def GetJobStatus(self, job_id):
767
    """Gets the status of a job.
768

769
    @type job_id: int
770
    @param job_id: job id whose status to query
771

772
    @rtype: dict
773
    @return: job status
774

775
    """
776
    return self._SendRequest(HTTP_GET,
777
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
778
                             None, None)
779

    
780
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
781
    """Waits for job changes.
782

783
    @type job_id: int
784
    @param job_id: Job ID for which to wait
785

786
    """
787
    body = {
788
      "fields": fields,
789
      "previous_job_info": prev_job_info,
790
      "previous_log_serial": prev_log_serial,
791
      }
792

    
793
    return self._SendRequest(HTTP_GET,
794
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
795
                             None, body)
796

    
797
  def CancelJob(self, job_id, dry_run=False):
798
    """Cancels a job.
799

800
    @type job_id: int
801
    @param job_id: id of the job to delete
802
    @type dry_run: bool
803
    @param dry_run: whether to perform a dry run
804

805
    """
806
    query = []
807
    if dry_run:
808
      query.append(("dry-run", 1))
809

    
810
    return self._SendRequest(HTTP_DELETE,
811
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
812
                             query, None)
813

    
814
  def GetNodes(self, bulk=False):
815
    """Gets all nodes in the cluster.
816

817
    @type bulk: bool
818
    @param bulk: whether to return all information about all instances
819

820
    @rtype: list of dict or str
821
    @return: if bulk is true, info about nodes in the cluster,
822
        else list of nodes in the cluster
823

824
    """
825
    query = []
826
    if bulk:
827
      query.append(("bulk", 1))
828

    
829
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
830
                              query, None)
831
    if bulk:
832
      return nodes
833
    else:
834
      return [n["id"] for n in nodes]
835

    
836
  def GetNodeInfo(self, node):
837
    """Gets information about a node.
838

839
    @type node: str
840
    @param node: node whose info to return
841

842
    @rtype: dict
843
    @return: info about the node
844

845
    """
846
    return self._SendRequest(HTTP_GET,
847
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
848
                             None, None)
849

    
850
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
851
                   dry_run=False):
852
    """Evacuates instances from a Ganeti node.
853

854
    @type node: str
855
    @param node: node to evacuate
856
    @type iallocator: str or None
857
    @param iallocator: instance allocator to use
858
    @type remote_node: str
859
    @param remote_node: node to evaucate to
860
    @type dry_run: bool
861
    @param dry_run: whether to perform a dry run
862

863
    @rtype: int
864
    @return: job id
865

866
    @raises GanetiApiError: if an iallocator and remote_node are both specified
867

868
    """
869
    if iallocator and remote_node:
870
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
871

    
872
    query = []
873
    if iallocator:
874
      query.append(("iallocator", iallocator))
875
    if remote_node:
876
      query.append(("remote_node", remote_node))
877
    if dry_run:
878
      query.append(("dry-run", 1))
879

    
880
    return self._SendRequest(HTTP_POST,
881
                             ("/%s/nodes/%s/evacuate" %
882
                              (GANETI_RAPI_VERSION, node)), query, None)
883

    
884
  def MigrateNode(self, node, live=True, dry_run=False):
885
    """Migrates all primary instances from a node.
886

887
    @type node: str
888
    @param node: node to migrate
889
    @type live: bool
890
    @param live: whether to use live migration
891
    @type dry_run: bool
892
    @param dry_run: whether to perform a dry run
893

894
    @rtype: int
895
    @return: job id
896

897
    """
898
    query = []
899
    if live:
900
      query.append(("live", 1))
901
    if dry_run:
902
      query.append(("dry-run", 1))
903

    
904
    return self._SendRequest(HTTP_POST,
905
                             ("/%s/nodes/%s/migrate" %
906
                              (GANETI_RAPI_VERSION, node)), query, None)
907

    
908
  def GetNodeRole(self, node):
909
    """Gets the current role for a node.
910

911
    @type node: str
912
    @param node: node whose role to return
913

914
    @rtype: str
915
    @return: the current role for a node
916

917
    """
918
    return self._SendRequest(HTTP_GET,
919
                             ("/%s/nodes/%s/role" %
920
                              (GANETI_RAPI_VERSION, node)), None, None)
921

    
922
  def SetNodeRole(self, node, role, force=False):
923
    """Sets the role for a node.
924

925
    @type node: str
926
    @param node: the node whose role to set
927
    @type role: str
928
    @param role: the role to set for the node
929
    @type force: bool
930
    @param force: whether to force the role change
931

932
    @rtype: int
933
    @return: job id
934

935
    @raise InvalidNodeRole: If an invalid node role is specified
936

937
    """
938
    if role not in VALID_NODE_ROLES:
939
      raise InvalidNodeRole("%s is not a valid node role" % role)
940

    
941
    query = [("force", force)]
942

    
943
    return self._SendRequest(HTTP_PUT,
944
                             ("/%s/nodes/%s/role" %
945
                              (GANETI_RAPI_VERSION, node)), query, role)
946

    
947
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
948
    """Gets the storage units for a node.
949

950
    @type node: str
951
    @param node: the node whose storage units to return
952
    @type storage_type: str
953
    @param storage_type: storage type whose units to return
954
    @type output_fields: str
955
    @param output_fields: storage type fields to return
956

957
    @rtype: int
958
    @return: job id where results can be retrieved
959

960
    """
961
    query = [
962
      ("storage_type", storage_type),
963
      ("output_fields", output_fields),
964
      ]
965

    
966
    return self._SendRequest(HTTP_GET,
967
                             ("/%s/nodes/%s/storage" %
968
                              (GANETI_RAPI_VERSION, node)), query, None)
969

    
970
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
971
    """Modifies parameters of storage units on the node.
972

973
    @type node: str
974
    @param node: node whose storage units to modify
975
    @type storage_type: str
976
    @param storage_type: storage type whose units to modify
977
    @type name: str
978
    @param name: name of the storage unit
979
    @type allocatable: bool or None
980
    @param allocatable: Whether to set the "allocatable" flag on the storage
981
                        unit (None=no modification, True=set, False=unset)
982

983
    @rtype: int
984
    @return: job id
985

986
    """
987
    query = [
988
      ("storage_type", storage_type),
989
      ("name", name),
990
      ]
991

    
992
    if allocatable is not None:
993
      query.append(("allocatable", allocatable))
994

    
995
    return self._SendRequest(HTTP_PUT,
996
                             ("/%s/nodes/%s/storage/modify" %
997
                              (GANETI_RAPI_VERSION, node)), query, None)
998

    
999
  def RepairNodeStorageUnits(self, node, storage_type, name):
1000
    """Repairs a storage unit on the node.
1001

1002
    @type node: str
1003
    @param node: node whose storage units to repair
1004
    @type storage_type: str
1005
    @param storage_type: storage type to repair
1006
    @type name: str
1007
    @param name: name of the storage unit to repair
1008

1009
    @rtype: int
1010
    @return: job id
1011

1012
    """
1013
    query = [
1014
      ("storage_type", storage_type),
1015
      ("name", name),
1016
      ]
1017

    
1018
    return self._SendRequest(HTTP_PUT,
1019
                             ("/%s/nodes/%s/storage/repair" %
1020
                              (GANETI_RAPI_VERSION, node)), query, None)
1021

    
1022
  def GetNodeTags(self, node):
1023
    """Gets the tags for a node.
1024

1025
    @type node: str
1026
    @param node: node whose tags to return
1027

1028
    @rtype: list of str
1029
    @return: tags for the node
1030

1031
    """
1032
    return self._SendRequest(HTTP_GET,
1033
                             ("/%s/nodes/%s/tags" %
1034
                              (GANETI_RAPI_VERSION, node)), None, None)
1035

    
1036
  def AddNodeTags(self, node, tags, dry_run=False):
1037
    """Adds tags to a node.
1038

1039
    @type node: str
1040
    @param node: node to add tags to
1041
    @type tags: list of str
1042
    @param tags: tags to add to the node
1043
    @type dry_run: bool
1044
    @param dry_run: whether to perform a dry run
1045

1046
    @rtype: int
1047
    @return: job id
1048

1049
    """
1050
    query = [("tag", t) for t in tags]
1051
    if dry_run:
1052
      query.append(("dry-run", 1))
1053

    
1054
    return self._SendRequest(HTTP_PUT,
1055
                             ("/%s/nodes/%s/tags" %
1056
                              (GANETI_RAPI_VERSION, node)), query, tags)
1057

    
1058
  def DeleteNodeTags(self, node, tags, dry_run=False):
1059
    """Delete tags from a node.
1060

1061
    @type node: str
1062
    @param node: node to remove tags from
1063
    @type tags: list of str
1064
    @param tags: tags to remove from the node
1065
    @type dry_run: bool
1066
    @param dry_run: whether to perform a dry run
1067

1068
    @rtype: int
1069
    @return: job id
1070

1071
    """
1072
    query = [("tag", t) for t in tags]
1073
    if dry_run:
1074
      query.append(("dry-run", 1))
1075

    
1076
    return self._SendRequest(HTTP_DELETE,
1077
                             ("/%s/nodes/%s/tags" %
1078
                              (GANETI_RAPI_VERSION, node)), query, None)