Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ ddaf6cd3

History | View | Annotate | Download (40.3 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
  GET_ALIASES = {
224
    "volume_group_name": "vg_name",
225
    "drbd_usermode_helper": "drbd_helper",
226
    }
227

    
228
  def GET(self):
229
    """Returns cluster information.
230

231
    """
232
    client = self.GetClient(query=True)
233
    return client.QueryClusterInfo()
234

    
235

    
236
class R_2_features(baserlib.ResourceBase):
237
  """/2/features resource.
238

239
  """
240
  @staticmethod
241
  def GET():
242
    """Returns list of optional RAPI features implemented.
243

244
    """
245
    return list(ALL_FEATURES)
246

    
247

    
248
class R_2_os(baserlib.OpcodeResource):
249
  """/2/os resource.
250

251
  """
252
  GET_OPCODE = opcodes.OpOsDiagnose
253

    
254
  def GET(self):
255
    """Return a list of all OSes.
256

257
    Can return error 500 in case of a problem.
258

259
    Example: ["debian-etch"]
260

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

    
269
    if not isinstance(diagnose_data, list):
270
      raise http.HttpBadGateway(message="Can't get OS list")
271

    
272
    os_names = []
273
    for (name, variants) in diagnose_data:
274
      os_names.extend(cli.CalculateOSNames(name, variants))
275

    
276
    return os_names
277

    
278

    
279
class R_2_redist_config(baserlib.OpcodeResource):
280
  """/2/redistribute-config resource.
281

282
  """
283
  PUT_OPCODE = opcodes.OpClusterRedistConf
284

    
285

    
286
class R_2_cluster_modify(baserlib.OpcodeResource):
287
  """/2/modify resource.
288

289
  """
290
  PUT_OPCODE = opcodes.OpClusterSetParams
291

    
292

    
293
class R_2_jobs(baserlib.ResourceBase):
294
  """/2/jobs resource.
295

296
  """
297
  def GET(self):
298
    """Returns a dictionary of jobs.
299

300
    @return: a dictionary with jobs id and uri.
301

302
    """
303
    client = self.GetClient(query=True)
304

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

    
313

    
314
class R_2_jobs_id(baserlib.ResourceBase):
315
  """/2/jobs/[job_id] resource.
316

317
  """
318
  def GET(self):
319
    """Returns a job status.
320

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

330
    """
331
    job_id = self.items[0]
332
    result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
333
    if result is None:
334
      raise http.HttpNotFound()
335
    return baserlib.MapFields(J_FIELDS, result)
336

    
337
  def DELETE(self):
338
    """Cancel not-yet-started job.
339

340
    """
341
    job_id = self.items[0]
342
    result = self.GetClient().CancelJob(job_id)
343
    return result
344

    
345

    
346
class R_2_jobs_id_wait(baserlib.ResourceBase):
347
  """/2/jobs/[job_id]/wait resource.
348

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

    
354
  def GET(self):
355
    """Waits for job changes.
356

357
    """
358
    job_id = self.items[0]
359

    
360
    fields = self.getBodyParameter("fields")
361
    prev_job_info = self.getBodyParameter("previous_job_info", None)
362
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
363

    
364
    if not isinstance(fields, list):
365
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
366

    
367
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
368
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
369
                                " be a list")
370

    
371
    if not (prev_log_serial is None or
372
            isinstance(prev_log_serial, (int, long))):
373
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
374
                                " be a number")
375

    
376
    client = self.GetClient()
377
    result = client.WaitForJobChangeOnce(job_id, fields,
378
                                         prev_job_info, prev_log_serial,
379
                                         timeout=_WFJC_TIMEOUT)
380
    if not result:
381
      raise http.HttpNotFound()
382

    
383
    if result == constants.JOB_NOTCHANGED:
384
      # No changes
385
      return None
386

    
387
    (job_info, log_entries) = result
388

    
389
    return {
390
      "job_info": job_info,
391
      "log_entries": log_entries,
392
      }
393

    
394

    
395
class R_2_nodes(baserlib.OpcodeResource):
396
  """/2/nodes resource.
397

398
  """
399
  GET_OPCODE = opcodes.OpNodeQuery
400

    
401
  def GET(self):
402
    """Returns a list of all nodes.
403

404
    """
405
    client = self.GetClient(query=True)
406

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

    
416

    
417
class R_2_nodes_name(baserlib.OpcodeResource):
418
  """/2/nodes/[node_name] resource.
419

420
  """
421
  GET_OPCODE = opcodes.OpNodeQuery
422
  GET_ALIASES = {
423
    "sip": "secondary_ip",
424
    }
425

    
426
  def GET(self):
427
    """Send information about a node.
428

429
    """
430
    node_name = self.items[0]
431
    client = self.GetClient(query=True)
