Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 02a6b50a

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_usage", "disk.uuids", "disk.names",
78
            "beparams", "hvparams",
79
            "oper_state", "oper_ram", "oper_vcpus", "status",
80
            "custom_hvparams", "custom_beparams", "custom_nicparams",
81
            ] + _COMMON_FIELDS
82

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
168

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

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

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

    
181
  return inst
182

    
183

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

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

192
    """
193
    return None
194

    
195

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

199
  """
200

    
201

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

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

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

213
    """
214
    return constants.RAPI_VERSION
215

    
216

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

220
  """
221
  GET_OPCODE = opcodes.OpClusterQuery
222

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

226
    """
227
    client = self.GetClient(query=True)
228
    return client.QueryClusterInfo()
229

    
230

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

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

239
    """
240
    return list(ALL_FEATURES)
241

    
242

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

246
  """
247
  GET_OPCODE = opcodes.OpOsDiagnose
248

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

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

254
    Example: ["debian-etch"]
255

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

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

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

    
271
    return os_names
272

    
273

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

277
  """
278
  PUT_OPCODE = opcodes.OpClusterRedistConf
279

    
280

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

284
  """
285
  PUT_OPCODE = opcodes.OpClusterSetParams
286

    
287

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

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

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

297
    """
298
    client = self.GetClient(query=True)
299

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

    
308

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

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

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

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

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

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

    
340

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

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

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

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

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

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

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

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

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

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

    
382
    (job_info, log_entries) = result
383

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

    
389

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

393
  """
394
  GET_OPCODE = opcodes.OpNodeQuery
395

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

399
    """
400
    client = self.GetClient(query=True)
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
  GET_OPCODE = opcodes.OpNodeQuery
417

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

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

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

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

    
431

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

435
  """
436
  POST_OPCODE = opcodes.OpNodePowercycle
437

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

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

    
447

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

451
  """
452
  PUT_OPCODE = opcodes.OpNodeSetParams
453

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

457
    @return: Node role
458

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

    
465
    return _NR_MAP[result[0][0]]
466

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

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

    
473
    role = self.request_body
474

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

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

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

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

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

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

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

    
506

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

510
  """
511
  POST_OPCODE = opcodes.OpNodeEvacuate
512

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

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

    
522

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

526
  """
527
  POST_OPCODE = opcodes.OpNodeMigrate
528

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

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

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

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

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

    
557

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

561
  """
562
  POST_OPCODE = opcodes.OpNodeSetParams
563

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

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

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

    
574

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

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

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

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

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

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

    
600

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

604
  """
605
  PUT_OPCODE = opcodes.OpNodeModifyStorage
606

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

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

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

    
618
    changes = {}
619

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

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

    
631

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

635
  """
636
  PUT_OPCODE = opcodes.OpRepairNodeStorage
637

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

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

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

    
654

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

658
  """
659
  GET_OPCODE = opcodes.OpNetworkQuery
660
  POST_OPCODE = opcodes.OpNetworkAdd
661
  POST_RENAME = {
662
    "name": "network_name",
663
    }
664

    
665
  def GetPostOpInput(self):
666
    """Create a network.
667

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

    
674
  def GET(self):
675
    """Returns a list of all networks.
676

677
    """
678
    client = self.GetClient(query=True)
679

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

    
689

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

693
  """
694
  DELETE_OPCODE = opcodes.OpNetworkRemove
695

    
696
  def GET(self):
697
    """Send information about a network.
698

699
    """
700
    network_name = self.items[0]
701
    client = self.GetClient(query=True)
702

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

    
708
    return baserlib.MapFields(NET_FIELDS, result[0])
709

    
710
  def GetDeleteOpInput(self):
711
    """Delete a network.
712

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

    
720

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

724
  """
725
  PUT_OPCODE = opcodes.OpNetworkConnect
726

    
727
  def GetPutOpInput(self):
728
    """Changes some parameters of node group.
729

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

    
737

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

741
  """
742
  PUT_OPCODE = opcodes.OpNetworkDisconnect
743

    
744
  def GetPutOpInput(self):
