Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 5349519d

History | View | Annotate | Download (39.9 kB)

1
#
2
#
3

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

    
21

    
22
"""Remote API resource implementations.
23

24
PUT or POST?
25
============
26

27
According to RFC2616 the main difference between PUT and POST is that
28
POST can create new resources but PUT can only create the resource the
29
URI was pointing to on the PUT request.
30

31
In the context of this module POST on ``/2/instances`` to change an existing
32
entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
33
new instance) with a name specified in the request.
34

35
Quoting from RFC2616, section 9.6::
36

37
  The fundamental difference between the POST and PUT requests is reflected in
38
  the different meaning of the Request-URI. The URI in a POST request
39
  identifies the resource that will handle the enclosed entity. That resource
40
  might be a data-accepting process, a gateway to some other protocol, or a
41
  separate entity that accepts annotations. In contrast, the URI in a PUT
42
  request identifies the entity enclosed with the request -- the user agent
43
  knows what URI is intended and the server MUST NOT attempt to apply the
44
  request to some other resource. If the server desires that the request be
45
  applied to a different URI, it MUST send a 301 (Moved Permanently) response;
46
  the user agent MAY then make its own decision regarding whether or not to
47
  redirect the request.
48

49
So when adding new methods, if they are operating on the URI entity itself,
50
PUT should be prefered over POST.
51

52
"""
53

    
54
# pylint: disable=C0103
55

    
56
# C0103: Invalid name, since the R_* names are not conforming
57

    
58
from ganeti import opcodes
59
from ganeti import objects
60
from ganeti import http
61
from ganeti import constants
62
from ganeti import cli
63
from ganeti import rapi
64
from ganeti import ht
65
from ganeti import compat
66
from ganeti import ssconf
67
from ganeti.rapi import baserlib
68

    
69

    
70
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
71
I_FIELDS = ["name", "admin_state", "os",
72
            "pnode", "snodes",
73
            "disk_template",
74
            "nic.ips", "nic.macs", "nic.modes", "nic.uuids", "nic.names",
75
            "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
76
            "network_port",
77
            "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
78
            "disk.names",
79
            "beparams", "hvparams",
80
            "oper_state", "oper_ram", "oper_vcpus", "status",
81
            "custom_hvparams", "custom_beparams", "custom_nicparams",
82
            ] + _COMMON_FIELDS
83

    
84
N_FIELDS = ["name", "offline", "master_candidate", "drained",
85
            "dtotal", "dfree", "sptotal", "spfree",
86
            "mtotal", "mnode", "mfree",
87
            "pinst_cnt", "sinst_cnt",
88
            "ctotal", "cnos", "cnodes", "csockets",
89
            "pip", "sip", "role",
90
            "pinst_list", "sinst_list",
91
            "master_capable", "vm_capable",
92
            "ndparams",
93
            "group.uuid",
94
            ] + _COMMON_FIELDS
95

    
96
NET_FIELDS = ["name", "network", "gateway",
97
              "network6", "gateway6",
98
              "mac_prefix",
99
              "free_count", "reserved_count",
100
              "map", "group_list", "inst_list",
101
              "external_reservations",
102
              ] + _COMMON_FIELDS
103

    
104
G_FIELDS = [
105
  "alloc_policy",
106
  "name",
107
  "node_cnt",
108
  "node_list",
109
  "ipolicy",
110
  "custom_ipolicy",
111
  "diskparams",
112
  "custom_diskparams",
113
  "ndparams",
114
  "custom_ndparams",
115
  ] + _COMMON_FIELDS
116

    
117
J_FIELDS_BULK = [
118
  "id", "ops", "status", "summary",
119
  "opstatus",
120
  "received_ts", "start_ts", "end_ts",
121
  ]
122

    
123
J_FIELDS = J_FIELDS_BULK + [
124
  "oplog",
125
  "opresult",
126
  ]
127

    
128
_NR_DRAINED = "drained"
129
_NR_MASTER_CANDIDATE = "master-candidate"
130
_NR_MASTER = "master"
131
_NR_OFFLINE = "offline"
132
_NR_REGULAR = "regular"
133

    
134
_NR_MAP = {
135
  constants.NR_MASTER: _NR_MASTER,
136
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
137
  constants.NR_DRAINED: _NR_DRAINED,
138
  constants.NR_OFFLINE: _NR_OFFLINE,
139
  constants.NR_REGULAR: _NR_REGULAR,
140
  }
141

    
142
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
143

    
144
# Request data version field
145
_REQ_DATA_VERSION = "__version__"
146

    
147
# Feature string for instance creation request data version 1
148
_INST_CREATE_REQV1 = "instance-create-reqv1"
149

    
150
# Feature string for instance reinstall request version 1
151
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
152

    
153
# Feature string for node migration version 1
154
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
155

    
156
# Feature string for node evacuation with LU-generated jobs
157
_NODE_EVAC_RES1 = "node-evac-res1"
158

    
159
ALL_FEATURES = compat.UniqueFrozenset([
160
  _INST_CREATE_REQV1,
161
  _INST_REINSTALL_REQV1,
162
  _NODE_MIGRATE_REQV1,
163
  _NODE_EVAC_RES1,
164
  ])
165

    
166
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
167
_WFJC_TIMEOUT = 10
168

    
169

    
170
# FIXME: For compatibility we update the beparams/memory field. Needs to be
171
#        removed in Ganeti 2.8
172
def _UpdateBeparams(inst):
173
  """Updates the beparams dict of inst to support the memory field.
174

175
  @param inst: Inst dict
176
  @return: Updated inst dict
177

178
  """
179
  beparams = inst["beparams"]
180
  beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
181

    
182
  return inst
183

    
184

    
185
class R_root(baserlib.ResourceBase):
186
  """/ resource.
187

188
  """
189
  @staticmethod
190
  def GET():
