Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / rapi.py @ 1316db51

History | View | Annotate | Download (53.3 kB)

1
#
2
#
3

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

    
22

    
23
"""Ganeti RAPI client."""
24

    
25
# No Ganeti-specific modules should be imported. The RAPI client is supposed to
26
# be standalone.
27

    
28
import requests
29
import logging
30
import simplejson
31
import time
32

    
33
GANETI_RAPI_PORT = 5080
34
GANETI_RAPI_VERSION = 2
35

    
36
HTTP_DELETE = "DELETE"
37
HTTP_GET = "GET"
38
HTTP_PUT = "PUT"
39
HTTP_POST = "POST"
40
HTTP_OK = 200
41
HTTP_NOT_FOUND = 404
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

    
49
NODE_EVAC_PRI = "primary-only"
50
NODE_EVAC_SEC = "secondary-only"
51
NODE_EVAC_ALL = "all"
52

    
53
NODE_ROLE_DRAINED = "drained"
54
NODE_ROLE_MASTER_CANDIATE = "master-candidate"
55
NODE_ROLE_MASTER = "master"
56
NODE_ROLE_OFFLINE = "offline"
57
NODE_ROLE_REGULAR = "regular"
58

    
59
JOB_STATUS_QUEUED = "queued"
60
JOB_STATUS_WAITING = "waiting"
61
JOB_STATUS_CANCELING = "canceling"
62
JOB_STATUS_RUNNING = "running"
63
JOB_STATUS_CANCELED = "canceled"
64
JOB_STATUS_SUCCESS = "success"
65
JOB_STATUS_ERROR = "error"
66
JOB_STATUS_FINALIZED = frozenset([
67
  JOB_STATUS_CANCELED,
68
  JOB_STATUS_SUCCESS,
69
  JOB_STATUS_ERROR,
70
  ])
71
JOB_STATUS_ALL = frozenset([
72
  JOB_STATUS_QUEUED,
73
  JOB_STATUS_WAITING,
74
  JOB_STATUS_CANCELING,
75
  JOB_STATUS_RUNNING,
76
  ]) | JOB_STATUS_FINALIZED
77

    
78
# Legacy name
79
JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
80

    
81
# Internal constants
82
_REQ_DATA_VERSION_FIELD = "__version__"
83
_QPARAM_DRY_RUN = "dry-run"
84
_QPARAM_FORCE = "force"
85

    
86
# Feature strings
87
INST_CREATE_REQV1 = "instance-create-reqv1"
88
INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
89
NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
90
NODE_EVAC_RES1 = "node-evac-res1"
91

    
92
# Old feature constant names in case they're references by users of this module
93
_INST_CREATE_REQV1 = INST_CREATE_REQV1
94
_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
95
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
96
_NODE_EVAC_RES1 = NODE_EVAC_RES1
97

    
98
#: Not enough resources (iallocator failure, disk space, memory, etc.)
99
ECODE_NORES = "insufficient_resources"
100

    
101
#: Temporarily out of resources; operation can be tried again
102
ECODE_TEMP_NORES = "temp_insufficient_resources"
103

    
104

    
105
class Error(Exception):
106
  """Base error class for this module.
107

108
  """
109
  pass
110

    
111

    
112
class GanetiApiError(Error):
113
  """Generic error raised from Ganeti API.
114

115
  """
116
  def __init__(self, msg, code=None):
117
    Error.__init__(self, msg)
118
    self.code = code
119

    
120

    
121
class CertificateError(GanetiApiError):
122
  """Raised when a problem is found with the SSL certificate.
123

124
  """
125
  pass
126

    
127

    
128
def _AppendIf(container, condition, value):
129
  """Appends to a list if a condition evaluates to truth.
130

131
  """
132
  if condition:
133
    container.append(value)
134

    
135
  return condition
136

    
137

    
138
def _AppendDryRunIf(container, condition):
139
  """Appends a "dry-run" parameter if a condition evaluates to truth.
140

141
  """
142
  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
143

    
144

    
145
def _AppendForceIf(container, condition):
146
  """Appends a "force" parameter if a condition evaluates to truth.
147

148
  """
149
  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
150

    
151

    
152
def _SetItemIf(container, condition, item, value):
153
  """Sets an item if a condition evaluates to truth.
154

155
  """
156
  if condition:
157
    container[item] = value
158

    
159
  return condition
160

    
161

    
162
class GanetiRapiClient(object): # pylint: disable=R0904
163
  """Ganeti RAPI client.
164

165
  """
166
  USER_AGENT = "Ganeti RAPI Client"
167
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
168

    
169
  def __init__(self, host, port=GANETI_RAPI_PORT,
170
               username=None, password=None, logger=logging):
171
    """Initializes this class.
172

173
    @type host: string
174
    @param host: the ganeti cluster master to interact with
175
    @type port: int
176
    @param port: the port on which the RAPI is running (default is 5080)
177
    @type username: string
178
    @param username: the username to connect with
179
    @type password: string
180
    @param password: the password to connect with
181
    @param logger: Logging object
182

183
    """
184
    self._logger = logger
185
    self._base_url = "https://%s:%s" % (host, port)
186

    
187
    if username is not None:
188
      if password is None:
189
        raise Error("Password not specified")
190
    elif password:
191
      raise Error("Specified password without username")
192

    
193
    self._auth = (username, password)
194

    
195
  def _SendRequest(self, method, path, query, content):
196
    """Sends an HTTP request.
197

198
    This constructs a full URL, encodes and decodes HTTP bodies, and
199
    handles invalid responses in a pythonic way.
200

201
    @type method: string
202
    @param method: HTTP method to use
203
    @type path: string
204
    @param path: HTTP URL path
205
    @type query: list of two-tuples
206
    @param query: query arguments to pass to urllib.urlencode
207
    @type content: str or None
208
    @param content: HTTP body content
209

210
    @rtype: str
211
    @return: JSON-Decoded response
212

213
    @raises CertificateError: If an invalid SSL certificate is found
214
    @raises GanetiApiError: If an invalid response is returned
215

216
    """
217
    assert path.startswith("/")
218
    url = "%s%s" % (self._base_url, path)
219

    
220
    headers = {}
221
    if content is not None:
222
      encoded_content = self._json_encoder.encode(content)
223
      headers = {"content-type": HTTP_APP_JSON,
224
                 "accept": HTTP_APP_JSON}
225
    else:
226
      encoded_content = ""
227

    
228
    if query is not None:
229
        query = dict(query)
230

    
231
    self._logger.debug("Sending request %s %s (query=%r) (content=%r)",
232
                       method, url, query, encoded_content)
233

    
234
    req_method = getattr(requests, method.lower())
235
    r = req_method(url, auth=self._auth, headers=headers, params=query,
236
                   data=encoded_content, verify=False)
237

    
238

    
239
    http_code = r.status_code
240
    if r.content is not None:
241
        response_content = simplejson.loads(r.content)
242
    else:
243
        response_content = None
244

    
245
    if http_code != HTTP_OK:
246
      if isinstance(response_content, dict):
247
        msg = ("%s %s: %s" %
248
               (response_content["code"],
249
                response_content["message"],
250
                response_content["explain"]))
251
      else:
252
        msg = str(response_content)
