Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / rapi.py @ 11d4d283

History | View | Annotate | Download (53.4 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
                       reason=None):
551
    """Replaces disks on an instance.
552

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

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

561
    """
562

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

    
567
    query = []
568
    _AppendIf(query, reason, ("reason", reason))
569
    _AppendDryRunIf(query, dry_run)
570

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

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

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

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

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

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

599
    More details for parameters can be found in the RAPI documentation.
600

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

612
    """
613
    body = {
614
      "amount": amount,
615
      }
616

    
617
    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
618

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

    
624
  def GetInstanceTags(self, instance):
625
    """Gets tags for an instance.
626

627
    @type instance: str
628
    @param instance: instance whose tags to return
629

630
    @rtype: list of str
631
    @return: tags for the instance
632

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

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

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

648
    @rtype: string
649
    @return: job id
650

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

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

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

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

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

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

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

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

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

    
702
    body = None
703
    if shutdown_timeout is not None:
704
        body = {"shutdown_timeout": shutdown_timeout}
705

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

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

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

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

    
731

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

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

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

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

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

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

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

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

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

    
787
    query = []
788
    _AppendIf(query, os, ("os", os))
789
    _AppendIf(query, no_startup, ("nostartup", 1))
790

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

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

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

812
    @rtype: string
813
    @return: job id
814

815
    """
816
    query = [
817
      ("mode", mode),
818
      ]
819

    
820
    # TODO: Convert to body parameters
821

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

    
826
    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
827
    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
828

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

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

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

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

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

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

861
    """
862
    body = {
863
      "destination": destination,
864
      "mode": mode,
865
      }
866

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

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

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

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

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

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

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

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

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

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

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

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

940
    """
941
    body = {
942
      "new_name": new_name,
943
      }
944

    
945
    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
946
    _SetItemIf(body, name_check is not None, "name_check", name_check)
947

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

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

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

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

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

968
    @rtype: list of int
969
    @return: job ids for the cluster
970

971
    """
972
    query = []
973
    _AppendIf(query, bulk, ("bulk", 1))
974

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

    
982

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

986
    @type job_id: string
987
    @param job_id: job id whose status to query
988

989
    @rtype: dict
990
    @return: job status
991

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

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

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

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

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

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

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

    
1026
      if period:
1027
        time.sleep(period)
1028

    
1029
      if retries > 0:
1030
        retries -= 1
1031

    
1032
    return False
1033

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

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

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

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

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

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

1064
    """
1065
    query = []
1066
    _AppendDryRunIf(query, dry_run)
1067

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

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

1075
    @type bulk: bool
1076
    @param bulk: whether to return all information about all instances
1077

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

1082
    """
1083
    query = []
1084
    _AppendIf(query, bulk, ("bulk", 1))
1085

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

    
1093
  def GetNode(self, node):
1094
    """Gets information about a node.
1095

1096
    @type node: str
1097
    @param node: node whose info to return
1098

1099
    @rtype: dict
1100
    @return: info about the node
1101

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

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

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

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

1134
    @raises GanetiApiError: if an iallocator and remote_node are both
1135
        specified
1136

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

    
1141
    query = []
1142
    _AppendDryRunIf(query, dry_run)
1143

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

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

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

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

    
1166
      _AppendIf(query, iallocator, ("iallocator", iallocator))
1167
      _AppendIf(query, remote_node, ("remote_node", remote_node))
1168
      _AppendIf(query, early_release, ("early_release", 1))
1169

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

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

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

1190
    @rtype: string
1191
    @return: job id
1192

1193
    """
1194
    query = []
1195
    _AppendDryRunIf(query, dry_run)
1196

    
1197
    if _NODE_MIGRATE_REQV1 in self.GetFeatures():
1198
      body = {}
1199

    
1200
      _SetItemIf(body, mode is not None, "mode", mode)
1201
      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
1202
      _SetItemIf(body, target_node is not None, "target_node", target_node)
1203

    
1204
      assert len(query) <= 1
1205

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

    
1215
      _AppendIf(query, mode is not None, ("mode", mode))
1216

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

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

1224
    @type node: str
1225
    @param node: node whose role to return
1226

1227
    @rtype: str
1228
    @return: the current role for a node
1229

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

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

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

1248
    @rtype: string
1249
    @return: job id
1250

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

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

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

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

1270
    """
1271
    query = []
1272
    _AppendForceIf(query, force)
1273

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

    
1278
  def ModifyNode(self, node, **kwargs):
1279
    """Modifies a node.
1280

1281
    More details for parameters can be found in the RAPI documentation.
1282

1283
    @type node: string
1284
    @param node: Node name
1285
    @rtype: string
1286
    @return: job id
1287

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

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

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

1303
    @rtype: string
1304
    @return: job id where results can be retrieved
1305