191
    """Supported for legacy reasons.
192

193
    """
194
    return None
195

    
196

    
197
class R_2(R_root):
198
  """/2 resource.
199

200
  """
201

    
202

    
203
class R_version(baserlib.ResourceBase):
204
  """/version resource.
205

206
  This resource should be used to determine the remote API version and
207
  to adapt clients accordingly.
208

209
  """
210
  @staticmethod
211
  def GET():
212
    """Returns the remote API version.
213

214
    """
215
    return constants.RAPI_VERSION
216

    
217

    
218
class R_2_info(baserlib.OpcodeResource):
219
  """/2/info resource.
220

221
  """
222
  GET_OPCODE = opcodes.OpClusterQuery
223

    
224
  def GET(self):
225
    """Returns cluster information.
226

227
    """
228
    client = self.GetClient()
229
    return client.QueryClusterInfo()
230

    
231

    
232
class R_2_features(baserlib.ResourceBase):
233
  """/2/features resource.
234

235
  """
236
  @staticmethod
237
  def GET():
238
    """Returns list of optional RAPI features implemented.
239

240
    """
241
    return list(ALL_FEATURES)
242

    
243

    
244
class R_2_os(baserlib.OpcodeResource):
245
  """/2/os resource.
246

247
  """
248
  GET_OPCODE = opcodes.OpOsDiagnose
249

    
250
  def GET(self):
251
    """Return a list of all OSes.
252

253
    Can return error 500 in case of a problem.
254

255
    Example: ["debian-etch"]
256

257
    """
258
    cl = self.GetClient()
259
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
260
    job_id = self.SubmitJob([op], cl=cl)
261
    # we use custom feedback function, instead of print we log the status
262
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
263
    diagnose_data = result[0]
264

    
265
    if not isinstance(diagnose_data, list):
266
      raise http.HttpBadGateway(message="Can't get OS list")
267

    
268
    os_names = []
269
    for (name, variants) in diagnose_data:
270
      os_names.extend(cli.CalculateOSNames(name, variants))
271

    
272
    return os_names
273

    
274

    
275
class R_2_redist_config(baserlib.OpcodeResource):
276
  """/2/redistribute-config resource.
277

278
  """
279
  PUT_OPCODE = opcodes.OpClusterRedistConf
280

    
281

    
282
class R_2_cluster_modify(baserlib.OpcodeResource):
283
  """/2/modify resource.
284

285
  """
286
  PUT_OPCODE = opcodes.OpClusterSetParams
287

    
288

    
289
class R_2_jobs(baserlib.ResourceBase):
290
  """/2/jobs resource.
291

292
  """
293
  def GET(self):
294
    """Returns a dictionary of jobs.
295

296
    @return: a dictionary with jobs id and uri.
297

298
    """
299
    client = self.GetClient()
300

    
301
    if self.useBulk():
302
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
303
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
304
    else:
305
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
306
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
307
                                   uri_fields=("id", "uri"))
308

    
309

    
310
class R_2_jobs_id(baserlib.ResourceBase):
311
  """/2/jobs/[job_id] resource.
312

313
  """
314
  def GET(self):
315
    """Returns a job status.
316

317
    @return: a dictionary with job parameters.
318
        The result includes:
319
            - id: job ID as a number
320
            - status: current job status as a string
321
            - ops: involved OpCodes as a list of dictionaries for each
322
              opcodes in the job
323
            - opstatus: OpCodes status as a list
324
            - opresult: OpCodes results as a list of lists
325

326
    """
327
    job_id = self.items[0]
328
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
329
    if result is None:
330
      raise http.HttpNotFound()
331
    return baserlib.MapFields(J_FIELDS, result)
332

    
333
  def DELETE(self):
334
    """Cancel not-yet-started job.
335

336
    """
337
    job_id = self.items[0]
338
    result = self.GetClient().CancelJob(job_id)
339
    return result
340

    
341

    
342
class R_2_jobs_id_wait(baserlib.ResourceBase):
343
  """/2/jobs/[job_id]/wait resource.
344

345
  """
346
  # WaitForJobChange provides access to sensitive information and blocks
347
  # machine resources (it's a blocking RAPI call), hence restricting access.
348
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
349

    
350
  def GET(self):
351
    """Waits for job changes.
352

353
    """
354
    job_id = self.items[0]
355

    
356
    fields = self.getBodyParameter("fields")
357
    prev_job_info = self.getBodyParameter("previous_job_info", None)
358
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
359

    
360
    if not isinstance(fields, list):
361
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
362

    
363
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
364
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
365
                                " be a list")
366

    
367
    if not (prev_log_serial is None or
368
            isinstance(prev_log_serial, (int, long))):
369
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
370
                                " be a number")
371

    
372
    client = self.GetClient()
373
    result = client.WaitForJobChangeOnce(job_id, fields,
374
                                         prev_job_info, prev_log_serial,
375
                                         timeout=_WFJC_TIMEOUT)
376
    if not result:
377
      raise http.HttpNotFound()
378

    
379
    if result == constants.JOB_NOTCHANGED:
380
      # No changes
381
      return None
382

    
383
    (job_info, log_entries) = result
384

    
385
    return {
386
      "job_info": job_info,
387
      "log_entries": log_entries,
388
      }
389

    
390

    
391
class R_2_nodes(baserlib.OpcodeResource):
392
  """/2/nodes resource.
393

394
  """
395

    
396
  def GET(self):
397
    """Returns a list of all nodes.
398

399
    """
400
    client = self.GetClient()
401

    
402
    if self.useBulk():
403
      bulkdata = client.QueryNodes([], N_FIELDS, False)
404
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
405
    else:
406
      nodesdata = client.QueryNodes([], ["name"], False)
407
      nodeslist = [row[0] for row in nodesdata]
408
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
409
                                   uri_fields=("id", "uri"))