253

    
254
      raise GanetiApiError(msg, code=http_code)
255

    
256
    return response_content
257

    
258
  def GetVersion(self):
259
    """Gets the Remote API version running on the cluster.
260

261
    @rtype: int
262
    @return: Ganeti Remote API version
263

264
    """
265
    return self._SendRequest(HTTP_GET, "/version", None, None)
266

    
267
  def GetFeatures(self):
268
    """Gets the list of optional features supported by RAPI server.
269

270
    @rtype: list
271
    @return: List of optional features
272

273
    """
274
    try:
275
      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
276
                               None, None)
277
    except GanetiApiError, err:
278
      # Older RAPI servers don't support this resource
279
      if err.code == HTTP_NOT_FOUND:
280
        return []
281

    
282
      raise
283

    
284
  def GetOperatingSystems(self):
285
    """Gets the Operating Systems running in the Ganeti cluster.
286

287
    @rtype: list of str
288
    @return: operating systems
289

290
    """
291
    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
292
                             None, None)
293

    
294
  def GetInfo(self):
295
    """Gets info about the cluster.
296

297
    @rtype: dict
298
    @return: information about the cluster
299

300
    """
301
    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
302
                             None, None)
303

    
304
  def RedistributeConfig(self):
305
    """Tells the cluster to redistribute its configuration files.
306

307
    @rtype: string
308
    @return: job id
309

310
    """
311
    return self._SendRequest(HTTP_PUT,
312
                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
313
                             None, None)
314

    
315
  def ModifyCluster(self, **kwargs):
316
    """Modifies cluster parameters.
317

318
    More details for parameters can be found in the RAPI documentation.
319

320
    @rtype: string
321
    @return: job id
322

323
    """
324
    body = kwargs
325

    
326
    return self._SendRequest(HTTP_PUT,
327
                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
328

    
329
  def GetClusterTags(self):
330
    """Gets the cluster tags.
331

332
    @rtype: list of str
333
    @return: cluster tags
334

335
    """
336
    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
337
                             None, None)
338

    
339
  def AddClusterTags(self, tags, dry_run=False):
340
    """Adds tags to the cluster.
341

342
    @type tags: list of str
343
    @param tags: tags to add to the cluster
344
    @type dry_run: bool
345
    @param dry_run: whether to perform a dry run
346

347
    @rtype: string
348
    @return: job id
349

350
    """
351
    query = [("tag", t) for t in tags]
352
    _AppendDryRunIf(query, dry_run)
353

    
354
    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
355
                             query, None)
356

    
357
  def DeleteClusterTags(self, tags, dry_run=False):
358
    """Deletes tags from the cluster.
359

360
    @type tags: list of str
361
    @param tags: tags to delete
362
    @type dry_run: bool
363
    @param dry_run: whether to perform a dry run
364
    @rtype: string
365
    @return: job id
366

367
    """
368
    query = [("tag", t) for t in tags]
369
    _AppendDryRunIf(query, dry_run)
370

    
371
    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
372
                             query, None)
373

    
374
  def GetInstances(self, bulk=False):
375
    """Gets information about instances on the cluster.
376

377
    @type bulk: bool
378
    @param bulk: whether to return all information about all instances
379

380
    @rtype: list of dict or list of str
381
    @return: if bulk is True, info about the instances, else a list of instances
382

383
    """
384
    query = []
385
    _AppendIf(query, bulk, ("bulk", 1))
386

    
387
    instances = self._SendRequest(HTTP_GET,
388
                                  "/%s/instances" % GANETI_RAPI_VERSION,
389
                                  query, None)
390
    if bulk:
391
      return instances
392
    else:
393
      return [i["id"] for i in instances]
394

    
395
  def GetInstance(self, instance):
396
    """Gets information about an instance.
397

398
    @type instance: str
399
    @param instance: instance whose info to return
400

401
    @rtype: dict
402
    @return: info about the instance
403

404
    """
405
    return self._SendRequest(HTTP_GET,
406
                             ("/%s/instances/%s" %
407
                              (GANETI_RAPI_VERSION, instance)), None, None)
408

    
409
  def GetInstanceInfo(self, instance, static=None):
410
    """Gets information about an instance.
411

412
    @type instance: string
413
    @param instance: Instance name
414
    @rtype: string
415
    @return: Job ID
416

417
    """
418
    if static is not None:
419
      query = [("static", static)]
420
    else:
421
      query = None
422

    
423
    return self._SendRequest(HTTP_GET,
424
                             ("/%s/instances/%s/info" %
425
                              (GANETI_RAPI_VERSION, instance)), query, None)
426

    
427
  def CreateInstance(self, mode, name, disk_template, disks, nics,
428
                     **kwargs):
429
    """Creates a new instance.
430

431
    More details for parameters can be found in the RAPI documentation.
432

433
    @type mode: string
434
    @param mode: Instance creation mode
435
    @type name: string
436
    @param name: Hostname of the instance to create
437
    @type disk_template: string
438
    @param disk_template: Disk template for instance (e.g. plain, diskless,
439
                          file, or drbd)
440
    @type disks: list of dicts
441
    @param disks: List of disk definitions
442
    @type nics: list of dicts
443
    @param nics: List of NIC definitions
444
    @type dry_run: bool
445
    @keyword dry_run: whether to perform a dry run
446

447
    @rtype: string
448
    @return: job id
449

450
    """
451
    query = []
452

    
453
    _AppendDryRunIf(query, kwargs.get("dry_run"))
454

    
455
    if _INST_CREATE_REQV1 in self.GetFeatures():
456
      # All required fields for request data version 1
457
      body = {
458
        _REQ_DATA_VERSION_FIELD: 1,
459
        "mode": mode,
460
        "name": name,
461
        "disk_template": disk_template,
462
        "disks": disks,
463
        "nics": nics,
464
        }
465

    
466
      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
467
      if conflicts:
468
        raise GanetiApiError("Required fields cannot be specified as"
469
                             " keywords: %s" % ", ".join(conflicts))
470

    
471
      body.update((key, value) for key, value in kwargs.iteritems()
472
                  if key != "dry_run")
473
    else:
474
      raise GanetiApiError("Server does not support new-style (version 1)"
475
                           " instance creation requests")
476

    
477
    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
478
                             query, body)
479

    
480
  def DeleteInstance(self, instance, dry_run=False, shutdown_timeout=None):
481
    """Deletes an instance.
482

483
    @type instance: str
484
    @param instance: the instance to delete
485

486
    @rtype: string
487
    @return: job id
488

489
    """
490
    query = []
491
    _AppendDryRunIf(query, dry_run)
492

    
493
    body = None
494
    if shutdown_timeout is not None:
495
        body = {"shutdown_timeout": shutdown_timeout}
496

    
497
    return self._SendRequest(HTTP_DELETE,
498
                             ("/%s/instances/%s" %
499
                              (GANETI_RAPI_VERSION, instance)), query, body)
500

    
501
  def ModifyInstance(self, instance, **kwargs):
502
    """Modifies an instance.
503

504
    More details for parameters can be found in the RAPI documentation.
505

506
    @type instance: string
507
    @param instance: Instance name
508
    @rtype: string
509
    @return: job id
510

511
    """
512
    body = kwargs