745
    """Changes some parameters of node group.
746

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

    
754

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

758
  """
759
  PUT_OPCODE = opcodes.OpNetworkSetParams
760

    
761
  def GetPutOpInput(self):
762
    """Changes some parameters of network.
763

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

    
770

    
771
class R_2_groups(baserlib.OpcodeResource):
772
  """/2/groups resource.
773

774
  """
775
  GET_OPCODE = opcodes.OpGroupQuery
776
  POST_OPCODE = opcodes.OpGroupAdd
777
  POST_RENAME = {
778
    "name": "group_name",
779
    }
780

    
781
  def GetPostOpInput(self):
782
    """Create a node group.
783

784

785
    """
786
    assert not self.items
787
    return (self.request_body, {
788
      "dry_run": self.dryRun(),
789
      })
790

    
791
  def GET(self):
792
    """Returns a list of all node groups.
793

794
    """
795
    client = self.GetClient(query=True)
796

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

    
806

    
807
class R_2_groups_name(baserlib.OpcodeResource):
808
  """/2/groups/[group_name] resource.
809

810
  """
811
  DELETE_OPCODE = opcodes.OpGroupRemove
812

    
813
  def GET(self):
814
    """Send information about a node group.
815

816
    """
817
    group_name = self.items[0]
818
    client = self.GetClient(query=True)
819

    
820
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
821
                                            names=[group_name], fields=G_FIELDS,
822
                                            use_locking=self.useLocking())
823

    
824
    return baserlib.MapFields(G_FIELDS, result[0])
825

    
826
  def GetDeleteOpInput(self):
827
    """Delete a node group.
828

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

    
836

    
837
class R_2_groups_name_modify(baserlib.OpcodeResource):
838
  """/2/groups/[group_name]/modify resource.
839

840
  """
841
  PUT_OPCODE = opcodes.OpGroupSetParams
842

    
843
  def GetPutOpInput(self):
844
    """Changes some parameters of node group.
845

846
    """
847
    assert self.items
848
    return (self.request_body, {
849
      "group_name": self.items[0],
850
      })
851

    
852

    
853
class R_2_groups_name_rename(baserlib.OpcodeResource):
854
  """/2/groups/[group_name]/rename resource.
855

856
  """
857
  PUT_OPCODE = opcodes.OpGroupRename
858

    
859
  def GetPutOpInput(self):
860
    """Changes the name of a node group.
861

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

    
869

    
870
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
871
  """/2/groups/[group_name]/assign-nodes resource.
872

873
  """
874
  PUT_OPCODE = opcodes.OpGroupAssignNodes
875

    
876
  def GetPutOpInput(self):
877
    """Assigns nodes to a group.
878

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

    
887

    
888
def _ConvertUsbDevices(data):
889
  """Convert in place the usb_devices string to the proper format.
890

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

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

    
908

    
909
class R_2_instances(baserlib.OpcodeResource):
910
  """/2/instances resource.
911

912
  """
913
  GET_OPCODE = opcodes.OpInstanceQuery
914
  POST_OPCODE = opcodes.OpInstanceCreate
915
  POST_RENAME = {
916
    "os": "os_type",
917
    "name": "instance_name",
918
    }
919

    
920
  def GET(self):
921
    """Returns a list of all available instances.
922

923
    """
924
    client = self.GetClient()
925

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

    
936
  def GetPostOpInput(self):
937
    """Create an instance.
938

939
    @return: a job id
940

941
    """
942
    baserlib.CheckType(self.request_body, dict, "Body contents")
943

    
944
    # Default to request data version 0
945
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
946

    
947
    if data_version == 0:
948
      raise http.HttpBadRequest("Instance creation request version 0 is no"
949
                                " longer supported")
950
    elif data_version != 1:
951
      raise http.HttpBadRequest("Unsupported request data version %s" %
952
                                data_version)
953

    
954
    data = self.request_body.copy()
955
    # Remove "__version__"
956
    data.pop(_REQ_DATA_VERSION, None)
957

    
958
    _ConvertUsbDevices(data)
959

    
960
    return (data, {
961
      "dry_run": self.dryRun(),
962
      })
963

    
964

    
965
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
966
  """/2/instances-multi-alloc resource.