410

    
411

    
412
class R_2_nodes_name(baserlib.OpcodeResource):
413
  """/2/nodes/[node_name] resource.
414

415
  """
416

    
417
  def GET(self):
418
    """Send information about a node.
419

420
    """
421
    node_name = self.items[0]
422
    client = self.GetClient()
423

    
424
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
425
                                            names=[node_name], fields=N_FIELDS,
426
                                            use_locking=self.useLocking())
427

    
428
    return baserlib.MapFields(N_FIELDS, result[0])
429

    
430

    
431
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
432
  """/2/nodes/[node_name]/powercycle resource.
433

434
  """
435
  POST_OPCODE = opcodes.OpNodePowercycle
436

    
437
  def GetPostOpInput(self):
438
    """Tries to powercycle a node.
439

440
    """
441
    return (self.request_body, {
442
      "node_name": self.items[0],
443
      "force": self.useForce(),
444
      })
445

    
446

    
447
class R_2_nodes_name_role(baserlib.OpcodeResource):
448
  """/2/nodes/[node_name]/role resource.
449

450
  """
451
  PUT_OPCODE = opcodes.OpNodeSetParams
452

    
453
  def GET(self):
454
    """Returns the current node role.
455

456
    @return: Node role
457

458
    """
459
    node_name = self.items[0]
460
    client = self.GetClient()
461
    result = client.QueryNodes(names=[node_name], fields=["role"],
462
                               use_locking=self.useLocking())
463

    
464
    return _NR_MAP[result[0][0]]
465

    
466
  def GetPutOpInput(self):
467
    """Sets the node role.
468

469
    """
470
    baserlib.CheckType(self.request_body, basestring, "Body contents")
471

    
472
    role = self.request_body
473

    
474
    if role == _NR_REGULAR:
475
      candidate = False
476
      offline = False
477
      drained = False
478

    
479
    elif role == _NR_MASTER_CANDIDATE:
480
      candidate = True
481
      offline = drained = None
482

    
483
    elif role == _NR_DRAINED:
484
      drained = True
485
      candidate = offline = None
486

    
487
    elif role == _NR_OFFLINE:
488
      offline = True
489
      candidate = drained = None
490

    
491
    else:
492
      raise http.HttpBadRequest("Can't set '%s' role" % role)
493

    
494
    assert len(self.items) == 1
495

    
496
    return ({}, {
497
      "node_name": self.items[0],
498
      "master_candidate": candidate,
499
      "offline": offline,
500
      "drained": drained,
501
      "force": self.useForce(),
502
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
503
      })
504

    
505

    
506
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
507
  """/2/nodes/[node_name]/evacuate resource.
508

509
  """
510
  POST_OPCODE = opcodes.OpNodeEvacuate
511

    
512
  def GetPostOpInput(self):
513
    """Evacuate all instances off a node.
514

515
    """
516
    return (self.request_body, {
517
      "node_name": self.items[0],
518
      "dry_run": self.dryRun(),
519
      })
520

    
521

    
522
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
523
  """/2/nodes/[node_name]/migrate resource.
524

525
  """
526
  POST_OPCODE = opcodes.OpNodeMigrate
527

    
528
  def GetPostOpInput(self):
529
    """Migrate all primary instances from a node.
530

531
    """
532
    if self.queryargs:
533
      # Support old-style requests
534
      if "live" in self.queryargs and "mode" in self.queryargs:
535
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
536
                                  " be passed")
537

    
538
      if "live" in self.queryargs:
539
        if self._checkIntVariable("live", default=1):
540
          mode = constants.HT_MIGRATION_LIVE
541
        else:
542
          mode = constants.HT_MIGRATION_NONLIVE
543
      else:
544
        mode = self._checkStringVariable("mode", default=None)
545

    
546
      data = {
547
        "mode": mode,
548
        }
549
    else:
550
      data = self.request_body
551

    
552
    return (data, {
553
      "node_name": self.items[0],
554
      })
555

    
556

    
557
class R_2_nodes_name_modify(baserlib.OpcodeResource):
558
  """/2/nodes/[node_name]/modify resource.
559

560
  """
561
  POST_OPCODE = opcodes.OpNodeSetParams
562

    
563
  def GetPostOpInput(self):
564
    """Changes parameters of a node.
565

566
    """
567
    assert len(self.items) == 1
568

    
569
    return (self.request_body, {
570
      "node_name": self.items[0],
571
      })
572

    
573

    
574
class R_2_nodes_name_storage(baserlib.OpcodeResource):
575
  """/2/nodes/[node_name]/storage resource.
576

577
  """
578
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
579
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
580
  GET_OPCODE = opcodes.OpNodeQueryStorage
581

    
582
  def GetGetOpInput(self):
583
    """List storage available on a node.
584

585
    """
586
    storage_type = self._checkStringVariable("storage_type", None)
587
    output_fields = self._checkStringVariable("output_fields", None)
588

    
589
    if not output_fields:
590
      raise http.HttpBadRequest("Missing the required 'output_fields'"
591
                                " parameter")
592

    
593
    return ({}, {
594
      "nodes": [self.items[0]],
595
      "storage_type": storage_type,
596
      "output_fields": output_fields.split(","),
597
      })
598

    
599

    
600
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
601
  """/2/nodes/[node_name]/storage/modify resource.
602

603
  """
604
  PUT_OPCODE = opcodes.OpNodeModifyStorage
605

    
606
  def GetPutOpInput(self):
607
    """Modifies a storage volume on a node.
608

609
    """
610
    storage_type = self._checkStringVariable("storage_type", None)
611
    name = self._checkStringVariable("name", None)
612

    
613
    if not name:
614
      raise http.HttpBadRequest("Missing the required 'name'"
615
                                " parameter")
616

    
617
    changes = {}
618

    
619
    if "allocatable" in self.queryargs:
620
      changes[constants.SF_ALLOCATABLE] = \