432

    
433
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
434
                                            names=[node_name], fields=N_FIELDS,
435
                                            use_locking=self.useLocking())
436

    
437
    return baserlib.MapFields(N_FIELDS, result[0])
438

    
439

    
440
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
441
  """/2/nodes/[node_name]/powercycle resource.
442

443
  """
444
  POST_OPCODE = opcodes.OpNodePowercycle
445

    
446
  def GetPostOpInput(self):
447
    """Tries to powercycle a node.
448

449
    """
450
    return (self.request_body, {
451
      "node_name": self.items[0],
452
      "force": self.useForce(),
453
      })
454

    
455

    
456
class R_2_nodes_name_role(baserlib.OpcodeResource):
457
  """/2/nodes/[node_name]/role resource.
458

459
  """
460
  PUT_OPCODE = opcodes.OpNodeSetParams
461

    
462
  def GET(self):
463
    """Returns the current node role.
464

465
    @return: Node role
466

467
    """
468
    node_name = self.items[0]
469
    client = self.GetClient(query=True)
470
    result = client.QueryNodes(names=[node_name], fields=["role"],
471
                               use_locking=self.useLocking())
472

    
473
    return _NR_MAP[result[0][0]]
474

    
475
  def GetPutOpInput(self):
476
    """Sets the node role.
477

478
    """
479
    baserlib.CheckType(self.request_body, basestring, "Body contents")
480

    
481
    role = self.request_body
482

    
483
    if role == _NR_REGULAR:
484
      candidate = False
485
      offline = False
486
      drained = False
487

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

    
492
    elif role == _NR_DRAINED:
493
      drained = True
494
      candidate = offline = None
495

    
496
    elif role == _NR_OFFLINE:
497
      offline = True
498
      candidate = drained = None
499

    
500
    else:
501
      raise http.HttpBadRequest("Can't set '%s' role" % role)
502

    
503
    assert len(self.items) == 1
504

    
505
    return ({}, {
506
      "node_name": self.items[0],
507
      "master_candidate": candidate,
508
      "offline": offline,
509
      "drained": drained,
510
      "force": self.useForce(),
511
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
512
      })
513

    
514

    
515
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
516
  """/2/nodes/[node_name]/evacuate resource.
517

518
  """
519
  POST_OPCODE = opcodes.OpNodeEvacuate
520

    
521
  def GetPostOpInput(self):
522
    """Evacuate all instances off a node.
523

524
    """
525
    return (self.request_body, {
526
      "node_name": self.items[0],
527
      "dry_run": self.dryRun(),
528
      })
529

    
530

    
531
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
532
  """/2/nodes/[node_name]/migrate resource.
533

534
  """
535
  POST_OPCODE = opcodes.OpNodeMigrate
536

    
537
  def GetPostOpInput(self):
538
    """Migrate all primary instances from a node.
539

540
    """
541
    if self.queryargs:
542
      # Support old-style requests
543
      if "live" in self.queryargs and "mode" in self.queryargs:
544
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
545
                                  " be passed")
546

    
547
      if "live" in self.queryargs:
548
        if self._checkIntVariable("live", default=1):
549
          mode = constants.HT_MIGRATION_LIVE
550
        else:
551
          mode = constants.HT_MIGRATION_NONLIVE
552
      else:
553
        mode = self._checkStringVariable("mode", default=None)
554

    
555
      data = {
556
        "mode": mode,
557
        }
558
    else:
559
      data = self.request_body
560

    
561
    return (data, {
562
      "node_name": self.items[0],
563
      })
564

    
565

    
566
class R_2_nodes_name_modify(baserlib.OpcodeResource):
567
  """/2/nodes/[node_name]/modify resource.
568

569
  """
570
  POST_OPCODE = opcodes.OpNodeSetParams
571

    
572
  def GetPostOpInput(self):
573
    """Changes parameters of a node.
574

575
    """
576
    assert len(self.items) == 1
577

    
578
    return (self.request_body, {
579
      "node_name": self.items[0],
580
      })
581

    
582

    
583
class R_2_nodes_name_storage(baserlib.OpcodeResource):
584
  """/2/nodes/[node_name]/storage resource.
585

586
  """
587
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
588
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
589
  GET_OPCODE = opcodes.OpNodeQueryStorage
590

    
591
  def GetGetOpInput(self):
592
    """List storage available on a node.
593

594
    """
595
    storage_type = self._checkStringVariable("storage_type", None)
596
    output_fields = self._checkStringVariable("output_fields", None)
597

    
598
    if not output_fields:
599
      raise http.HttpBadRequest("Missing the required 'output_fields'"
600
                                " parameter")
601

    
602
    return ({}, {
603
      "nodes": [self.items[0]],
604
      "storage_type": storage_type,
605
      "output_fields": output_fields.split(","),
606
      })