967

968
  """
969
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
970

    
971
  def GetPostOpInput(self):
972
    """Try to allocate multiple instances.
973

974
    @return: A dict with submitted jobs, allocatable instances and failed
975
             allocations
976

977
    """
978
    if "instances" not in self.request_body:
979
      raise http.HttpBadRequest("Request is missing required 'instances' field"
980
                                " in body")
981

    
982
    op_id = {
983
      "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
984
      }
985
    body = objects.FillDict(self.request_body, {
986
      "instances": [objects.FillDict(inst, op_id)
987
                    for inst in self.request_body["instances"]],
988
      })
989

    
990
    return (body, {
991
      "dry_run": self.dryRun(),
992
      })
993

    
994

    
995
class R_2_instances_name(baserlib.OpcodeResource):
996
  """/2/instances/[instance_name] resource.
997

998
  """
999
  GET_OPCODE = opcodes.OpInstanceQuery
1000
  DELETE_OPCODE = opcodes.OpInstanceRemove
1001

    
1002
  def GET(self):
1003
    """Send information about an instance.
1004

1005
    """
1006
    client = self.GetClient()
1007
    instance_name = self.items[0]
1008

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

    
1014
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1015

    
1016
  def GetDeleteOpInput(self):
1017
    """Delete an instance.
1018

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

    
1027

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

1031
  """
1032
  GET_OPCODE = opcodes.OpInstanceQueryData
1033

    
1034
  def GetGetOpInput(self):
1035
    """Request detailed instance information.
1036

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

    
1044

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

1048
  Implements an instance reboot.
1049

1050
  """
1051
  POST_OPCODE = opcodes.OpInstanceReboot
1052

    
1053
  def GetPostOpInput(self):
1054
    """Reboot an instance.
1055

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

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

    
1068

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

1072
  Implements an instance startup.
1073

1074
  """
1075
  PUT_OPCODE = opcodes.OpInstanceStartup
1076

    
1077
  def GetPutOpInput(self):
1078
    """Startup an instance.
1079

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

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

    
1091

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

1095
  Implements an instance shutdown.
1096

1097
  """
1098
  PUT_OPCODE = opcodes.OpInstanceShutdown
1099

    
1100
  def GetPutOpInput(self):
1101
    """Shutdown an instance.
1102

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

    
1110

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

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

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

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

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

    
1132
  return ops
1133

    
1134

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

1138
  Implements an instance reinstall.
1139

1140
  """
1141
  POST_OPCODE = opcodes.OpInstanceReinstall
1142

    
1143
  def POST(self):
1144
    """Reinstall an instance.
1145

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

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

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

    
1165
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1166

    
1167
    return self.SubmitJob(ops)
1168

    
1169

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

1173
  """
1174
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1175

    
1176
  def GetPostOpInput(self):
1177
    """Replaces disks on an instance.
1178

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

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

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

    
1213
    return (data, static)
1214

    
1215

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

1219
  """
1220
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1221

    
1222
  def GetPutOpInput(self):
1223
    """Activate disks for an instance.
1224

1225
    The URI might contain ignore_size to ignore current recorded size.
1226

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

    
1233

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

1237
  """
1238
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1239

    
1240
  def GetPutOpInput(self):
1241
    """Deactivate disks for an instance.
1242

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

    
1248

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

1252
  """
1253
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1254

    
1255
  def GetPostOpInput(self):
1256
    """Recreate disks for an instance.
1257

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

    
1263

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

1267
  """
1268
  PUT_OPCODE = opcodes.OpBackupPrepare
1269

    
1270
  def GetPutOpInput(self):
1271
    """Prepares an export for an instance.
1272

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

    
1279

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

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

    
1289
  def GetPutOpInput(self):
1290
    """Exports an instance.
1291

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

    
1297

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

1301
  """
1302
  PUT_OPCODE = opcodes.OpInstanceMigrate
1303

    
1304
  def GetPutOpInput(self):