621
        bool(self._checkIntVariable("allocatable", default=1))
622

    
623
    return ({}, {
624
      "node_name": self.items[0],
625
      "storage_type": storage_type,
626
      "name": name,
627
      "changes": changes,
628
      })
629

    
630

    
631
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
632
  """/2/nodes/[node_name]/storage/repair resource.
633

634
  """
635
  PUT_OPCODE = opcodes.OpRepairNodeStorage
636

    
637
  def GetPutOpInput(self):
638
    """Repairs a storage volume on a node.
639

640
    """
641
    storage_type = self._checkStringVariable("storage_type", None)
642
    name = self._checkStringVariable("name", None)
643
    if not name:
644
      raise http.HttpBadRequest("Missing the required 'name'"
645
                                " parameter")
646

    
647
    return ({}, {
648
      "node_name": self.items[0],
649
      "storage_type": storage_type,
650
      "name": name,
651
      })
652

    
653

    
654
class R_2_networks(baserlib.OpcodeResource):
655
  """/2/networks resource.
656

657
  """
658
  POST_OPCODE = opcodes.OpNetworkAdd
659
  POST_RENAME = {
660
    "name": "network_name",
661
    }
662

    
663
  def GetPostOpInput(self):
664
    """Create a network.
665

666
    """
667
    assert not self.items
668
    return (self.request_body, {
669
      "dry_run": self.dryRun(),
670
      })
671

    
672
  def GET(self):
673
    """Returns a list of all networks.
674

675
    """
676
    client = self.GetClient()
677

    
678
    if self.useBulk():
679
      bulkdata = client.QueryNetworks([], NET_FIELDS, False)
680
      return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
681
    else:
682
      data = client.QueryNetworks([], ["name"], False)
683
      networknames = [row[0] for row in data]
684
      return baserlib.BuildUriList(networknames, "/2/networks/%s",
685
                                   uri_fields=("name", "uri"))
686

    
687

    
688
class R_2_networks_name(baserlib.OpcodeResource):
689
  """/2/networks/[network_name] resource.
690

691
  """
692
  DELETE_OPCODE = opcodes.OpNetworkRemove
693

    
694
  def GET(self):
695
    """Send information about a network.
696

697
    """
698
    network_name = self.items[0]
699
    client = self.GetClient()
700

    
701
    result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
702
                                            names=[network_name],
703
                                            fields=NET_FIELDS,
704
                                            use_locking=self.useLocking())
705

    
706
    return baserlib.MapFields(NET_FIELDS, result[0])
707

    
708
  def GetDeleteOpInput(self):
709
    """Delete a network.
710

711
    """
712
    assert len(self.items) == 1
713
    return (self.request_body, {
714
      "network_name": self.items[0],
715
      "dry_run": self.dryRun(),
716
      })
717

    
718

    
719
class R_2_networks_name_connect(baserlib.OpcodeResource):
720
  """/2/networks/[network_name]/connect resource.
721

722
  """
723
  PUT_OPCODE = opcodes.OpNetworkConnect
724

    
725
  def GetPutOpInput(self):
726
    """Changes some parameters of node group.
727

728
    """
729
    assert self.items
730
    return (self.request_body, {
731
      "network_name": self.items[0],
732
      "dry_run": self.dryRun(),
733
      })
734

    
735

    
736
class R_2_networks_name_disconnect(baserlib.OpcodeResource):
737
  """/2/networks/[network_name]/disconnect resource.
738

739
  """
740
  PUT_OPCODE = opcodes.OpNetworkDisconnect
741

    
742
  def GetPutOpInput(self):
743
    """Changes some parameters of node group.
744

745
    """
746
    assert self.items
747
    return (self.request_body, {
748
      "network_name": self.items[0],
749
      "dry_run": self.dryRun(),
750
      })
751

    
752

    
753
class R_2_networks_name_modify(baserlib.OpcodeResource):
754
  """/2/networks/[network_name]/modify resource.
755

756
  """
757
  PUT_OPCODE = opcodes.OpNetworkSetParams
758

    
759
  def GetPutOpInput(self):
760
    """Changes some parameters of network.
761

762
    """
763
    assert self.items
764
    return (self.request_body, {
765
      "network_name": self.items[0],
766
      })
767

    
768

    
769
class R_2_groups(baserlib.OpcodeResource):
770
  """/2/groups resource.
771

772
  """
773
  POST_OPCODE = opcodes.OpGroupAdd
774
  POST_RENAME = {
775
    "name": "group_name",
776
    }
777

    
778
  def GetPostOpInput(self):
779
    """Create a node group.
780

781

782
    """
783
    assert not self.items
784
    return (self.request_body, {
785
      "dry_run": self.dryRun(),
786
      })
787

    
788
  def GET(self):
789
    """Returns a list of all node groups.
790

791
    """
792
    client = self.GetClient()
793

    
794
    if self.useBulk():
795
      bulkdata = client.QueryGroups([], G_FIELDS, False)
796
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
797
    else:
798
      data = client.QueryGroups([], ["name"], False)
799
      groupnames = [row[0] for row in data]
800
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
801
                                   uri_fields=("name", "uri"))
802

    
803

    
804
class R_2_groups_name(baserlib.OpcodeResource):
805
  """/2/groups/[group_name] resource.
806

807
  """
808
  DELETE_OPCODE = opcodes.OpGroupRemove
809

    
810
  def GET(self):
811
    """Send information about a node group.
812

813
    """
814
    group_name = self.items[0]
815
    client = self.GetClient()
816

    
817
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
818
                                            names=[group_name], fields=G_FIELDS,
819
                                            use_locking=self.useLocking())
820

    
821
    return baserlib.MapFields(G_FIELDS, result[0])
822

    
823
  def GetDeleteOpInput(self):
824
    """Delete a node group.
825

826
    """