607

    
608

    
609
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
610
  """/2/nodes/[node_name]/storage/modify resource.
611

612
  """
613
  PUT_OPCODE = opcodes.OpNodeModifyStorage
614

    
615
  def GetPutOpInput(self):
616
    """Modifies a storage volume on a node.
617

618
    """
619
    storage_type = self._checkStringVariable("storage_type", None)
620
    name = self._checkStringVariable("name", None)
621

    
622
    if not name:
623
      raise http.HttpBadRequest("Missing the required 'name'"
624
                                " parameter")
625

    
626
    changes = {}
627

    
628
    if "allocatable" in self.queryargs:
629
      changes[constants.SF_ALLOCATABLE] = \
630
        bool(self._checkIntVariable("allocatable", default=1))
631

    
632
    return ({}, {
633
      "node_name": self.items[0],
634
      "storage_type": storage_type,
635
      "name": name,
636
      "changes": changes,
637
      })
638

    
639

    
640
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
641
  """/2/nodes/[node_name]/storage/repair resource.
642

643
  """
644
  PUT_OPCODE = opcodes.OpRepairNodeStorage
645

    
646
  def GetPutOpInput(self):
647
    """Repairs a storage volume on a node.
648

649
    """
650
    storage_type = self._checkStringVariable("storage_type", None)
651
    name = self._checkStringVariable("name", None)
652
    if not name:
653
      raise http.HttpBadRequest("Missing the required 'name'"
654
                                " parameter")
655

    
656
    return ({}, {
657
      "node_name": self.items[0],
658
      "storage_type": storage_type,
659
      "name": name,
660
      })
661

    
662

    
663
class R_2_networks(baserlib.OpcodeResource):
664
  """/2/networks resource.
665

666
  """
667
  GET_OPCODE = opcodes.OpNetworkQuery
668
  POST_OPCODE = opcodes.OpNetworkAdd
669
  POST_RENAME = {
670
    "name": "network_name",
671
    }
672

    
673
  def GetPostOpInput(self):
674
    """Create a network.
675

676
    """
677
    assert not self.items
678
    return (self.request_body, {
679
      "dry_run": self.dryRun(),
680
      })
681

    
682
  def GET(self):
683
    """Returns a list of all networks.
684

685
    """
686
    client = self.GetClient(query=True)
687

    
688
    if self.useBulk():
689
      bulkdata = client.QueryNetworks([], NET_FIELDS, False)
690
      return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
691
    else:
692
      data = client.QueryNetworks([], ["name"], False)
693
      networknames = [row[0] for row in data]
694
      return baserlib.BuildUriList(networknames, "/2/networks/%s",
695
                                   uri_fields=("name", "uri"))
696

    
697

    
698
class R_2_networks_name(baserlib.OpcodeResource):
699
  """/2/networks/[network_name] resource.
700

701
  """
702
  DELETE_OPCODE = opcodes.OpNetworkRemove
703

    
704
  def GET(self):
705
    """Send information about a network.
706

707
    """
708
    network_name = self.items[0]
709
    client = self.GetClient(query=True)
710

    
711
    result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
712
                                            names=[network_name],
713
                                            fields=NET_FIELDS,
714
                                            use_locking=self.useLocking())
715

    
716
    return baserlib.MapFields(NET_FIELDS, result[0])
717

    
718
  def GetDeleteOpInput(self):
719
    """Delete a network.
720

721
    """
722
    assert len(self.items) == 1
723
    return (self.request_body, {
724
      "network_name": self.items[0],
725
      "dry_run": self.dryRun(),
726
      })
727

    
728

    
729
class R_2_networks_name_connect(baserlib.OpcodeResource):
730
  """/2/networks/[network_name]/connect resource.
731

732
  """
733
  PUT_OPCODE = opcodes.OpNetworkConnect
734

    
735
  def GetPutOpInput(self):
736
    """Changes some parameters of node group.
737

738
    """
739
    assert self.items
740
    return (self.request_body, {
741
      "network_name": self.items[0],
742
      "dry_run": self.dryRun(),
743
      })
744

    
745

    
746
class R_2_networks_name_disconnect(baserlib.OpcodeResource):
747
  """/2/networks/[network_name]/disconnect resource.
748

749
  """
750
  PUT_OPCODE = opcodes.OpNetworkDisconnect
751

    
752
  def GetPutOpInput(self):
753
    """Changes some parameters of node group.
754

755
    """
756
    assert self.items
757
    return (self.request_body, {
758
      "network_name": self.items[0],
759
      "dry_run": self.dryRun(),
760
      })
761

    
762

    
763
class R_2_networks_name_modify(baserlib.OpcodeResource):
764
  """/2/networks/[network_name]/modify resource.
765

766
  """