513

    
514
    return self._SendRequest(HTTP_PUT,
515
                             ("/%s/instances/%s/modify" %
516
                              (GANETI_RAPI_VERSION, instance)), None, body)
517

    
518
  def ActivateInstanceDisks(self, instance, ignore_size=None):
519
    """Activates an instance's disks.
520

521
    @type instance: string
522
    @param instance: Instance name
523
    @type ignore_size: bool
524
    @param ignore_size: Whether to ignore recorded size
525
    @rtype: string
526
    @return: job id
527

528
    """
529
    query = []
530
    _AppendIf(query, ignore_size, ("ignore_size", 1))
531

    
532
    return self._SendRequest(HTTP_PUT,
533
                             ("/%s/instances/%s/activate-disks" %
534
                              (GANETI_RAPI_VERSION, instance)), query, None)
535

    
536
  def DeactivateInstanceDisks(self, instance):
537
    """Deactivates an instance's disks.
538

539
    @type instance: string
540
    @param instance: Instance name
541
    @rtype: string
542
    @return: job id
543

544
    """
545
    return self._SendRequest(HTTP_PUT,
546
                             ("/%s/instances/%s/deactivate-disks" %
547
                              (GANETI_RAPI_VERSION, instance)), None, None)
548

    
549
  def SnapshotInstance(self, instance, snapshot_name, dry_run=False):
550
    """Replaces disks on an instance.
551

552
    @type instance: str
553
    @param instance: instance whose disks to replace
554
    @type snapshot_name: str
555
    @param snapshot_name: name of the new snapshot
556

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

560
    """
561

    
562
    body = {
563
      "disks": [(0, {"snapshot_name": snapshot_name})],
564
      }
565

    
566
    query = []
567
    _AppendDryRunIf(query, dry_run)
568

    
569
    return self._SendRequest(HTTP_PUT,
570
                             ("/%s/instances/%s/snapshot" %
571
                              (GANETI_RAPI_VERSION, instance)), query, body)
572

    
573
  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
574
    """Recreate an instance's disks.
575

576
    @type instance: string
577
    @param instance: Instance name
578
    @type disks: list of int
579
    @param disks: List of disk indexes
580
    @type nodes: list of string
581
    @param nodes: New instance nodes, if relocation is desired
582
    @rtype: string
583
    @return: job id
584

585
    """
586
    body = {}
587
    _SetItemIf(body, disks is not None, "disks", disks)
588
    _SetItemIf(body, nodes is not None, "nodes", nodes)
589

    
590
    return self._SendRequest(HTTP_POST,
591
                             ("/%s/instances/%s/recreate-disks" %
592
                              (GANETI_RAPI_VERSION, instance)), None, body)
593

    
594
  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
595
    """Grows a disk of an instance.
596

597
    More details for parameters can be found in the RAPI documentation.
598

599
    @type instance: string
600
    @param instance: Instance name
601
    @type disk: integer
602
    @param disk: Disk index
603
    @type amount: integer
604
    @param amount: Grow disk by this amount (MiB)
605
    @type wait_for_sync: bool
606
    @param wait_for_sync: Wait for disk to synchronize
607
    @rtype: string
608
    @return: job id
609

610
    """
611
    body = {
612
      "amount": amount,
613
      }
614

    
615
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
616

    
617
    return self._SendRequest(HTTP_POST,
618
                             ("/%s/instances/%s/disk/%s/grow" %
619
                              (GANETI_RAPI_VERSION, instance, disk)),
620
                             None, body)
621

    
622
  def GetInstanceTags(self, instance):
623
    """Gets tags for an instance.
624

625
    @type instance: str
626
    @param instance: instance whose tags to return
627

628
    @rtype: list of str
629
    @return: tags for the instance
630

631
    """
632
    return self._SendRequest(HTTP_GET,
633
                             ("/%s/instances/%s/tags" %
634
                              (GANETI_RAPI_VERSION, instance)), None, None)
635

    
636
  def AddInstanceTags(self, instance, tags, dry_run=False):
637
    """Adds tags to an instance.
638

639
    @type instance: str
640
    @param instance: instance to add tags to
641
    @type tags: list of str
642
    @param tags: tags to add to the instance
643
    @type dry_run: bool
644
    @param dry_run: whether to perform a dry run
645

646
    @rtype: string
647
    @return: job id
648

649
    """
650
    query = [("tag", t) for t in tags]
651
    _AppendDryRunIf(query, dry_run)
652

    
653
    return self._SendRequest(HTTP_PUT,
654
                             ("/%s/instances/%s/tags" %
655
                              (GANETI_RAPI_VERSION, instance)), query, None)
656

    
657
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
658
    """Deletes tags from an instance.
659

660
    @type instance: str
661
    @param instance: instance to delete tags from
662
    @type tags: list of str
663
    @param tags: tags to delete
664
    @type dry_run: bool
665
    @param dry_run: whether to perform a dry run
666
    @rtype: string
667
    @return: job id
668

669
    """
670
    query = [("tag", t) for t in tags]
671
    _AppendDryRunIf(query, dry_run)
672

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

    
677
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
678
                     dry_run=False, shutdown_timeout=None):
679
    """Reboots an instance.
680

681
    @type instance: str
682
    @param instance: instance to rebot
683
    @type reboot_type: str
684
    @param reboot_type: one of: hard, soft, full
685
    @type ignore_secondaries: bool
686
    @param ignore_secondaries: if True, ignores errors for the secondary node
687
        while re-assembling disks (in hard-reboot mode only)
688
    @type dry_run: bool
689
    @param dry_run: whether to perform a dry run
690
    @rtype: string
691
    @return: job id
692

693
    """
694
    query = []
695
    _AppendDryRunIf(query, dry_run)
696
    _AppendIf(query, reboot_type, ("type", reboot_type))
697
    _AppendIf(query, ignore_secondaries is not None,
698
              ("ignore_secondaries", ignore_secondaries))
699

    
700
    body = None
701
    if shutdown_timeout is not None:
702
        body = {"shutdown_timeout": shutdown_timeout}
703

    
704
    return self._SendRequest(HTTP_POST,
705
                             ("/%s/instances/%s/reboot" %
706
                              (GANETI_RAPI_VERSION, instance)), query, body)
707

    
708
  def ShutdownInstance(self, instance, dry_run=False, no_remember=False,
709
                       timeout=None):
710
    """Shuts down an instance.
711

712
    @type instance: str
713
    @param instance: the instance to shut down
714
    @type dry_run: bool
715
    @param dry_run: whether to perform a dry run
716
    @type no_remember: bool
717
    @param no_remember: if true, will not record the state change
718
    @rtype: string
719
    @return: job id
720

721
    """
722
    query = []
723
    _AppendDryRunIf(query, dry_run)
724
    _AppendIf(query, no_remember, ("no-remember", 1))
725
    body = None
726
    if timeout is not None:
727
        body = {"timeout": timeout}
728

    
729

    
730
    return self._SendRequest(HTTP_PUT,
731
                             ("/%s/instances/%s/shutdown" %
732
                              (GANETI_RAPI_VERSION, instance)), query, body)
733

    
734
  def StartupInstance(self, instance, dry_run=False, no_remember=False):