827
    assert len(self.items) == 1
828
    return ({}, {
829
      "group_name": self.items[0],
830
      "dry_run": self.dryRun(),
831
      })
832

    
833

    
834
class R_2_groups_name_modify(baserlib.OpcodeResource):
835
  """/2/groups/[group_name]/modify resource.
836

837
  """
838
  PUT_OPCODE = opcodes.OpGroupSetParams
839

    
840
  def GetPutOpInput(self):
841
    """Changes some parameters of node group.
842

843
    """
844
    assert self.items
845
    return (self.request_body, {
846
      "group_name": self.items[0],
847
      })
848

    
849

    
850
class R_2_groups_name_rename(baserlib.OpcodeResource):
851
  """/2/groups/[group_name]/rename resource.
852

853
  """
854
  PUT_OPCODE = opcodes.OpGroupRename
855

    
856
  def GetPutOpInput(self):
857
    """Changes the name of a node group.
858

859
    """
860
    assert len(self.items) == 1
861
    return (self.request_body, {
862
      "group_name": self.items[0],
863
      "dry_run": self.dryRun(),
864
      })
865

    
866

    
867
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
868
  """/2/groups/[group_name]/assign-nodes resource.
869

870
  """
871
  PUT_OPCODE = opcodes.OpGroupAssignNodes
872

    
873
  def GetPutOpInput(self):
874
    """Assigns nodes to a group.
875

876
    """
877
    assert len(self.items) == 1
878
    return (self.request_body, {
879
      "group_name": self.items[0],
880
      "dry_run": self.dryRun(),
881
      "force": self.useForce(),
882
      })
883

    
884

    
885
def _ConvertUsbDevices(data):
886
  """Convert in place the usb_devices string to the proper format.
887

888
  In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
889
  comma to space because commas cannot be accepted on the command line
890
  (they already act as the separator between different hvparams). RAPI
891
  should be able to accept commas for backwards compatibility, but we want
892
  it to also accept the new space separator. Therefore, we convert
893
  spaces into commas here and keep the old parsing logic elsewhere.
894

895
  """
896
  try:
897
    hvparams = data["hvparams"]
898
    usb_devices = hvparams[constants.HV_USB_DEVICES]
899
    hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
900
    data["hvparams"] = hvparams
901
  except KeyError:
902
    #No usb_devices, no modification required
903
    pass
904

    
905

    
906
class R_2_instances(baserlib.OpcodeResource):
907
  """/2/instances resource.
908

909
  """
910
  POST_OPCODE = opcodes.OpInstanceCreate
911
  POST_RENAME = {
912
    "os": "os_type",
913
    "name": "instance_name",
914
    }
915

    
916
  def GET(self):
917
    """Returns a list of all available instances.
918

919
    """
920
    client = self.GetClient()
921

    
922
    use_locking = self.useLocking()
923
    if self.useBulk():
924
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
925
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
926
    else:
927
      instancesdata = client.QueryInstances([], ["name"], use_locking)
928
      instanceslist = [row[0] for row in instancesdata]
929
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
930
                                   uri_fields=("id", "uri"))
931

    
932
  def GetPostOpInput(self):
933
    """Create an instance.
934

935
    @return: a job id
936

937
    """
938
    baserlib.CheckType(self.request_body, dict, "Body contents")
939

    
940
    # Default to request data version 0
941
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
942

    
943
    if data_version == 0:
944
      raise http.HttpBadRequest("Instance creation request version 0 is no"
945
                                " longer supported")
946
    elif data_version != 1:
947
      raise http.HttpBadRequest("Unsupported request data version %s" %
948
                                data_version)
949

    
950
    data = self.request_body.copy()
951
    # Remove "__version__"
952
    data.pop(_REQ_DATA_VERSION, None)
953

    
954
    _ConvertUsbDevices(data)
955

    
956
    return (data, {
957
      "dry_run": self.dryRun(),
958
      })
959

    
960

    
961
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
962
  """/2/instances-multi-alloc resource.
963

964
  """
965
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
966

    
967
  def GetPostOpInput(self):
968
    """Try to allocate multiple instances.
969

970
    @return: A dict with submitted jobs, allocatable instances and failed
971
             allocations
972

973
    """
974
    if "instances" not in self.request_body:
975
      raise http.HttpBadRequest("Request is missing required 'instances' field"
976
                                " in body")
977

    
978
    # Unlike most other RAPI calls, this one is composed of individual opcodes,
979
    # and we have to do the filling ourselves
980
    OPCODE_RENAME = {
981
      "os": "os_type",
982
      "name": "instance_name",
983
    }
984

    
985
    body = objects.FillDict(self.request_body, {
986
      "instances": [
987
        baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
988
                            rename=OPCODE_RENAME)
989
        for inst in self.request_body["instances"]
990
        ],
991
      })
992

    
993
    return (body, {
994
      "dry_run": self.dryRun(),
995
      })
996

    
997

    
998
class R_2_instances_name(baserlib.OpcodeResource):
999
  """/2/instances/[instance_name] resource.
1000

1001
  """
1002
  DELETE_OPCODE = opcodes.OpInstanceRemove
1003

    
1004
  def GET(self):
1005
    """Send information about an instance.
1006

1007
    """
1008
    client = self.GetClient()
1009
    instance_name = self.items[0]
1010

    
1011
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1012
                                            names=[instance_name],
1013
                                            fields=I_FIELDS,
1014
                                            use_locking=self.useLocking())
1015

    
1016
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1017

    
1018
  def GetDeleteOpInput(self):
1019
    """Delete an instance.
1020

1021
    """
1022
    assert len(self.items) == 1
1023
    return (self.request_body, {
1024
      "instance_name": self.items[0],
1025
      "ignore_failures": False,
1026
      "dry_run": self.dryRun(),
1027
      })