767
  PUT_OPCODE = opcodes.OpNetworkSetParams
768

    
769
  def GetPutOpInput(self):
770
    """Changes some parameters of network.
771

772
    """
773
    assert self.items
774
    return (self.request_body, {
775
      "network_name": self.items[0],
776
      })
777

    
778

    
779
class R_2_groups(baserlib.OpcodeResource):
780
  """/2/groups resource.
781

782
  """
783
  GET_OPCODE = opcodes.OpGroupQuery
784
  POST_OPCODE = opcodes.OpGroupAdd
785
  POST_RENAME = {
786
    "name": "group_name",
787
    }
788

    
789
  def GetPostOpInput(self):
790
    """Create a node group.
791

792

793
    """
794
    assert not self.items
795
    return (self.request_body, {
796
      "dry_run": self.dryRun(),
797
      })
798

    
799
  def GET(self):
800
    """Returns a list of all node groups.
801

802
    """
803
    client = self.GetClient(query=True)
804

    
805
    if self.useBulk():
806
      bulkdata = client.QueryGroups([], G_FIELDS, False)
807
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
808
    else:
809
      data = client.QueryGroups([], ["name"], False)
810
      groupnames = [row[0] for row in data]
811
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
812
                                   uri_fields=("name", "uri"))
813

    
814

    
815
class R_2_groups_name(baserlib.OpcodeResource):
816
  """/2/groups/[group_name] resource.
817

818
  """
819
  DELETE_OPCODE = opcodes.OpGroupRemove
820

    
821
  def GET(self):
822
    """Send information about a node group.
823

824
    """
825
    group_name = self.items[0]
826
    client = self.GetClient(query=True)
827

    
828
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
829
                                            names=[group_name], fields=G_FIELDS,
830
                                            use_locking=self.useLocking())
831

    
832
    return baserlib.MapFields(G_FIELDS, result[0])
833

    
834
  def GetDeleteOpInput(self):
835
    """Delete a node group.
836

837
    """
838
    assert len(self.items) == 1
839
    return ({}, {
840
      "group_name": self.items[0],
841
      "dry_run": self.dryRun(),
842
      })
843

    
844

    
845
class R_2_groups_name_modify(baserlib.OpcodeResource):
846
  """/2/groups/[group_name]/modify resource.
847

848
  """
849
  PUT_OPCODE = opcodes.OpGroupSetParams
850

    
851
  def GetPutOpInput(self):
852
    """Changes some parameters of node group.
853

854
    """
855
    assert self.items
856
    return (self.request_body, {
857
      "group_name": self.items[0],
858
      })
859

    
860

    
861
class R_2_groups_name_rename(baserlib.OpcodeResource):
862
  """/2/groups/[group_name]/rename resource.
863

864
  """
865
  PUT_OPCODE = opcodes.OpGroupRename
866

    
867
  def GetPutOpInput(self):
868
    """Changes the name of a node group.
869

870
    """
871
    assert len(self.items) == 1
872
    return (self.request_body, {
873
      "group_name": self.items[0],
874
      "dry_run": self.dryRun(),
875
      })
876

    
877

    
878
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
879
  """/2/groups/[group_name]/assign-nodes resource.
880

881
  """
882
  PUT_OPCODE = opcodes.OpGroupAssignNodes
883

    
884
  def GetPutOpInput(self):
885
    """Assigns nodes to a group.
886

887
    """
888
    assert len(self.items) == 1
889
    return (self.request_body, {
890
      "group_name": self.items[0],
891
      "dry_run": self.dryRun(),
892
      "force": self.useForce(),
893
      })
894

    
895

    
896
def _ConvertUsbDevices(data):
897
  """Convert in place the usb_devices string to the proper format.
898

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

906
  """
907
  try:
908
    hvparams = data["hvparams"]
909
    usb_devices = hvparams[constants.HV_USB_DEVICES]
910
    hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
911
    data["hvparams"] = hvparams
912
  except KeyError:
913
    #No usb_devices, no modification required
914
    pass
915

    
916

    
917
class R_2_instances(baserlib.OpcodeResource):
918
  """/2/instances resource.
919

920
  """
921
  GET_OPCODE = opcodes.OpInstanceQuery
922
  POST_OPCODE = opcodes.OpInstanceCreate
923
  POST_RENAME = {
924
    "os": "os_type",
925
    "name": "instance_name",
926
    }
927

    
928
  def GET(self):
929
    """Returns a list of all available instances.
930

931
    """
932
    client = self.GetClient()
933

    
934
    use_locking = self.useLocking()
935
    if self.useBulk():
936
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
937
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
938
    else:
939
      instancesdata = client.QueryInstances([], ["name"], use_locking)
940
      instanceslist = [row[0] for row in instancesdata]