735
    """Starts up an instance.
736

737
    @type instance: str
738
    @param instance: the instance to start up
739
    @type dry_run: bool
740
    @param dry_run: whether to perform a dry run
741
    @type no_remember: bool
742
    @param no_remember: if true, will not record the state change
743
    @rtype: string
744
    @return: job id
745

746
    """
747
    query = []
748
    _AppendDryRunIf(query, dry_run)
749
    _AppendIf(query, no_remember, ("no-remember", 1))
750

    
751
    return self._SendRequest(HTTP_PUT,
752
                             ("/%s/instances/%s/startup" %
753
                              (GANETI_RAPI_VERSION, instance)), query, None)
754

    
755
  def ReinstallInstance(self, instance, os=None, no_startup=False,
756
                        osparams=None):
757
    """Reinstalls an instance.
758

759
    @type instance: str
760
    @param instance: The instance to reinstall
761
    @type os: str or None
762
    @param os: The operating system to reinstall. If None, the instance's
763
        current operating system will be installed again
764
    @type no_startup: bool
765
    @param no_startup: Whether to start the instance automatically
766
    @rtype: string
767
    @return: job id
768

769
    """
770
    if _INST_REINSTALL_REQV1 in self.GetFeatures():
771
      body = {
772
        "start": not no_startup,
773
        }
774
      _SetItemIf(body, os is not None, "os", os)
775
      _SetItemIf(body, osparams is not None, "osparams", osparams)
776
      return self._SendRequest(HTTP_POST,
777
                               ("/%s/instances/%s/reinstall" %
778
                                (GANETI_RAPI_VERSION, instance)), None, body)
779

    
780
    # Use old request format
781
    if osparams:
782
      raise GanetiApiError("Server does not support specifying OS parameters"
783
                           " for instance reinstallation")
784

    
785
    query = []
786
    _AppendIf(query, os, ("os", os))
787
    _AppendIf(query, no_startup, ("nostartup", 1))
788

    
789
    return self._SendRequest(HTTP_POST,
790
                             ("/%s/instances/%s/reinstall" %
791
                              (GANETI_RAPI_VERSION, instance)), query, None)
792

    
793
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
794
                           remote_node=None, iallocator=None):
795
    """Replaces disks on an instance.
796

797
    @type instance: str
798
    @param instance: instance whose disks to replace
799
    @type disks: list of ints
800
    @param disks: Indexes of disks to replace
801
    @type mode: str
802
    @param mode: replacement mode to use (defaults to replace_auto)
803
    @type remote_node: str or None
804
    @param remote_node: new secondary node to use (for use with
805
        replace_new_secondary mode)
806
    @type iallocator: str or None
807
    @param iallocator: instance allocator plugin to use (for use with
808
                       replace_auto mode)
809

810
    @rtype: string
811
    @return: job id
812

813
    """
814
    query = [
815
      ("mode", mode),
816
      ]
817

    
818
    # TODO: Convert to body parameters
819

    
820
    if disks is not None:
821
      _AppendIf(query, True,
822
                ("disks", ",".join(str(idx) for idx in disks)))
823

    
824
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
825
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
826

    
827
    return self._SendRequest(HTTP_POST,
828
                             ("/%s/instances/%s/replace-disks" %
829
                              (GANETI_RAPI_VERSION, instance)), query, None)
830

    
831
  def PrepareExport(self, instance, mode):
832
    """Prepares an instance for an export.
833

834
    @type instance: string
835
    @param instance: Instance name
836
    @type mode: string
837
    @param mode: Export mode
838
    @rtype: string
839
    @return: Job ID
840

841
    """
842
    query = [("mode", mode)]
843
    return self._SendRequest(HTTP_PUT,
844
                             ("/%s/instances/%s/prepare-export" %
845
                              (GANETI_RAPI_VERSION, instance)), query, None)
846

    
847
  def ExportInstance(self, instance, mode, destination, shutdown=None,
848
                     remove_instance=None,
849
                     x509_key_name=None, destination_x509_ca=None):
850
    """Exports an instance.
851

852
    @type instance: string
853
    @param instance: Instance name
854
    @type mode: string
855
    @param mode: Export mode
856
    @rtype: string
857
    @return: Job ID
858

859
    """
860
    body = {
861
      "destination": destination,
862
      "mode": mode,
863
      }
864

    
865
    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
866
    _SetItemIf(body, remove_instance is not None,
867
               "remove_instance", remove_instance)
868
    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
869
    _SetItemIf(body, destination_x509_ca is not None,
870
               "destination_x509_ca", destination_x509_ca)
871

    
872
    return self._SendRequest(HTTP_PUT,
873
                             ("/%s/instances/%s/export" %
874
                              (GANETI_RAPI_VERSION, instance)), None, body)
875

    
876
  def MigrateInstance(self, instance, mode=None, cleanup=None):
877
    """Migrates an instance.
878

879
    @type instance: string
880
    @param instance: Instance name
881
    @type mode: string
882
    @param mode: Migration mode
883
    @type cleanup: bool
884
    @param cleanup: Whether to clean up a previously failed migration
885
    @rtype: string
886
    @return: job id
887

888
    """
889
    body = {}
890
    _SetItemIf(body, mode is not None, "mode", mode)
891
    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
892

    
893
    return self._SendRequest(HTTP_PUT,
894
                             ("/%s/instances/%s/migrate" %
895
                              (GANETI_RAPI_VERSION, instance)), None, body)
896

    
897
  def FailoverInstance(self, instance, iallocator=None,
898
                       ignore_consistency=None, target_node=None):
899
    """Does a failover of an instance.
900

901
    @type instance: string
902
    @param instance: Instance name
903
    @type iallocator: string
904
    @param iallocator: Iallocator for deciding the target node for
905
      shared-storage instances
906
    @type ignore_consistency: bool
907
    @param ignore_consistency: Whether to ignore disk consistency
908
    @type target_node: string
909
    @param target_node: Target node for shared-storage instances
910
    @rtype: string
911
    @return: job id
912

913
    """
914
    body = {}
915
    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
916
    _SetItemIf(body, ignore_consistency is not None,
917
               "ignore_consistency", ignore_consistency)
918
    _SetItemIf(body, target_node is not None, "target_node", target_node)
919

    
920
    return self._SendRequest(HTTP_PUT,
921
                             ("/%s/instances/%s/failover" %
922
                              (GANETI_RAPI_VERSION, instance)), None, body)
923

    
924
  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
925
    """Changes the name of an instance.
926

927
    @type instance: string
928
    @param instance: Instance name
929
    @type new_name: string
930
    @param new_name: New instance name
931
    @type ip_check: bool
932
    @param ip_check: Whether to ensure instance's IP address is inactive
933
    @type name_check: bool
934
    @param name_check: Whether to ensure instance's name is resolvable
935
    @rtype: string
936
    @return: job id
937

938
    """
939
    body = {
940
      "new_name": new_name,
941
      }
942

    
943
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
944
    _SetItemIf(body, name_check is not None, "name_check", name_check)
945

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

    
950
  def GetInstanceConsole(self, instance):
951
    """Request information for connecting to instance's console.
952

953
    @type instance: string
954
    @param instance: Instance name
955
    @rtype: dict
956
    @return: dictionary containing information about instance's console
957

958
    """