1028

    
1029

    
1030
class R_2_instances_name_info(baserlib.OpcodeResource):
1031
  """/2/instances/[instance_name]/info resource.
1032

1033
  """
1034
  GET_OPCODE = opcodes.OpInstanceQueryData
1035

    
1036
  def GetGetOpInput(self):
1037
    """Request detailed instance information.
1038

1039
    """
1040
    assert len(self.items) == 1
1041
    return ({}, {
1042
      "instances": [self.items[0]],
1043
      "static": bool(self._checkIntVariable("static", default=0)),
1044
      })
1045

    
1046

    
1047
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1048
  """/2/instances/[instance_name]/reboot resource.
1049

1050
  Implements an instance reboot.
1051

1052
  """
1053
  POST_OPCODE = opcodes.OpInstanceReboot
1054

    
1055
  def GetPostOpInput(self):
1056
    """Reboot an instance.
1057

1058
    The URI takes type=[hard|soft|full] and
1059
    ignore_secondaries=[False|True] parameters.
1060

1061
    """
1062
    return (self.request_body, {
1063
      "instance_name": self.items[0],
1064
      "reboot_type":
1065
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1066
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1067
      "dry_run": self.dryRun(),
1068
      })
1069

    
1070

    
1071
class R_2_instances_name_startup(baserlib.OpcodeResource):
1072
  """/2/instances/[instance_name]/startup resource.
1073

1074
  Implements an instance startup.
1075

1076
  """
1077
  PUT_OPCODE = opcodes.OpInstanceStartup
1078

    
1079
  def GetPutOpInput(self):
1080
    """Startup an instance.
1081

1082
    The URI takes force=[False|True] parameter to start the instance
1083
    if even if secondary disks are failing.
1084

1085
    """
1086
    return ({}, {
1087
      "instance_name": self.items[0],
1088
      "force": self.useForce(),
1089
      "dry_run": self.dryRun(),
1090
      "no_remember": bool(self._checkIntVariable("no_remember")),
1091
      })
1092

    
1093

    
1094
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1095
  """/2/instances/[instance_name]/shutdown resource.
1096

1097
  Implements an instance shutdown.
1098

1099
  """
1100
  PUT_OPCODE = opcodes.OpInstanceShutdown
1101

    
1102
  def GetPutOpInput(self):
1103
    """Shutdown an instance.
1104

1105
    """
1106
    return (self.request_body, {
1107
      "instance_name": self.items[0],
1108
      "no_remember": bool(self._checkIntVariable("no_remember")),
1109
      "dry_run": self.dryRun(),
1110
      })
1111

    
1112

    
1113
def _ParseInstanceReinstallRequest(name, data):
1114
  """Parses a request for reinstalling an instance.
1115

1116
  """
1117
  if not isinstance(data, dict):
1118
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1119

    
1120
  ostype = baserlib.CheckParameter(data, "os", default=None)
1121
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1122
                                  default=True)
1123
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1124

    
1125
  ops = [
1126
    opcodes.OpInstanceShutdown(instance_name=name),
1127
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1128
                                osparams=osparams),
1129
    ]
1130

    
1131
  if start:
1132
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1133

    
1134
  return ops
1135

    
1136

    
1137
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1138
  """/2/instances/[instance_name]/reinstall resource.
1139

1140
  Implements an instance reinstall.
1141

1142
  """
1143
  POST_OPCODE = opcodes.OpInstanceReinstall
1144

    
1145
  def POST(self):
1146
    """Reinstall an instance.
1147

1148
    The URI takes os=name and nostartup=[0|1] optional
1149
    parameters. By default, the instance will be started
1150
    automatically.
1151

1152
    """
1153
    if self.request_body:
1154
      if self.queryargs:
1155
        raise http.HttpBadRequest("Can't combine query and body parameters")
1156

    
1157
      body = self.request_body
1158
    elif self.queryargs:
1159
      # Legacy interface, do not modify/extend
1160
      body = {
1161
        "os": self._checkStringVariable("os"),
1162
        "start": not self._checkIntVariable("nostartup"),
1163
        }
1164
    else:
1165
      body = {}
1166

    
1167
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1168

    
1169
    return self.SubmitJob(ops)
1170

    
1171

    
1172
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1173
  """/2/instances/[instance_name]/replace-disks resource.
1174

1175
  """
1176
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1177

    
1178
  def GetPostOpInput(self):
1179
    """Replaces disks on an instance.
1180

1181
    """
1182
    static = {
1183
      "instance_name": self.items[0],
1184
      }
1185

    
1186
    if self.request_body:
1187
      data = self.request_body
1188
    elif self.queryargs:
1189
      # Legacy interface, do not modify/extend
1190
      data = {
1191
        "remote_node": self._checkStringVariable("remote_node", default=None),
1192
        "mode": self._checkStringVariable("mode", default=None),
1193
        "disks": self._checkStringVariable("disks", default=None),
1194
        "iallocator": self._checkStringVariable("iallocator", default=None),
1195
        }
1196
    else:
1197
      data = {}
1198

    
1199
    # Parse disks
1200
    try:
1201
      raw_disks = data.pop("disks")
1202
    except KeyError:
1203
      pass
1204
    else:
1205
      if raw_disks:
1206
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1207
          data["disks"] = raw_disks
1208
        else:
1209
          # Backwards compatibility for strings of the format "1, 2, 3"
1210
          try:
1211
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1212
          except (TypeError, ValueError), err:
1213
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1214

    
1215
    return (data, static)
1216

    
1217

    
1218
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1219
  """/2/instances/[instance_name]/activate-disks resource.
1220

1221
  """
1222
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1223

    
1224
  def GetPutOpInput(self):
1225
    """Activate disks for an instance.
1226

1227
    The URI might contain ignore_size to ignore current recorded size.
1228

1229
    """
1230
    return ({}, {
1231
      "instance_name": self.items[0],
1232
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
1233
      })