1305
    """Migrates an instance.
1306

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

    
1312

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

1316
  """
1317
  PUT_OPCODE = opcodes.OpInstanceFailover
1318

    
1319
  def GetPutOpInput(self):
1320
    """Does a failover of an instance.
1321

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

    
1327

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

1331
  """
1332
  PUT_OPCODE = opcodes.OpInstanceRename
1333

    
1334
  def GetPutOpInput(self):
1335
    """Changes the name of an instance.
1336

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

    
1342

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

1346
  """
1347
  PUT_OPCODE = opcodes.OpInstanceSetParams
1348

    
1349
  def GetPutOpInput(self):
1350
    """Changes parameters of an instance.
1351

1352
    """
1353
    data = self.request_body.copy()
1354
    _ConvertUsbDevices(data)
1355

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

    
1360

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

1364
  """
1365
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1366

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

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

    
1376

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

1380
  """
1381
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1382
  GET_OPCODE = opcodes.OpInstanceConsole
1383

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

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

1390
    """
1391
    client = self.GetClient()
1392

    
1393
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1394

    
1395
    if console is None:
1396
      raise http.HttpServiceUnavailable("Instance console unavailable")
1397

    
1398
    assert isinstance(console, dict)
1399
    return console
1400

    
1401

    
1402
def _GetQueryFields(args):
1403
  """Tries to extract C{fields} query parameter.
1404

1405
  @type args: dictionary
1406
  @rtype: list of string
1407
  @raise http.HttpBadRequest: When parameter can't be found
1408

1409
  """
1410
  try:
1411
    fields = args["fields"]
1412
  except KeyError:
1413
    raise http.HttpBadRequest("Missing 'fields' query argument")
1414

    
1415
  return _SplitQueryFields(fields[0])
1416

    
1417

    
1418
def _SplitQueryFields(fields):
1419
  """Splits fields as given for a query request.
1420

1421
  @type fields: string
1422
  @rtype: list of string
1423

1424
  """
1425
  return [i.strip() for i in fields.split(",")]
1426

    
1427

    
1428
class R_2_query(baserlib.ResourceBase):
1429
  """/2/query/[resource] resource.
1430

1431
  """
1432
  # Results might contain sensitive information
1433
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1434
  PUT_ACCESS = GET_ACCESS
1435
  GET_OPCODE = opcodes.OpQuery
1436
  PUT_OPCODE = opcodes.OpQuery
1437

    
1438
  def _Query(self, fields, qfilter):
1439
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1440

    
1441
  def GET(self):
1442
    """Returns resource information.
1443

1444
    @return: Query result, see L{objects.QueryResponse}
1445

1446
    """
1447
    return self._Query(_GetQueryFields(self.queryargs), None)
1448

    
1449
  def PUT(self):
1450
    """Submits job querying for resources.
1451

1452
    @return: Query result, see L{objects.QueryResponse}
1453

1454
    """
1455
    body = self.request_body
1456

    
1457
    baserlib.CheckType(body, dict, "Body contents")
1458

    
1459
    try:
1460
      fields = body["fields"]
1461
    except KeyError:
1462
      fields = _GetQueryFields(self.queryargs)
1463

    
1464
    qfilter = body.get("qfilter", None)
1465
    # TODO: remove this after 2.7
1466
    if qfilter is None:
1467
      qfilter = body.get("filter", None)
1468

    
1469
    return self._Query(fields, qfilter)
1470

    
1471

    
1472
class R_2_query_fields(baserlib.ResourceBase):
1473
  """/2/query/[resource]/fields resource.
1474

1475
  """
1476
  GET_OPCODE = opcodes.OpQueryFields
1477

    
1478
  def GET(self):
1479
    """Retrieves list of available fields for a resource.
1480

1481
    @return: List of serialized L{objects.QueryFieldDefinition}
1482

1483
    """
1484
    try:
1485
      raw_fields = self.queryargs["fields"]
1486
    except KeyError:
1487
      fields = None
1488
    else:
1489
      fields = _SplitQueryFields(raw_fields[0])
1490

    
1491
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1492

    
1493

    
1494
class _R_Tags(baserlib.OpcodeResource):
1495
  """Quasiclass for tagging resources.