959
    return self._SendRequest(HTTP_GET,
960
                             ("/%s/instances/%s/console" %
961
                              (GANETI_RAPI_VERSION, instance)), None, None)
962

    
963
  def GetJobs(self, bulk=False):
964
    """Gets all jobs for the cluster.
965

966
    @rtype: list of int
967
    @return: job ids for the cluster
968

969
    """
970
    query = []
971
    _AppendIf(query, bulk, ("bulk", 1))
972

    
973
    jobs = self._SendRequest(HTTP_GET, "/%s/jobs" % GANETI_RAPI_VERSION,
974
                             query, None)
975
    if bulk:
976
        return jobs
977
    else:
978
        return [int(j["id"]) for j in jobs]
979

    
980

    
981
  def GetJobStatus(self, job_id):
982
    """Gets the status of a job.
983

984
    @type job_id: string
985
    @param job_id: job id whose status to query
986

987
    @rtype: dict
988
    @return: job status
989

990
    """
991
    return self._SendRequest(HTTP_GET,
992
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
993
                             None, None)
994

    
995
  def WaitForJobCompletion(self, job_id, period=5, retries=-1):
996
    """Polls cluster for job status until completion.
997

998
    Completion is defined as any of the following states listed in
999
    L{JOB_STATUS_FINALIZED}.
1000

1001
    @type job_id: string
1002
    @param job_id: job id to watch
1003
    @type period: int
1004
    @param period: how often to poll for status (optional, default 5s)
1005
    @type retries: int
1006
    @param retries: how many time to poll before giving up
1007
                    (optional, default -1 means unlimited)
1008

1009
    @rtype: bool
1010
    @return: C{True} if job succeeded or C{False} if failed/status timeout
1011
    @deprecated: It is recommended to use L{WaitForJobChange} wherever
1012
      possible; L{WaitForJobChange} returns immediately after a job changed and
1013
      does not use polling
1014

1015
    """
1016
    while retries != 0:
1017
      job_result = self.GetJobStatus(job_id)
1018

    
1019
      if job_result and job_result["status"] == JOB_STATUS_SUCCESS:
1020
        return True
1021
      elif not job_result or job_result["status"] in JOB_STATUS_FINALIZED:
1022
        return False
1023

    
1024
      if period:
1025
        time.sleep(period)
1026

    
1027
      if retries > 0:
1028
        retries -= 1
1029

    
1030
    return False
1031

    
1032
  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
1033
    """Waits for job changes.
1034

1035
    @type job_id: string
1036
    @param job_id: Job ID for which to wait
1037
    @return: C{None} if no changes have been detected and a dict with two keys,
1038
      C{job_info} and C{log_entries} otherwise.
1039
    @rtype: dict
1040

1041
    """
1042
    body = {
1043
      "fields": fields,
1044
      "previous_job_info": prev_job_info,
1045
      "previous_log_serial": prev_log_serial,
1046
      }
1047

    
1048
    return self._SendRequest(HTTP_GET,
1049
                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
1050
                             None, body)
1051

    
1052
  def CancelJob(self, job_id, dry_run=False):
1053
    """Cancels a job.
1054

1055
    @type job_id: string
1056
    @param job_id: id of the job to delete
1057
    @type dry_run: bool
1058
    @param dry_run: whether to perform a dry run
1059
    @rtype: tuple
1060
    @return: tuple containing the result, and a message (bool, string)
1061

1062
    """
1063
    query = []
1064
    _AppendDryRunIf(query, dry_run)
1065

    
1066
    return self._SendRequest(HTTP_DELETE,
1067
                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
1068
                             query, None)
1069

    
1070
  def GetNodes(self, bulk=False):
1071
    """Gets all nodes in the cluster.
1072

1073
    @type bulk: bool
1074
    @param bulk: whether to return all information about all instances
1075

1076
    @rtype: list of dict or str
1077
    @return: if bulk is true, info about nodes in the cluster,
1078
        else list of nodes in the cluster
1079

1080
    """
1081
    query = []
1082
    _AppendIf(query, bulk, ("bulk", 1))
1083

    
1084
    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
1085
                              query, None)
1086
    if bulk:
1087
      return nodes
1088
    else:
1089
      return [n["id"] for n in nodes]
1090

    
1091
  def GetNode(self, node):
1092
    """Gets information about a node.
1093

1094
    @type node: str
1095
    @param node: node whose info to return
1096

1097
    @rtype: dict
1098
    @return: info about the node
1099

1100
    """
1101
    return self._SendRequest(HTTP_GET,
1102
                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
1103
                             None, None)
1104

    
1105
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
1106
                   dry_run=False, early_release=None,
1107
                   mode=None, accept_old=False):
1108
    """Evacuates instances from a Ganeti node.
1109

1110
    @type node: str
1111
    @param node: node to evacuate
1112
    @type iallocator: str or None
1113
    @param iallocator: instance allocator to use
1114
    @type remote_node: str
1115
    @param remote_node: node to evaucate to
1116
    @type dry_run: bool
1117
    @param dry_run: whether to perform a dry run
1118
    @type early_release: bool
1119
    @param early_release: whether to enable parallelization
1120
    @type mode: string
1121
    @param mode: Node evacuation mode
1122
    @type accept_old: bool
1123
    @param accept_old: Whether caller is ready to accept old-style (pre-2.5)
1124
        results
1125

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

1132
    @raises GanetiApiError: if an iallocator and remote_node are both
1133
        specified
1134

1135
    """
1136
    if iallocator and remote_node:
1137
      raise GanetiApiError("Only one of iallocator or remote_node can be used")
1138

    
1139
    query = []
1140
    _AppendDryRunIf(query, dry_run)
1141

    
1142
    if _NODE_EVAC_RES1 in self.GetFeatures():
1143
      # Server supports body parameters
1144
      body = {}
1145

    
1146
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1147
      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
1148
      _SetItemIf(body, early_release is not None,
1149
                 "early_release", early_release)
1150
      _SetItemIf(body, mode is not None, "mode", mode)
1151
    else:
1152
      # Pre-2.5 request format
1153
      body = None
1154

    
1155
      if not accept_old:
1156
        raise GanetiApiError("Server is version 2.4 or earlier and caller does"
1157
                             " not accept old-style results (parameter"
1158
                             " accept_old)")
1159

    
1160
      # Pre-2.5 servers can only evacuate secondaries
1161
      if mode is not None and mode != NODE_EVAC_SEC:
1162
        raise GanetiApiError("Server can only evacuate secondary instances")
1163

    
1164
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1165
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1166
      _AppendIf(query, early_release, ("early_release", 1))
1167

    
1168
    return self._SendRequest(HTTP_POST,
1169
                             ("/%s/nodes/%s/evacuate" %
1170
                              (GANETI_RAPI_VERSION, node)), query, body)
1171

    
1172
  def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None,
1173
                  target_node=None):
1174
    """Migrates all primary instances from a node.
1175

1176
    @type node: str
1177
    @param node: node to migrate
1178
    @type mode: string
1179
    @param mode: if passed, it will overwrite the live migration type,
1180
        otherwise the hypervisor default will be used
1181
    @type dry_run: bool
1182
    @param dry_run: whether to perform a dry run
1183
    @type iallocator: string
1184
    @param iallocator: instance allocator to use
1185
    @type target_node: string
1186
    @param target_node: Target node for shared-storage instances
1187

1188
    @rtype: string
1189
    @return: job id
1190

1191
    """