1234

    
1235

    
1236
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1237
  """/2/instances/[instance_name]/deactivate-disks resource.
1238

1239
  """
1240
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1241

    
1242
  def GetPutOpInput(self):
1243
    """Deactivate disks for an instance.
1244

1245
    """
1246
    return ({}, {
1247
      "instance_name": self.items[0],
1248
      })
1249

    
1250

    
1251
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1252
  """/2/instances/[instance_name]/recreate-disks resource.
1253

1254
  """
1255
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1256

    
1257
  def GetPostOpInput(self):
1258
    """Recreate disks for an instance.
1259

1260
    """
1261
    return ({}, {
1262
      "instance_name": self.items[0],
1263
      })
1264

    
1265

    
1266
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1267
  """/2/instances/[instance_name]/prepare-export resource.
1268

1269
  """
1270
  PUT_OPCODE = opcodes.OpBackupPrepare
1271

    
1272
  def GetPutOpInput(self):
1273
    """Prepares an export for an instance.
1274

1275
    """
1276
    return ({}, {
1277
      "instance_name": self.items[0],
1278
      "mode": self._checkStringVariable("mode"),
1279
      })
1280

    
1281

    
1282
class R_2_instances_name_export(baserlib.OpcodeResource):
1283
  """/2/instances/[instance_name]/export resource.
1284

1285
  """
1286
  PUT_OPCODE = opcodes.OpBackupExport
1287
  PUT_RENAME = {
1288
    "destination": "target_node",
1289
    }
1290

    
1291
  def GetPutOpInput(self):
1292
    """Exports an instance.
1293

1294
    """
1295
    return (self.request_body, {
1296
      "instance_name": self.items[0],
1297
      })
1298

    
1299

    
1300
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1301
  """/2/instances/[instance_name]/migrate resource.
1302

1303
  """
1304
  PUT_OPCODE = opcodes.OpInstanceMigrate
1305

    
1306
  def GetPutOpInput(self):
1307
    """Migrates an instance.
1308

1309
    """
1310
    return (self.request_body, {
1311
      "instance_name": self.items[0],
1312
      })
1313

    
1314

    
1315
class R_2_instances_name_failover(baserlib.OpcodeResource):
1316
  """/2/instances/[instance_name]/failover resource.
1317

1318
  """
1319
  PUT_OPCODE = opcodes.OpInstanceFailover
1320

    
1321
  def GetPutOpInput(self):
1322
    """Does a failover of an instance.
1323

1324
    """
1325
    return (self.request_body, {
1326
      "instance_name": self.items[0],
1327
      })
1328

    
1329

    
1330
class R_2_instances_name_rename(baserlib.OpcodeResource):
1331
  """/2/instances/[instance_name]/rename resource.
1332

1333
  """
1334
  PUT_OPCODE = opcodes.OpInstanceRename
1335

    
1336
  def GetPutOpInput(self):
1337
    """Changes the name of an instance.
1338

1339
    """
1340
    return (self.request_body, {
1341
      "instance_name": self.items[0],
1342
      })
1343

    
1344

    
1345
class R_2_instances_name_modify(baserlib.OpcodeResource):
1346
  """/2/instances/[instance_name]/modify resource.
1347

1348
  """
1349
  PUT_OPCODE = opcodes.OpInstanceSetParams
1350

    
1351
  def GetPutOpInput(self):
1352
    """Changes parameters of an instance.
1353

1354
    """
1355
    data = self.request_body.copy()
1356
    _ConvertUsbDevices(data)
1357

    
1358
    return (data, {
1359
      "instance_name": self.items[0],
1360
      })
1361

    
1362

    
1363
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1364
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1365

1366
  """
1367
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1368

    
1369
  def GetPostOpInput(self):
1370
    """Increases the size of an instance disk.
1371

1372
    """
1373
    return (self.request_body, {
1374
      "instance_name": self.items[0],
1375
      "disk": int(self.items[1]),
1376
      })
1377

    
1378

    
1379
class R_2_instances_name_console(baserlib.ResourceBase):
1380
  """/2/instances/[instance_name]/console resource.
1381

1382
  """
1383
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1384
  GET_OPCODE = opcodes.OpInstanceConsole
1385

    
1386
  def GET(self):
1387
    """Request information for connecting to instance's console.
1388

1389
    @return: Serialized instance console description, see
1390
             L{objects.InstanceConsole}
1391

1392
    """
1393
    instance_name = self.items[0]
1394
    client = self.GetClient()
1395

    
1396
    ((console, oper_state), ) = \
1397
        client.QueryInstances([instance_name], ["console", "oper_state"], False)
1398

    
1399
    if not oper_state:
1400
      raise http.HttpServiceUnavailable("Instance console unavailable")
1401

    
1402
    assert isinstance(console, dict)
1403
    return console
1404

    
1405

    
1406
def _GetQueryFields(args):
1407
  """Tries to extract C{fields} query parameter.
1408

1409
  @type args: dictionary
1410
  @rtype: list of string
1411
  @raise http.HttpBadRequest: When parameter can't be found
1412

1413
  """
1414
  try:
1415
    fields = args["fields"]
1416
  except KeyError:
1417
    raise http.HttpBadRequest("Missing 'fields' query argument")
1418

    
1419
  return _SplitQueryFields(fields[0])
1420

    
1421

    
1422
def _SplitQueryFields(fields):
1423
  """Splits fields as given for a query request.
1424

1425
  @type fields: string
1426
  @rtype: list of string
1427

1428
  """
1429
  return [i.strip() for i in fields.split(",")]
1430

    
1431

    
1432
class R_2_query(baserlib.ResourceBase):
1433
  """/2/query/[resource] resource.
1434