1496

1497
  Manages tags. When inheriting this class you must define the
1498
  TAG_LEVEL for it.
1499

1500
  """
1501
  TAG_LEVEL = None
1502
  GET_OPCODE = opcodes.OpTagsGet
1503
  PUT_OPCODE = opcodes.OpTagsSet
1504
  DELETE_OPCODE = opcodes.OpTagsDel
1505

    
1506
  def __init__(self, items, queryargs, req, **kwargs):
1507
    """A tag resource constructor.
1508

1509
    We have to override the default to sort out cluster naming case.
1510

1511
    """
1512
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1513

    
1514
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1515
      self.name = None
1516
    else:
1517
      self.name = items[0]
1518

    
1519
  def GET(self):
1520
    """Returns a list of tags.
1521

1522
    Example: ["tag1", "tag2", "tag3"]
1523

1524
    """
1525
    kind = self.TAG_LEVEL
1526

    
1527
    if kind in (constants.TAG_INSTANCE,
1528
                constants.TAG_NODEGROUP,
1529
                constants.TAG_NODE,
1530
                constants.TAG_NETWORK):
1531
      if not self.name:
1532
        raise http.HttpBadRequest("Missing name on tag request")
1533

    
1534
      cl = self.GetClient(query=True)
1535
      tags = list(cl.QueryTags(kind, self.name))
1536

    
1537
    elif kind == constants.TAG_CLUSTER:
1538
      assert not self.name
1539
      # TODO: Use query API?
1540
      ssc = ssconf.SimpleStore()
1541
      tags = ssc.GetClusterTags()
1542

    
1543
    else:
1544
      raise http.HttpBadRequest("Unhandled tag type!")
1545

    
1546
    return list(tags)
1547

    
1548
  def GetPutOpInput(self):
1549
    """Add a set of tags.
1550

1551
    The request as a list of strings should be PUT to this URI. And
1552
    you'll have back a job id.
1553

1554
    """
1555
    return ({}, {
1556
      "kind": self.TAG_LEVEL,
1557
      "name": self.name,
1558
      "tags": self.queryargs.get("tag", []),
1559
      "dry_run": self.dryRun(),
1560
      })
1561

    
1562
  def GetDeleteOpInput(self):
1563
    """Delete a tag.
1564

1565
    In order to delete a set of tags, the DELETE
1566
    request should be addressed to URI like:
1567
    /tags?tag=[tag]&tag=[tag]
1568

1569
    """
1570
    # Re-use code
1571
    return self.GetPutOpInput()
1572

    
1573

    
1574
class R_2_instances_name_tags(_R_Tags):
1575
  """ /2/instances/[instance_name]/tags resource.
1576

1577
  Manages per-instance tags.
1578

1579
  """
1580
  TAG_LEVEL = constants.TAG_INSTANCE
1581

    
1582

    
1583
class R_2_nodes_name_tags(_R_Tags):
1584
  """ /2/nodes/[node_name]/tags resource.
1585

1586
  Manages per-node tags.
1587

1588
  """
1589
  TAG_LEVEL = constants.TAG_NODE
1590

    
1591

    
1592
class R_2_groups_name_tags(_R_Tags):
1593
  """ /2/groups/[group_name]/tags resource.
1594

1595
  Manages per-nodegroup tags.
1596

1597
  """
1598
  TAG_LEVEL = constants.TAG_NODEGROUP
1599

    
1600

    
1601
class R_2_networks_name_tags(_R_Tags):
1602
  """ /2/networks/[network_name]/tags resource.
1603

1604
  Manages per-network tags.
1605

1606
  """
1607
  TAG_LEVEL = constants.TAG_NETWORK
1608

    
1609

    
1610
class R_2_tags(_R_Tags):
1611
  """ /2/tags resource.
1612

1613
  Manages cluster tags.
1614

1615
  """
1616
  TAG_LEVEL = constants.TAG_CLUSTER