1192
    query = []
1193
    _AppendDryRunIf(query, dry_run)
1194

    
1195
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1196
      body = {}
1197

    
1198
      _SetItemIf(body, mode is not None, "mode", mode)
1199
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1200
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1201

    
1202
      assert len(query) <= 1
1203

    
1204
      return self._SendRequest(HTTP_POST,
1205
                               ("/%s/nodes/%s/migrate" %
1206
                                (GANETI_RAPI_VERSION, node)), query, body)
1207
    else:
1208
      # Use old request format
1209
      if target_node is not None:
1210
        raise GanetiApiError("Server does not support specifying target node"
1211
                             " for node migration")
1212

    
1213
      _AppendIf(query, mode is not None, ("mode", mode))
1214

    
1215
      return self._SendRequest(HTTP_POST,
1216
                               ("/%s/nodes/%s/migrate" %
1217
                                (GANETI_RAPI_VERSION, node)), query, None)
1218

    
1219
  def GetNodeRole(self, node):
1220
    """Gets the current role for a node.
1221

1222
    @type node: str
1223
    @param node: node whose role to return
1224

1225
    @rtype: str
1226
    @return: the current role for a node
1227

1228
    """
1229
    return self._SendRequest(HTTP_GET,
1230
                             ("/%s/nodes/%s/role" %
1231
                              (GANETI_RAPI_VERSION, node)), None, None)
1232

    
1233
  def SetNodeRole(self, node, role, force=False, auto_promote=None):
1234
    """Sets the role for a node.
1235

1236
    @type node: str
1237
    @param node: the node whose role to set
1238
    @type role: str
1239
    @param role: the role to set for the node
1240
    @type force: bool
1241
    @param force: whether to force the role change
1242
    @type auto_promote: bool
1243
    @param auto_promote: Whether node(s) should be promoted to master candidate
1244
                         if necessary
1245

1246
    @rtype: string
1247
    @return: job id
1248

1249
    """
1250
    query = []
1251
    _AppendForceIf(query, force)
1252
    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
1253

    
1254
    return self._SendRequest(HTTP_PUT,
1255
                             ("/%s/nodes/%s/role" %
1256
                              (GANETI_RAPI_VERSION, node)), query, role)
1257

    
1258
  def PowercycleNode(self, node, force=False):
1259
    """Powercycles a node.
1260

1261
    @type node: string
1262
    @param node: Node name
1263
    @type force: bool
1264
    @param force: Whether to force the operation
1265
    @rtype: string
1266
    @return: job id
1267

1268
    """
1269
    query = []
1270
    _AppendForceIf(query, force)
1271

    
1272
    return self._SendRequest(HTTP_POST,
1273
                             ("/%s/nodes/%s/powercycle" %
1274
                              (GANETI_RAPI_VERSION, node)), query, None)
1275

    
1276
  def ModifyNode(self, node, **kwargs):
1277
    """Modifies a node.
1278

1279
    More details for parameters can be found in the RAPI documentation.
1280

1281
    @type node: string
1282
    @param node: Node name
1283
    @rtype: string
1284
    @return: job id
1285

1286
    """
1287
    return self._SendRequest(HTTP_POST,
1288
                             ("/%s/nodes/%s/modify" %
1289
                              (GANETI_RAPI_VERSION, node)), None, kwargs)
1290

    
1291
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
1292
    """Gets the storage units for a node.
1293

1294
    @type node: str
1295
    @param node: the node whose storage units to return
1296
    @type storage_type: str
1297
    @param storage_type: storage type whose units to return
1298
    @type output_fields: str
1299
    @param output_fields: storage type fields to return
1300

1301
    @rtype: string
1302
    @return: job id where results can be retrieved
1303

1304
    """
1305
    query = [
1306
      ("storage_type", storage_type),
1307
      ("output_fields", output_fields),
1308
      ]
1309

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

    
1314
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
1315
    """Modifies parameters of storage units on the node.
1316

1317
    @type node: str
1318
    @param node: node whose storage units to modify
1319
    @type storage_type: str
1320
    @param storage_type: storage type whose units to modify
1321
    @type name: str
1322
    @param name: name of the storage unit
1323
    @type allocatable: bool or None
1324
    @param allocatable: Whether to set the "allocatable" flag on the storage
1325
                        unit (None=no modification, True=set, False=unset)
1326

1327
    @rtype: string
1328
    @return: job id
1329

1330
    """
1331
    query = [
1332
      ("storage_type", storage_type),
1333
      ("name", name),
1334
      ]
1335

    
1336
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1337

    
1338
    return self._SendRequest(HTTP_PUT,
1339
                             ("/%s/nodes/%s/storage/modify" %
1340
                              (GANETI_RAPI_VERSION, node)), query, None)
1341

    
1342
  def RepairNodeStorageUnits(self, node, storage_type, name):
1343
    """Repairs a storage unit on the node.
1344

1345
    @type node: str
1346
    @param node: node whose storage units to repair
1347
    @type storage_type: str
1348
    @param storage_type: storage type to repair
1349
    @type name: str
1350
    @param name: name of the storage unit to repair
1351

1352
    @rtype: string
1353
    @return: job id
1354

1355
    """
1356
    query = [
1357
      ("storage_type", storage_type),
1358
      ("name", name),
1359
      ]
1360

    
1361
    return self._SendRequest(HTTP_PUT,
1362
                             ("/%s/nodes/%s/storage/repair" %
1363
                              (GANETI_RAPI_VERSION, node)), query, None)
1364

    
1365
  def GetNodeTags(self, node):
1366
    """Gets the tags for a node.
1367

1368
    @type node: str
1369
    @param node: node whose tags to return
1370

1371
    @rtype: list of str
1372
    @return: tags for the node
1373

1374
    """
1375
    return self._SendRequest(HTTP_GET,
1376
                             ("/%s/nodes/%s/tags" %
1377
                              (GANETI_RAPI_VERSION, node)), None, None)
1378

    
1379
  def AddNodeTags(self, node, tags, dry_run=False):
1380
    """Adds tags to a node.
1381

1382
    @type node: str
1383
    @param node: node to add tags to
1384
    @type tags: list of str
1385
    @param tags: tags to add to the node
1386
    @type dry_run: bool
1387
    @param dry_run: whether to perform a dry run
1388

1389
    @rtype: string
1390
    @return: job id
1391

1392
    """
1393
    query = [("tag", t) for t in tags]
1394
    _AppendDryRunIf(query, dry_run)
1395

    
1396
    return self._SendRequest(HTTP_PUT,
1397
                             ("/%s/nodes/%s/tags" %
1398
                              (GANETI_RAPI_VERSION, node)), query, tags)
1399

    
1400
  def DeleteNodeTags(self, node, tags, dry_run=False):
1401
    """Delete tags from a node.
1402

1403
    @type node: str
1404
    @param node: node to remove tags from
1405
    @type tags: list of str
1406
    @param tags: tags to remove from the node
1407
    @type dry_run: bool
1408
    @param dry_run: whether to perform a dry run
1409

1410
    @rtype: string
1411
    @return: job id
1412

1413
    """