941
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
942
                                   uri_fields=("id", "uri"))
943

    
944
  def GetPostOpInput(self):
945
    """Create an instance.
946

947
    @return: a job id
948

949
    """
950
    baserlib.CheckType(self.request_body, dict, "Body contents")
951

    
952
    # Default to request data version 0
953
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
954

    
955
    if data_version == 0:
956
      raise http.HttpBadRequest("Instance creation request version 0 is no"
957
                                " longer supported")
958
    elif data_version != 1:
959
      raise http.HttpBadRequest("Unsupported request data version %s" %
960
                                data_version)
961

    
962
    data = self.request_body.copy()
963
    # Remove "__version__"
964
    data.pop(_REQ_DATA_VERSION, None)
965

    
966
    _ConvertUsbDevices(data)
967

    
968
    return (data, {
969
      "dry_run": self.dryRun(),
970
      })
971

    
972

    
973
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
974
  """/2/instances-multi-alloc resource.
975

976
  """
977
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
978

    
979
  def GetPostOpInput(self):
980
    """Try to allocate multiple instances.
981

982
    @return: A dict with submitted jobs, allocatable instances and failed
983
             allocations
984

985
    """
986
    if "instances" not in self.request_body:
987
      raise http.HttpBadRequest("Request is missing required 'instances' field"
988
                                " in body")
989

    
990
    # Unlike most other RAPI calls, this one is composed of individual opcodes,
991
    # and we have to do the filling ourselves
992
    OPCODE_RENAME = {
993
      "os": "os_type",
994
      "name": "instance_name",
995
    }
996

    
997
    body = objects.FillDict(self.request_body, {
998
      "instances": [
999
        baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
1000
                            rename=OPCODE_RENAME)
1001
        for inst in self.request_body["instances"]
1002
        ],
1003
      })
1004

    
1005
    return (body, {
1006
      "dry_run": self.dryRun(),
1007
      })
1008

    
1009

    
1010
class R_2_instances_name(baserlib.OpcodeResource):
1011
  """/2/instances/[instance_name] resource.
1012

1013
  """
1014
  GET_OPCODE = opcodes.OpInstanceQuery
1015
  DELETE_OPCODE = opcodes.OpInstanceRemove
1016

    
1017
  def GET(self):
1018
    """Send information about an instance.
1019

1020
    """
1021
    client = self.GetClient()
1022
    instance_name = self.items[0]
1023

    
1024
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1025
                                            names=[instance_name],
1026
                                            fields=I_FIELDS,
1027
                                            use_locking=self.useLocking())
1028

    
1029
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1030

    
1031
  def GetDeleteOpInput(self):
1032
    """Delete an instance.
1033

1034
    """
1035
    assert len(self.items) == 1
1036
    return (self.request_body, {
1037
      "instance_name": self.items[0],
1038
      "ignore_failures": False,
1039
      "dry_run": self.dryRun(),
1040
      })
1041

    
1042

    
1043
class R_2_instances_name_info(baserlib.OpcodeResource):
1044
  """/2/instances/[instance_name]/info resource.
1045

1046
  """
1047
  GET_OPCODE = opcodes.OpInstanceQueryData
1048

    
1049
  def GetGetOpInput(self):
1050
    """Request detailed instance information.
1051

1052
    """
1053
    assert len(self.items) == 1
1054
    return ({}, {
1055
      "instances": [self.items[0]],
1056
      "static": bool(self._checkIntVariable("static", default=0)),
1057
      })
1058

    
1059

    
1060
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1061
  """/2/instances/[instance_name]/reboot resource.
1062

1063
  Implements an instance reboot.
1064

1065
  """
1066
  POST_OPCODE = opcodes.OpInstanceReboot
1067

    
1068
  def GetPostOpInput(self):
1069
    """Reboot an instance.
1070

1071
    The URI takes type=[hard|soft|full] and
1072
    ignore_secondaries=[False|True] parameters.
1073

1074
    """
1075
    return (self.request_body, {
1076
      "instance_name": self.items[0],
1077
      "reboot_type":
1078
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1079
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1080
      "dry_run": self.dryRun(),
1081
      })
1082

    
1083

    
1084
class R_2_instances_name_startup(baserlib.OpcodeResource):
1085
  """/2/instances/[instance_name]/startup resource.
1086

1087
  Implements an instance startup.
1088

1089
  """
1090
  PUT_OPCODE = opcodes.OpInstanceStartup
1091

    
1092
  def GetPutOpInput(self):
1093
    """Startup an instance.
1094

1095
    The URI takes force=[False|True] parameter to start the instance
1096
    if even if secondary disks are failing.
1097

1098
    """
1099
    return ({}, {
1100
      "instance_name": self.items[0],
1101
      "force": self.useForce(),
1102
      "dry_run": self.dryRun(),
1103
      "no_remember": bool(self._checkIntVariable("no_remember")),
1104
      })