1435
  """
1436
  # Results might contain sensitive information
1437
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1438
  PUT_ACCESS = GET_ACCESS
1439
  GET_OPCODE = opcodes.OpQuery
1440
  PUT_OPCODE = opcodes.OpQuery
1441

    
1442
  def _Query(self, fields, qfilter):
1443
    client = self.GetClient()
1444
    return client.Query(self.items[0], fields, qfilter).ToDict()
1445

    
1446
  def GET(self):
1447
    """Returns resource information.
1448

1449
    @return: Query result, see L{objects.QueryResponse}
1450

1451
    """
1452
    return self._Query(_GetQueryFields(self.queryargs), None)
1453

    
1454
  def PUT(self):
1455
    """Submits job querying for resources.
1456

1457
    @return: Query result, see L{objects.QueryResponse}
1458

1459
    """
1460
    body = self.request_body
1461

    
1462
    baserlib.CheckType(body, dict, "Body contents")
1463

    
1464
    try:
1465
      fields = body["fields"]
1466
    except KeyError:
1467
      fields = _GetQueryFields(self.queryargs)
1468

    
1469
    qfilter = body.get("qfilter", None)
1470
    # TODO: remove this after 2.7
1471
    if qfilter is None:
1472
      qfilter = body.get("filter", None)
1473

    
1474
    return self._Query(fields, qfilter)
1475

    
1476

    
1477
class R_2_query_fields(baserlib.ResourceBase):
1478
  """/2/query/[resource]/fields resource.
1479

1480
  """
1481
  GET_OPCODE = opcodes.OpQueryFields
1482

    
1483
  def GET(self):
1484
    """Retrieves list of available fields for a resource.
1485

1486
    @return: List of serialized L{objects.QueryFieldDefinition}
1487

1488
    """
1489
    try:
1490
      raw_fields = self.queryargs["fields"]
1491
    except KeyError:
1492
      fields = None
1493
    else:
1494
      fields = _SplitQueryFields(raw_fields[0])
1495

    
1496
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1497

    
1498

    
1499
class _R_Tags(baserlib.OpcodeResource):
1500
  """Quasiclass for tagging resources.
1501

1502
  Manages tags. When inheriting this class you must define the
1503
  TAG_LEVEL for it.
1504

1505
  """
1506
  TAG_LEVEL = None
1507
  GET_OPCODE = opcodes.OpTagsGet
1508
  PUT_OPCODE = opcodes.OpTagsSet
1509
  DELETE_OPCODE = opcodes.OpTagsDel
1510

    
1511
  def __init__(self, items, queryargs, req, **kwargs):
1512
    """A tag resource constructor.
1513

1514
    We have to override the default to sort out cluster naming case.
1515

1516
    """
1517
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1518

    
1519
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1520
      self.name = None
1521
    else:
1522
      self.name = items[0]
1523

    
1524
  def GET(self):
1525
    """Returns a list of tags.
1526

1527
    Example: ["tag1", "tag2", "tag3"]
1528

1529
    """
1530
    kind = self.TAG_LEVEL
1531

    
1532
    if kind in (constants.TAG_INSTANCE,
1533
                constants.TAG_NODEGROUP,
1534
                constants.TAG_NODE,
1535
                constants.TAG_NETWORK):
1536
      if not self.name:
1537
        raise http.HttpBadRequest("Missing name on tag request")
1538

    
1539
      cl = self.GetClient()
1540
      tags = list(cl.QueryTags(kind, self.name))
1541

    
1542
    elif kind == constants.TAG_CLUSTER:
1543
      assert not self.name
1544
      # TODO: Use query API?
1545
      ssc = ssconf.SimpleStore()
1546
      tags = ssc.GetClusterTags()
1547

    
1548
    else:
1549
      raise http.HttpBadRequest("Unhandled tag type!")
1550

    
1551
    return list(tags)
1552

    
1553
  def GetPutOpInput(self):
1554
    """Add a set of tags.
1555

1556
    The request as a list of strings should be PUT to this URI. And
1557
    you'll have back a job id.
1558

1559
    """
1560
    return ({}, {
1561
      "kind": self.TAG_LEVEL,
1562
      "name": self.name,
1563
      "tags": self.queryargs.get("tag", []),
1564
      "dry_run": self.dryRun(),
1565
      })
1566

    
1567
  def GetDeleteOpInput(self):
1568
    """Delete a tag.
1569

1570
    In order to delete a set of tags, the DELETE
1571
    request should be addressed to URI like:
1572
    /tags?tag=[tag]&tag=[tag]
1573

1574
    """
1575
    # Re-use code
1576
    return self.GetPutOpInput()
1577

    
1578

    
1579
class R_2_instances_name_tags(_R_Tags):
1580
  """ /2/instances/[instance_name]/tags resource.
1581

1582
  Manages per-instance tags.
1583

1584
  """
1585
  TAG_LEVEL = constants.TAG_INSTANCE
1586

    
1587

    
1588
class R_2_nodes_name_tags(_R_Tags):
1589
  """ /2/nodes/[node_name]/tags resource.
1590

1591
  Manages per-node tags.
1592

1593
  """
1594
  TAG_LEVEL = constants.TAG_NODE
1595

    
1596

    
1597
class R_2_groups_name_tags(_R_Tags):
1598
  """ /2/groups/[group_name]/tags resource.
1599

1600
  Manages per-nodegroup tags.
1601

1602
  """
1603
  TAG_LEVEL = constants.TAG_NODEGROUP
1604

    
1605

    
1606
class R_2_networks_name_tags(_R_Tags):
1607
  """ /2/networks/[network_name]/tags resource.
1608

1609
  Manages per-network tags.
1610

1611
  """
1612
  TAG_LEVEL = constants.TAG_NETWORK
1613

    
1614

    
1615
class R_2_tags(_R_Tags):
1616
  """ /2/tags resource.
1617

1618
  Manages cluster tags.
1619

1620
  """
1621
  TAG_LEVEL = constants.TAG_CLUSTER