1414
    query = [("tag", t) for t in tags]
1415
    _AppendDryRunIf(query, dry_run)
1416

    
1417
    return self._SendRequest(HTTP_DELETE,
1418
                             ("/%s/nodes/%s/tags" %
1419
                              (GANETI_RAPI_VERSION, node)), query, None)
1420

    
1421
  def GetNetworks(self, bulk=False):
1422
    """Gets all networks in the cluster.
1423

1424
    @type bulk: bool
1425
    @param bulk: whether to return all information about the networks
1426

1427
    @rtype: list of dict or str
1428
    @return: if bulk is true, a list of dictionaries with info about all
1429
        networks in the cluster, else a list of names of those networks
1430

1431
    """
1432
    query = []
1433
    _AppendIf(query, bulk, ("bulk", 1))
1434

    
1435
    networks = self._SendRequest(HTTP_GET, "/%s/networks" % GANETI_RAPI_VERSION,
1436
                               query, None)
1437
    if bulk:
1438
      return networks
1439
    else:
1440
      return [n["name"] for n in networks]
1441

    
1442
  def GetNetwork(self, network):
1443
    """Gets information about a network.
1444

1445
    @type group: str
1446
    @param group: name of the network whose info to return
1447

1448
    @rtype: dict
1449
    @return: info about the network
1450

1451
    """
1452
    return self._SendRequest(HTTP_GET,
1453
                             "/%s/networks/%s" % (GANETI_RAPI_VERSION, network),
1454
                             None, None)
1455

    
1456
  def CreateNetwork(self, network_name, network, gateway=None, network6=None,
1457
                    gateway6=None, mac_prefix=None,
1458
                    add_reserved_ips=None, tags=[],
1459
                    conflicts_check=False, dry_run=False):
1460
    """Creates a new network.
1461

1462
    @type name: str
1463
    @param name: the name of network to create
1464
    @type dry_run: bool
1465
    @param dry_run: whether to peform a dry run
1466

1467
    @rtype: string
1468
    @return: job id
1469

1470
    """
1471
    query = []
1472
    _AppendDryRunIf(query, dry_run)
1473

    
1474
    body = {
1475
      "network_name": network_name,
1476
      "gateway": gateway,
1477
      "network": network,
1478
      "gateway6": gateway6,
1479
      "network6": network6,
1480
      "mac_prefix": mac_prefix,
1481
      "add_reserved_ips": add_reserved_ips,
1482
      "conflicts_check": conflicts_check,
1483
      "tags": tags,
1484
      }
1485

    
1486
    return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
1487
                             query, body)
1488

    
1489
  def ConnectNetwork(self, network_name, group_name, mode, link,
1490
                     conflicts_check=False, depends=None, dry_run=False):
1491
    """Connects a Network to a NodeGroup with the given netparams
1492

1493
    """
1494
    body = {
1495
      "group_name": group_name,
1496
      "network_mode": mode,
1497
      "network_link": link,
1498
      "conflicts_check": conflicts_check,
1499
      }
1500

    
1501
    if depends:
1502
      body['depends'] = depends
1503

    
1504
    query = []
1505
    _AppendDryRunIf(query, dry_run)
1506

    
1507
    return self._SendRequest(HTTP_PUT,
1508
                             ("/%s/networks/%s/connect" %
1509
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1510

    
1511
  def DisconnectNetwork(self, network_name, group_name,
1512
                        depends=None, dry_run=False):
1513
    """Connects a Network to a NodeGroup with the given netparams
1514

1515
    """
1516
    body = {
1517
      "group_name": group_name
1518
      }
1519

    
1520
    if depends:
1521
      body['depends'] = depends
1522

    
1523
    query = []
1524
    _AppendDryRunIf(query, dry_run)
1525

    
1526
    return self._SendRequest(HTTP_PUT,
1527
                             ("/%s/networks/%s/disconnect" %
1528
                             (GANETI_RAPI_VERSION, network_name)), query, body)
1529

    
1530

    
1531
  def ModifyNetwork(self, network, **kwargs):
1532
    """Modifies a network.
1533

1534
    More details for parameters can be found in the RAPI documentation.
1535

1536
    @type network: string
1537
    @param network: Network name
1538
    @rtype: string
1539
    @return: job id
1540

1541
    """
1542
    return self._SendRequest(HTTP_PUT,
1543
                             ("/%s/networks/%s/modify" %
1544
                              (GANETI_RAPI_VERSION, network)), None, kwargs)
1545

    
1546
  def DeleteNetwork(self, network, depends=None, dry_run=False):
1547
    """Deletes a network.
1548

1549
    @type group: str
1550
    @param group: the network to delete
1551
    @type dry_run: bool
1552
    @param dry_run: whether to peform a dry run
1553

1554
    @rtype: string
1555
    @return: job id
1556

1557
    """
1558
    body = {}
1559
    if depends:
1560
      body['depends'] = depends
1561

    
1562
    query = []
1563
    _AppendDryRunIf(query, dry_run)
1564

    
1565
    return self._SendRequest(HTTP_DELETE,
1566
                             ("/%s/networks/%s" %
1567
                              (GANETI_RAPI_VERSION, network)), query, body)
1568

    
1569
  def GetNetworkTags(self, network):
1570
    """Gets tags for a network.
1571

1572
    @type network: string
1573
    @param network: Node group whose tags to return
1574

1575
    @rtype: list of strings
1576
    @return: tags for the network
1577

1578
    """
1579
    return self._SendRequest(HTTP_GET,
1580
                             ("/%s/networks/%s/tags" %
1581
                              (GANETI_RAPI_VERSION, network)), None, None)
1582

    
1583
  def AddNetworkTags(self, network, tags, dry_run=False):
1584
    """Adds tags to a network.
1585

1586
    @type network: str
1587
    @param network: network to add tags to
1588
    @type tags: list of string
1589
    @param tags: tags to add to the network
1590
    @type dry_run: bool
1591
    @param dry_run: whether to perform a dry run
1592

1593
    @rtype: string
1594
    @return: job id
1595

1596
    """
1597
    query = [("tag", t) for t in tags]
1598
    _AppendDryRunIf(query, dry_run)
1599

    
1600
    return self._SendRequest(HTTP_PUT,
1601
                             ("/%s/networks/%s/tags" %
1602
                              (GANETI_RAPI_VERSION, network)), query, None)
1603

    
1604
  def DeleteNetworkTags(self, network, tags, dry_run=False):
1605
    """Deletes tags from a network.
1606

1607
    @type network: str
1608
    @param network: network to delete tags from
1609
    @type tags: list of string
1610
    @param tags: tags to delete
1611
    @type dry_run: bool
1612
    @param dry_run: whether to perform a dry run
1613
    @rtype: string
1614
    @return: job id
1615

1616
    """
1617
    query = [("tag", t) for t in tags]
1618
    _AppendDryRunIf(query, dry_run)
1619

    
1620
    return self._SendRequest(HTTP_DELETE,
1621
                             ("/%s/networks/%s/tags" %
1622
                              (GANETI_RAPI_VERSION, network)), query, None)
1623

    
1624

    
1625
  def GetGroups(self, bulk=False):
1626
    """Gets all node groups in the cluster.
1627

1628
    @type bulk: bool
1629
    @param bulk: whether to return all information about the groups
1630

1631
    @rtype: list of dict or str
1632
    @return: if bulk is true, a list of dictionaries with info about all node
1633
        groups in the cluster, else a list of names of those node groups
1634

1635
    """
1636
    query = []
1637
    _AppendIf(query, bulk, ("bulk", 1))
1638

    
1639
    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
1640
                               query, None)
1641
    if bulk:
1642
      return groups
1643
    else:
1644
      return [g["name"] for g in groups]
1645

    
1646
  def GetGroup(self, group):
1647
    """Gets information about a node group.
1648

1649
    @type group: str
1650
    @param group: name of the node group whose info to return
1651

1652
    @rtype: dict
1653
    @return: info about the node group
1654

1655
    """
1656
    return self._SendRequest(HTTP_GET,
1657
                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
1658
                             None, None)
1659

    
1660
  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
1661
    """Creates a new node group.
1662

1663
    @type name: str
1664
    @param name: the name of node group to create
1665
    @type alloc_policy: str
1666
    @param alloc_policy: the desired allocation policy for the group, if any
1667
    @type dry_run: bool
1668
    @param dry_run: whether to peform a dry run
1669

1670
    @rtype: string
1671
    @return: job id
1672

1673
    """
1674
    query = []
1675
    _AppendDryRunIf(query, dry_run)
1676

    
1677
    body = {
1678
      "name": name,
1679
      "alloc_policy": alloc_policy
1680
      }
1681

    
1682
    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
1683
                             query, body)