1105

    
1106

    
1107
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1108
  """/2/instances/[instance_name]/shutdown resource.
1109

1110
  Implements an instance shutdown.
1111

1112
  """
1113
  PUT_OPCODE = opcodes.OpInstanceShutdown
1114

    
1115
  def GetPutOpInput(self):
1116
    """Shutdown an instance.
1117

1118
    """
1119
    return (self.request_body, {
1120
      "instance_name": self.items[0],
1121
      "no_remember": bool(self._checkIntVariable("no_remember")),
1122
      "dry_run": self.dryRun(),
1123
      })
1124

    
1125

    
1126
def _ParseInstanceReinstallRequest(name, data):
1127
  """Parses a request for reinstalling an instance.
1128

1129
  """
1130
  if not isinstance(data, dict):
1131
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1132

    
1133
  ostype = baserlib.CheckParameter(data, "os", default=None)
1134
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1135
                                  default=True)
1136
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1137

    
1138
  ops = [
1139
    opcodes.OpInstanceShutdown(instance_name=name),
1140
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1141
                                osparams=osparams),
1142
    ]
1143

    
1144
  if start:
1145
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1146

    
1147
  return ops
1148

    
1149

    
1150
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1151
  """/2/instances/[instance_name]/reinstall resource.
1152

1153
  Implements an instance reinstall.
1154

1155
  """
1156
  POST_OPCODE = opcodes.OpInstanceReinstall
1157

    
1158
  def POST(self):
1159
    """Reinstall an instance.
1160

1161
    The URI takes os=name and nostartup=[0|1] optional
1162
    parameters. By default, the instance will be started
1163
    automatically.
1164

1165
    """
1166
    if self.request_body:
1167
      if self.queryargs:
1168
        raise http.HttpBadRequest("Can't combine query and body parameters")
1169

    
1170
      body = self.request_body
1171
    elif self.queryargs:
1172
      # Legacy interface, do not modify/extend
1173
      body = {
1174
        "os": self._checkStringVariable("os"),
1175
        "start": not self._checkIntVariable("nostartup"),
1176
        }
1177
    else:
1178
      body = {}
1179

    
1180
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1181

    
1182
    return self.SubmitJob(ops)
1183

    
1184

    
1185
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1186
  """/2/instances/[instance_name]/replace-disks resource.
1187

1188
  """
1189
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1190

    
1191
  def GetPostOpInput(self):
1192
    """Replaces disks on an instance.
1193

1194
    """
1195
    static = {
1196
      "instance_name": self.items[0],
1197
      }
1198

    
1199
    if self.request_body:
1200
      data = self.request_body
1201
    elif self.queryargs:
1202
      # Legacy interface, do not modify/extend
1203
      data = {
1204
        "remote_node": self._checkStringVariable("remote_node", default=None),
1205
        "mode": self._checkStringVariable("mode", default=None),
1206
        "disks": self._checkStringVariable("disks", default=None),
1207
        "iallocator": self._checkStringVariable("iallocator", default=None),
1208
        }
1209
    else:
1210
      data = {}
1211

    
1212
    # Parse disks
1213
    try:
1214
      raw_disks = data.pop("disks")
1215
    except KeyError:
1216
      pass
1217
    else:
1218
      if raw_disks:
1219
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1220
          data["disks"] = raw_disks
1221
        else:
1222
          # Backwards compatibility for strings of the format "1, 2, 3"
1223
          try:
1224
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1225
          except (TypeError, ValueError), err:
1226
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1227

    
1228
    return (data, static)
1229

    
1230

    
1231
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1232
  """/2/instances/[instance_name]/activate-disks resource.
1233

1234
  """
1235
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1236

    
1237
  def GetPutOpInput(self):
1238
    """Activate disks for an instance.
1239

1240
    The URI might contain ignore_size to ignore current recorded size.
1241

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

    
1248

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

1252
  """
1253
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1254

    
1255
  def GetPutOpInput(self):
1256
    """Deactivate disks for an instance.
1257

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

    
1263

    
1264
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1265
  """/2/instances/[instance_name]/recreate-disks resource.
1266

1267
  """
1268
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1269

    
1270
  def GetPostOpInput(self):
1271
    """Recreate disks for an instance.
1272

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

    
1278

    
1279
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1280
  """/2/instances/[instance_name]/prepare-export resource.
1281

1282
  """
1283
  PUT_OPCODE = opcodes.OpBackupPrepare
1284

    
1285
  def GetPutOpInput(self):
1286
    """Prepares an export for an instance.
1287

1288
    """
1289
    return ({}, {
1290
      "instance_name": self.items[0],
1291
      "mode": self._checkStringVariable("mode"),
1292
      })