1306
    """
1307
    query = [
1308
      ("storage_type", storage_type),
1309
      ("output_fields", output_fields),
1310
      ]
1311

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

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

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

1329
    @rtype: string
1330
    @return: job id
1331

1332
    """
1333
    query = [
1334
      ("storage_type", storage_type),
1335
      ("name", name),
1336
      ]
1337

    
1338
    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
1339

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

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

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

1354
    @rtype: string
1355
    @return: job id
1356

1357
    """
1358
    query = [
1359
      ("storage_type", storage_type),
1360
      ("name", name),
1361
      ]
1362

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

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

1370
    @type node: str
1371
    @param node: node whose tags to return
1372

1373
    @rtype: list of str
1374
    @return: tags for the node
1375

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

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

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

1391
    @rtype: string
1392
    @return: job id
1393

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

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

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

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

1412
    @rtype: string
1413
    @return: job id
1414

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

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

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

1426
    @type bulk: bool
1427
    @param bulk: whether to return all information about the networks
1428

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

1433
    """
1434
    query = []
1435
    _AppendIf(query, bulk, ("bulk", 1))
1436

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

    
1444
  def GetNetwork(self, network):
1445
    """Gets information about a network.
1446

1447
    @type group: str
1448
    @param group: name of the network whose info to return
1449

1450
    @rtype: dict
1451
    @return: info about the network
1452

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

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

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

1469
    @rtype: string
1470
    @return: job id
1471

1472
    """
1473
    query = []
1474
    _AppendDryRunIf(query, dry_run)
1475

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

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

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

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

    
1503
    if depends:
1504
      body['depends'] = depends
1505

    
1506
    query = []
1507
    _AppendDryRunIf(query, dry_run)
1508

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

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

1517
    """
1518
    body = {
1519
      "group_name": group_name
1520
      }
1521

    
1522
    if depends:
1523
      body['depends'] = depends
1524

    
1525
    query = []
1526
    _AppendDryRunIf(query, dry_run)
1527

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

    
1532

    
1533
  def ModifyNetwork(self, network, **kwargs):
1534
    """Modifies a network.
1535

1536
    More details for parameters can be found in the RAPI documentation.
1537

1538
    @type network: string
1539
    @param network: Network name
1540
    @rtype: string
1541
    @return: job id
1542

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

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

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

1556
    @rtype: string
1557
    @return: job id
1558

1559
    """
1560
    body = {}
1561
    if depends:
1562
      body['depends'] = depends
1563

    
1564
    query = []
1565
    _AppendDryRunIf(query, dry_run)
1566

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

    
1571
  def GetNetworkTags(self, network):
1572
    """Gets tags for a network.
1573

1574
    @type network: string
1575
    @param network: Node group whose tags to return
1576

1577
    @rtype: list of strings
1578
    @return: tags for the network
1579

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

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

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

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

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

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

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

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

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

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

    
1626

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

1630
    @type bulk: bool
1631
    @param bulk: whether to return all information about the groups
1632

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

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

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

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

1651
    @type group: str
1652
    @param group: name of the node group whose info to return
1653

1654
    @rtype: dict
1655
    @return: info about the node group
1656

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

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

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

1672
    @rtype: string
1673
    @return: job id
1674

1675
    """
1676
    query = []
1677
    _AppendDryRunIf(query, dry_run)
1678

    
1679
    body = {
1680
      "name": name,
1681
      "alloc_policy": alloc_policy
1682
      }
1683

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

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

1690
    More details for parameters can be found in the RAPI documentation.
1691

1692
    @type group: string
1693
    @param group: Node group name
1694
    @rtype: string
1695
    @return: job id
1696

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

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

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

1710
    @rtype: string
1711
    @return: job id
1712

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

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

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

1724
    @type group: string
1725
    @param group: Node group name
1726
    @type new_name: string
1727
    @param new_name: New node group name
1728

1729
    @rtype: string
1730
    @return: job id
1731

1732
    """
1733
    body = {
1734
      "new_name": new_name,
1735
      }
1736

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

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

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

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

1752
    """
1753
    query = []
1754
    _AppendForceIf(query, force)
1755
    _AppendDryRunIf(query, dry_run)
1756

    
1757
    body = {
1758
      "nodes": nodes,
1759
      }
1760

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

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

1768
    @type group: string
1769
    @param group: Node group whose tags to return
1770

1771
    @rtype: list of strings
1772
    @return: tags for the group
1773

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

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

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

1789
    @rtype: string
1790
    @return: job id
1791

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

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

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

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

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

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

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

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

1830
    @rtype: string
1831
    @return: job id
1832

1833
    """
1834
    body = {
1835
      "fields": fields,
1836
      }
1837

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

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

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

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

1854
    @rtype: string
1855
    @return: job id
1856

1857
    """
1858
    query = []
1859

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

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