1684

    
1685
  def ModifyGroup(self, group, **kwargs):
1686
    """Modifies a node group.
1687

1688
    More details for parameters can be found in the RAPI documentation.
1689

1690
    @type group: string
1691
    @param group: Node group name
1692
    @rtype: string
1693
    @return: job id
1694

1695
    """
1696
    return self._SendRequest(HTTP_PUT,
1697
                             ("/%s/groups/%s/modify" %
1698
                              (GANETI_RAPI_VERSION, group)), None, kwargs)
1699

    
1700
  def DeleteGroup(self, group, dry_run=False):
1701
    """Deletes a node group.
1702

1703
    @type group: str
1704
    @param group: the node group to delete
1705
    @type dry_run: bool
1706
    @param dry_run: whether to peform a dry run
1707

1708
    @rtype: string
1709
    @return: job id
1710

1711
    """
1712
    query = []
1713
    _AppendDryRunIf(query, dry_run)
1714

    
1715
    return self._SendRequest(HTTP_DELETE,
1716
                             ("/%s/groups/%s" %
1717
                              (GANETI_RAPI_VERSION, group)), query, None)
1718

    
1719
  def RenameGroup(self, group, new_name):
1720
    """Changes the name of a node group.
1721

1722
    @type group: string
1723
    @param group: Node group name
1724
    @type new_name: string
1725
    @param new_name: New node group name
1726

1727
    @rtype: string
1728
    @return: job id
1729

1730
    """
1731
    body = {
1732
      "new_name": new_name,
1733
      }
1734

    
1735
    return self._SendRequest(HTTP_PUT,
1736
                             ("/%s/groups/%s/rename" %
1737
                              (GANETI_RAPI_VERSION, group)), None, body)
1738

    
1739
  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
1740
    """Assigns nodes to a group.
1741

1742
    @type group: string
1743
    @param group: Node gropu name
1744
    @type nodes: list of strings
1745
    @param nodes: List of nodes to assign to the group
1746

1747
    @rtype: string
1748
    @return: job id
1749

1750
    """
1751
    query = []
1752
    _AppendForceIf(query, force)
1753
    _AppendDryRunIf(query, dry_run)
1754

    
1755
    body = {
1756
      "nodes": nodes,
1757
      }
1758

    
1759
    return self._SendRequest(HTTP_PUT,
1760
                             ("/%s/groups/%s/assign-nodes" %
1761
                             (GANETI_RAPI_VERSION, group)), query, body)
1762

    
1763
  def GetGroupTags(self, group):
1764
    """Gets tags for a node group.
1765

1766
    @type group: string
1767
    @param group: Node group whose tags to return
1768

1769
    @rtype: list of strings
1770
    @return: tags for the group
1771

1772
    """
1773
    return self._SendRequest(HTTP_GET,
1774
                             ("/%s/groups/%s/tags" %
1775
                              (GANETI_RAPI_VERSION, group)), None, None)
1776

    
1777
  def AddGroupTags(self, group, tags, dry_run=False):
1778
    """Adds tags to a node group.
1779

1780
    @type group: str
1781
    @param group: group to add tags to
1782
    @type tags: list of string
1783
    @param tags: tags to add to the group
1784
    @type dry_run: bool
1785
    @param dry_run: whether to perform a dry run
1786

1787
    @rtype: string
1788
    @return: job id
1789

1790
    """
1791
    query = [("tag", t) for t in tags]
1792
    _AppendDryRunIf(query, dry_run)
1793

    
1794
    return self._SendRequest(HTTP_PUT,
1795
                             ("/%s/groups/%s/tags" %
1796
                              (GANETI_RAPI_VERSION, group)), query, None)
1797

    
1798
  def DeleteGroupTags(self, group, tags, dry_run=False):
1799
    """Deletes tags from a node group.
1800

1801
    @type group: str
1802
    @param group: group to delete tags from
1803
    @type tags: list of string
1804
    @param tags: tags to delete
1805
    @type dry_run: bool
1806
    @param dry_run: whether to perform a dry run
1807
    @rtype: string
1808
    @return: job id
1809

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

    
1814
    return self._SendRequest(HTTP_DELETE,
1815
                             ("/%s/groups/%s/tags" %
1816
                              (GANETI_RAPI_VERSION, group)), query, None)
1817

    
1818
  def Query(self, what, fields, qfilter=None):
1819
    """Retrieves information about resources.
1820

1821
    @type what: string
1822
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1823
    @type fields: list of string
1824
    @param fields: Requested fields
1825
    @type qfilter: None or list
1826
    @param qfilter: Query filter
1827

1828
    @rtype: string
1829
    @return: job id
1830

1831
    """
1832
    body = {
1833
      "fields": fields,
1834
      }
1835

    
1836
    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
1837
    # TODO: remove "filter" after 2.7
1838
    _SetItemIf(body, qfilter is not None, "filter", qfilter)
1839

    
1840
    return self._SendRequest(HTTP_PUT,
1841
                             ("/%s/query/%s" %
1842
                              (GANETI_RAPI_VERSION, what)), None, body)
1843

    
1844
  def QueryFields(self, what, fields=None):
1845
    """Retrieves available fields for a resource.
1846

1847
    @type what: string
1848
    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
1849
    @type fields: list of string
1850
    @param fields: Requested fields
1851

1852
    @rtype: string
1853
    @return: job id
1854

1855
    """
1856
    query = []
1857

    
1858
    if fields is not None:
1859
      _AppendIf(query, True, ("fields", ",".join(fields)))
1860

    
1861
    return self._SendRequest(HTTP_GET,
1862
                             ("/%s/query/%s/fields" %
1863
                              (GANETI_RAPI_VERSION, what)), query, None)