1293

    
1294

    
1295
class R_2_instances_name_export(baserlib.OpcodeResource):
1296
  """/2/instances/[instance_name]/export resource.
1297

1298
  """
1299
  PUT_OPCODE = opcodes.OpBackupExport
1300
  PUT_RENAME = {
1301
    "destination": "target_node",
1302
    }
1303

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

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

    
1312

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

1316
  """
1317
  PUT_OPCODE = opcodes.OpInstanceMigrate
1318

    
1319
  def GetPutOpInput(self):
1320
    """Migrates an instance.
1321

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

    
1327

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

1331
  """
1332
  PUT_OPCODE = opcodes.OpInstanceFailover
1333

    
1334
  def GetPutOpInput(self):
1335
    """Does a failover 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_rename(baserlib.OpcodeResource):
1344
  """/2/instances/[instance_name]/rename resource.
1345

1346
  """
1347
  PUT_OPCODE = opcodes.OpInstanceRename
1348

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

1352
    """
1353
    return (self.request_body, {
1354
      "instance_name": self.items[0],
1355
      })
1356

    
1357

    
1358
class R_2_instances_name_modify(baserlib.OpcodeResource):
1359
  """/2/instances/[instance_name]/modify resource.
1360

1361
  """
1362
  PUT_OPCODE = opcodes.OpInstanceSetParams
1363

    
1364
  def GetPutOpInput(self):
1365
    """Changes parameters of an instance.
1366

1367
    """
1368
    data = self.request_body.copy()
1369
    _ConvertUsbDevices(data)
1370

    
1371
    return (data, {
1372
      "instance_name": self.items[0],
1373
      })
1374

    
1375

    
1376
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1377
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1378

1379
  """
1380
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1381

    
1382
  def GetPostOpInput(self):
1383
    """Increases the size of an instance disk.
1384

1385
    """
1386
    return (self.request_body, {
1387
      "instance_name": self.items[0],
1388
      "disk": int(self.items[1]),
1389
      })
1390

    
1391

    
1392
class R_2_instances_name_console(baserlib.ResourceBase):
1393
  """/2/instances/[instance_name]/console resource.
1394

1395
  """
1396
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1397
  GET_OPCODE = opcodes.OpInstanceConsole
1398

    
1399
  def GET(self):
1400
    """Request information for connecting to instance's console.
1401

1402
    @return: Serialized instance console description, see
1403
             L{objects.InstanceConsole}
1404

1405
    """
1406
    client = self.GetClient()
1407

    
1408
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1409

    
1410
    if console is None:
1411
      raise http.HttpServiceUnavailable("Instance console unavailable")
1412

    
1413
    assert isinstance(console, dict)
1414
    return console
1415

    
1416

    
1417
def _GetQueryFields(args):
1418
  """Tries to extract C{fields} query parameter.
1419

1420
  @type args: dictionary
1421
  @rtype: list of string
1422
  @raise http.HttpBadRequest: When parameter can't be found
1423

1424
  """
1425
  try:
1426
    fields = args["fields"]
1427
  except KeyError:
1428
    raise http.HttpBadRequest("Missing 'fields' query argument")
1429

    
1430
  return _SplitQueryFields(fields[0])
1431

    
1432

    
1433
def _SplitQueryFields(fields):
1434
  """Splits fields as given for a query request.
1435

1436
  @type fields: string
1437
  @rtype: list of string
1438

1439
  """
1440
  return [i.strip() for i in fields.split(",")]
1441

    
1442

    
1443
class R_2_query(baserlib.ResourceBase):
1444
  """/2/query/[resource] resource.
1445

1446
  """
1447
  # Results might contain sensitive information
1448
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1449
  PUT_ACCESS = GET_ACCESS
1450
  GET_OPCODE = opcodes.OpQuery
1451
  PUT_OPCODE = opcodes.OpQuery
1452

    
1453
  def _Query(self, fields, qfilter):
1454
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1455

    
1456
  def GET(self):
1457
    """Returns resource information.
1458

1459
    @return: Query result, see L{objects.QueryResponse}
1460

1461
    """
1462
    return self._Query(_GetQueryFields(self.queryargs), None)
1463

    
1464
  def PUT(self):
1465
    """Submits job querying for resources.
1466

1467
    @return: Query result, see L{objects.QueryResponse}
1468

1469
    """
1470
    body = self.request_body
1471

    
1472
    baserlib.CheckType(body, dict, "Body contents")
1473

    
1474
    try:
1475
      fields = body["fields"]
1476
    except KeyError:
1477
      fields = _GetQueryFields(self.queryargs)
1478

    
1479
    qfilter = body.get("qfilter", None)
1480
    # TODO: remove this after 2.7
1481
    if qfilter is None:
1482
      qfilter = body.get("filter", None)
1483

    
1484
    return self._Query(fields, qfilter)
1485

    
1486

    
1487
class R_2_query_fields(baserlib.ResourceBase):
1488
  """/2/query/[resource]/fields resource.
1489

1490
  """
1491
  GET_OPCODE = opcodes.OpQueryFields
1492

    
1493
  def GET(self):
1494
    """Retrieves list of available fields for a resource.
1495

1496
    @return: List of serialized L{objects.QueryFieldDefinition}
1497

1498
    """
1499
    try:
1500
      raw_fields = self.queryargs["fields"]
1501
    except KeyError:
1502
      fields = None
1503
    else:
1504
      fields = _SplitQueryFields(raw_fields[0])
1505

    
1506
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1507

    
1508

    
1509
class _R_Tags(baserlib.OpcodeResource):
1510
  """Quasiclass for tagging resources.
1511

1512
  Manages tags. When inheriting this class you must define the
1513
  TAG_LEVEL for it.
1514

1515
  """
1516
  TAG_LEVEL = None
1517
  GET_OPCODE = opcodes.OpTagsGet
1518
  PUT_OPCODE = opcodes.OpTagsSet
1519
  DELETE_OPCODE = opcodes.OpTagsDel
1520

    
1521
  def __init__(self, items, queryargs, req, **kwargs):
1522
    """A tag resource constructor.
1523

1524
    We have to override the default to sort out cluster naming case.
1525

1526
    """
1527
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1528

    
1529
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1530
      self.name = None
1531
    else:
1532
      self.name = items[0]
1533

    
1534
  def GET(self):
1535
    """Returns a list of tags.
1536

1537
    Example: ["tag1", "tag2", "tag3"]
1538

1539
    """
1540
    kind = self.TAG_LEVEL
1541

    
1542
    if kind in (constants.TAG_INSTANCE,
1543
                constants.TAG_NODEGROUP,
1544
                constants.TAG_NODE,
1545
                constants.TAG_NETWORK):
1546
      if not self.name:
1547
        raise http.HttpBadRequest("Missing name on tag request")
1548

    
1549
      cl = self.GetClient(query=True)
1550
      tags = list(cl.QueryTags(kind, self.name))
1551

    
1552
    elif kind == constants.TAG_CLUSTER:
1553
      assert not self.name
1554
      # TODO: Use query API?
1555
      ssc = ssconf.SimpleStore()
1556
      tags = ssc.GetClusterTags()
1557

    
1558
    else:
1559
      raise http.HttpBadRequest("Unhandled tag type!")
1560

    
1561
    return list(tags)
1562

    
1563
  def GetPutOpInput(self):
1564
    """Add a set of tags.
1565

1566
    The request as a list of strings should be PUT to this URI. And
1567
    you'll have back a job id.
1568

1569
    """
1570
    return ({}, {
1571
      "kind": self.TAG_LEVEL,
1572
      "name": self.name,
1573
      "tags": self.queryargs.get("tag", []),
1574
      "dry_run": self.dryRun(),
1575
      })
1576

    
1577
  def GetDeleteOpInput(self):
1578
    """Delete a tag.
1579

1580
    In order to delete a set of tags, the DELETE
1581
    request should be addressed to URI like:
1582
    /tags?tag=[tag]&tag=[tag]
1583

1584
    """
1585
    # Re-use code
1586
    return self.GetPutOpInput()
1587

    
1588

    
1589
class R_2_instances_name_tags(_R_Tags):
1590
  """ /2/instances/[instance_name]/tags resource.
1591

1592
  Manages per-instance tags.
1593

1594
  """
1595
  TAG_LEVEL = constants.TAG_INSTANCE
1596

    
1597

    
1598
class R_2_nodes_name_tags(_R_Tags):
1599
  """ /2/nodes/[node_name]/tags resource.
1600

1601
  Manages per-node tags.
1602

1603
  """
1604
  TAG_LEVEL = constants.TAG_NODE
1605

    
1606

    
1607
class R_2_groups_name_tags(_R_Tags):
1608
  """ /2/groups/[group_name]/tags resource.
1609

1610
  Manages per-nodegroup tags.
1611

1612
  """
1613
  TAG_LEVEL = constants.TAG_NODEGROUP
1614

    
1615

    
1616
class R_2_networks_name_tags(_R_Tags):
1617
  """ /2/networks/[network_name]/tags resource.
1618

1619
  Manages per-network tags.
1620

1621
  """
1622
  TAG_LEVEL = constants.TAG_NETWORK
1623

    
1624

    
1625
class R_2_tags(_R_Tags):
1626
  """ /2/tags resource.
1627

1628
  Manages cluster tags.
1629

1630
  """
1631
  TAG_LEVEL = constants.TAG_CLUSTER