Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 31d3b918

History | View | Annotate | Download (39.8 kB)

1
#
2
#
3

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

    
21

    
22
"""Remote API resource implementations.
23

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

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

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

35
Quoting from RFC2616, section 9.6::
36

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

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

52
"""
53

    
54
# pylint: disable=C0103
55

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

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

    
69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
169

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

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

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

    
182
  return inst
183

    
184

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

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

193
    """
194
    return None
195

    
196

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

200
  """
201

    
202

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

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

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

214
    """
215
    return constants.RAPI_VERSION
216

    
217

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

221
  """
222
  GET_OPCODE = opcodes.OpClusterQuery
223

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

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

    
231

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

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

240
    """
241
    return list(ALL_FEATURES)
242

    
243

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

247
  """
248
  GET_OPCODE = opcodes.OpOsDiagnose
249

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

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

255
    Example: ["debian-etch"]
256

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

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

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

    
272
    return os_names
273

    
274

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

278
  """
279
  PUT_OPCODE = opcodes.OpClusterRedistConf
280

    
281

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

285
  """
286
  PUT_OPCODE = opcodes.OpClusterSetParams
287

    
288

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

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

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

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

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

    
309

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

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

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

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

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

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

    
341

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

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

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

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

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

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

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

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

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

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

    
383
    (job_info, log_entries) = result
384

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

    
390

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

394
  """
395

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

399
    """
400
    client = self.GetClient(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

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

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

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

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

    
430

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

434
  """
435
  POST_OPCODE = opcodes.OpNodePowercycle
436

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

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

    
446

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

450
  """
451
  PUT_OPCODE = opcodes.OpNodeSetParams
452

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

456
    @return: Node role
457

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

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

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

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

    
472
    role = self.request_body
473

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

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

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

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

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

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

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

    
505

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

509
  """
510
  POST_OPCODE = opcodes.OpNodeEvacuate
511

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

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

    
521

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

525
  """
526
  POST_OPCODE = opcodes.OpNodeMigrate
527

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

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

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

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

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

    
556

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

560
  """
561
  POST_OPCODE = opcodes.OpNodeSetParams
562

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

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

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

    
573

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

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

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

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

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

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

    
599

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

603
  """
604
  PUT_OPCODE = opcodes.OpNodeModifyStorage
605

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

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

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

    
617
    changes = {}
618

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

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

    
630

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

634
  """
635
  PUT_OPCODE = opcodes.OpRepairNodeStorage
636

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

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

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

    
653

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

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

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

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

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

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

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

    
687

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

691
  """
692
  DELETE_OPCODE = opcodes.OpNetworkRemove
693

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

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

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

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

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

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

    
718

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

722
  """
723
  PUT_OPCODE = opcodes.OpNetworkConnect
724

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

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

    
735

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

739
  """
740
  PUT_OPCODE = opcodes.OpNetworkDisconnect
741

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

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

    
752

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

756
  """
757
  PUT_OPCODE = opcodes.OpNetworkSetParams
758

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

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

    
768

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

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

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

781

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

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

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

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

    
803

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

807
  """
808
  DELETE_OPCODE = opcodes.OpGroupRemove
809

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

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

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

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

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

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

    
833

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

837
  """
838
  PUT_OPCODE = opcodes.OpGroupSetParams
839

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

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

    
849

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

853
  """
854
  PUT_OPCODE = opcodes.OpGroupRename
855

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

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

    
866

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

870
  """
871
  PUT_OPCODE = opcodes.OpGroupAssignNodes
872

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

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

    
884

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

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

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

    
905

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

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

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

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

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

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

935
    @return: a job id
936

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

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

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

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

    
954
    _ConvertUsbDevices(data)
955

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

    
960

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

964
  """
965
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
966

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

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

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

    
978
    op_id = {
979
      "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
980
      }
981
    body = objects.FillDict(self.request_body, {
982
      "instances": [objects.FillDict(inst, op_id)
983
                    for inst in self.request_body["instances"]],
984
      })
985

    
986
    return (body, {
987
      "dry_run": self.dryRun(),
988
      })
989

    
990

    
991
class R_2_instances_name(baserlib.OpcodeResource):
992
  """/2/instances/[instance_name] resource.
993

994
  """
995
  DELETE_OPCODE = opcodes.OpInstanceRemove
996

    
997
  def GET(self):
998
    """Send information about an instance.
999

1000
    """
1001
    client = self.GetClient(query=True)
1002
    instance_name = self.items[0]
1003

    
1004
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1005
                                            names=[instance_name],
1006
                                            fields=I_FIELDS,
1007
                                            use_locking=self.useLocking())
1008

    
1009
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1010

    
1011
  def GetDeleteOpInput(self):
1012
    """Delete an instance.
1013

1014
    """
1015
    assert len(self.items) == 1
1016
    return (self.request_body, {
1017
      "instance_name": self.items[0],
1018
      "ignore_failures": False,
1019
      "dry_run": self.dryRun(),
1020
      })
1021

    
1022

    
1023
class R_2_instances_name_info(baserlib.OpcodeResource):
1024
  """/2/instances/[instance_name]/info resource.
1025

1026
  """
1027
  GET_OPCODE = opcodes.OpInstanceQueryData
1028

    
1029
  def GetGetOpInput(self):
1030
    """Request detailed instance information.
1031

1032
    """
1033
    assert len(self.items) == 1
1034
    return ({}, {
1035
      "instances": [self.items[0]],
1036
      "static": bool(self._checkIntVariable("static", default=0)),
1037
      })
1038

    
1039

    
1040
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1041
  """/2/instances/[instance_name]/reboot resource.
1042

1043
  Implements an instance reboot.
1044

1045
  """
1046
  POST_OPCODE = opcodes.OpInstanceReboot
1047

    
1048
  def GetPostOpInput(self):
1049
    """Reboot an instance.
1050

1051
    The URI takes type=[hard|soft|full] and
1052
    ignore_secondaries=[False|True] parameters.
1053

1054
    """
1055
    return (self.request_body, {
1056
      "instance_name": self.items[0],
1057
      "reboot_type":
1058
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1059
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1060
      "dry_run": self.dryRun(),
1061
      })
1062

    
1063

    
1064
class R_2_instances_name_startup(baserlib.OpcodeResource):
1065
  """/2/instances/[instance_name]/startup resource.
1066

1067
  Implements an instance startup.
1068

1069
  """
1070
  PUT_OPCODE = opcodes.OpInstanceStartup
1071

    
1072
  def GetPutOpInput(self):
1073
    """Startup an instance.
1074

1075
    The URI takes force=[False|True] parameter to start the instance
1076
    if even if secondary disks are failing.
1077

1078
    """
1079
    return ({}, {
1080
      "instance_name": self.items[0],
1081
      "force": self.useForce(),
1082
      "dry_run": self.dryRun(),
1083
      "no_remember": bool(self._checkIntVariable("no_remember")),
1084
      })
1085

    
1086

    
1087
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1088
  """/2/instances/[instance_name]/shutdown resource.
1089

1090
  Implements an instance shutdown.
1091

1092
  """
1093
  PUT_OPCODE = opcodes.OpInstanceShutdown
1094

    
1095
  def GetPutOpInput(self):
1096
    """Shutdown an instance.
1097

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

    
1105

    
1106
def _ParseInstanceReinstallRequest(name, data):
1107
  """Parses a request for reinstalling an instance.
1108

1109
  """
1110
  if not isinstance(data, dict):
1111
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1112

    
1113
  ostype = baserlib.CheckParameter(data, "os", default=None)
1114
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1115
                                  default=True)
1116
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1117

    
1118
  ops = [
1119
    opcodes.OpInstanceShutdown(instance_name=name),
1120
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1121
                                osparams=osparams),
1122
    ]
1123

    
1124
  if start:
1125
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1126

    
1127
  return ops
1128

    
1129

    
1130
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1131
  """/2/instances/[instance_name]/reinstall resource.
1132

1133
  Implements an instance reinstall.
1134

1135
  """
1136
  POST_OPCODE = opcodes.OpInstanceReinstall
1137

    
1138
  def POST(self):
1139
    """Reinstall an instance.
1140

1141
    The URI takes os=name and nostartup=[0|1] optional
1142
    parameters. By default, the instance will be started
1143
    automatically.
1144

1145
    """
1146
    if self.request_body:
1147
      if self.queryargs:
1148
        raise http.HttpBadRequest("Can't combine query and body parameters")
1149

    
1150
      body = self.request_body
1151
    elif self.queryargs:
1152
      # Legacy interface, do not modify/extend
1153
      body = {
1154
        "os": self._checkStringVariable("os"),
1155
        "start": not self._checkIntVariable("nostartup"),
1156
        }
1157
    else:
1158
      body = {}
1159

    
1160
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1161

    
1162
    return self.SubmitJob(ops)
1163

    
1164

    
1165
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1166
  """/2/instances/[instance_name]/replace-disks resource.
1167

1168
  """
1169
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1170

    
1171
  def GetPostOpInput(self):
1172
    """Replaces disks on an instance.
1173

1174
    """
1175
    static = {
1176
      "instance_name": self.items[0],
1177
      }
1178

    
1179
    if self.request_body:
1180
      data = self.request_body
1181
    elif self.queryargs:
1182
      # Legacy interface, do not modify/extend
1183
      data = {
1184
        "remote_node": self._checkStringVariable("remote_node", default=None),
1185
        "mode": self._checkStringVariable("mode", default=None),
1186
        "disks": self._checkStringVariable("disks", default=None),
1187
        "iallocator": self._checkStringVariable("iallocator", default=None),
1188
        }
1189
    else:
1190
      data = {}
1191

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

    
1208
    return (data, static)
1209

    
1210

    
1211
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1212
  """/2/instances/[instance_name]/activate-disks resource.
1213

1214
  """
1215
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1216

    
1217
  def GetPutOpInput(self):
1218
    """Activate disks for an instance.
1219

1220
    The URI might contain ignore_size to ignore current recorded size.
1221

1222
    """
1223
    return ({}, {
1224
      "instance_name": self.items[0],
1225
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
1226
      })
1227

    
1228

    
1229
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1230
  """/2/instances/[instance_name]/deactivate-disks resource.
1231

1232
  """
1233
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1234

    
1235
  def GetPutOpInput(self):
1236
    """Deactivate disks for an instance.
1237

1238
    """
1239
    return ({}, {
1240
      "instance_name": self.items[0],
1241
      })
1242

    
1243

    
1244
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1245
  """/2/instances/[instance_name]/recreate-disks resource.
1246

1247
  """
1248
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1249

    
1250
  def GetPostOpInput(self):
1251
    """Recreate disks for an instance.
1252

1253
    """
1254
    return ({}, {
1255
      "instance_name": self.items[0],
1256
      })
1257

    
1258

    
1259
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1260
  """/2/instances/[instance_name]/prepare-export resource.
1261

1262
  """
1263
  PUT_OPCODE = opcodes.OpBackupPrepare
1264

    
1265
  def GetPutOpInput(self):
1266
    """Prepares an export for an instance.
1267

1268
    """
1269
    return ({}, {
1270
      "instance_name": self.items[0],
1271
      "mode": self._checkStringVariable("mode"),
1272
      })
1273

    
1274

    
1275
class R_2_instances_name_export(baserlib.OpcodeResource):
1276
  """/2/instances/[instance_name]/export resource.
1277

1278
  """
1279
  PUT_OPCODE = opcodes.OpBackupExport
1280
  PUT_RENAME = {
1281
    "destination": "target_node",
1282
    }
1283

    
1284
  def GetPutOpInput(self):
1285
    """Exports an instance.
1286

1287
    """
1288
    return (self.request_body, {
1289
      "instance_name": self.items[0],
1290
      })
1291

    
1292

    
1293
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1294
  """/2/instances/[instance_name]/migrate resource.
1295

1296
  """
1297
  PUT_OPCODE = opcodes.OpInstanceMigrate
1298

    
1299
  def GetPutOpInput(self):
1300
    """Migrates an instance.
1301

1302
    """
1303
    return (self.request_body, {
1304
      "instance_name": self.items[0],
1305
      })
1306

    
1307

    
1308
class R_2_instances_name_failover(baserlib.OpcodeResource):
1309
  """/2/instances/[instance_name]/failover resource.
1310

1311
  """
1312
  PUT_OPCODE = opcodes.OpInstanceFailover
1313

    
1314
  def GetPutOpInput(self):
1315
    """Does a failover of an instance.
1316

1317
    """
1318
    return (self.request_body, {
1319
      "instance_name": self.items[0],
1320
      })
1321

    
1322

    
1323
class R_2_instances_name_rename(baserlib.OpcodeResource):
1324
  """/2/instances/[instance_name]/rename resource.
1325

1326
  """
1327
  PUT_OPCODE = opcodes.OpInstanceRename
1328

    
1329
  def GetPutOpInput(self):
1330
    """Changes the name of an instance.
1331

1332
    """
1333
    return (self.request_body, {
1334
      "instance_name": self.items[0],
1335
      })
1336

    
1337

    
1338
class R_2_instances_name_modify(baserlib.OpcodeResource):
1339
  """/2/instances/[instance_name]/modify resource.
1340

1341
  """
1342
  PUT_OPCODE = opcodes.OpInstanceSetParams
1343

    
1344
  def GetPutOpInput(self):
1345
    """Changes parameters of an instance.
1346

1347
    """
1348
    data = self.request_body.copy()
1349
    _ConvertUsbDevices(data)
1350

    
1351
    return (data, {
1352
      "instance_name": self.items[0],
1353
      })
1354

    
1355

    
1356
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1357
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1358

1359
  """
1360
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1361

    
1362
  def GetPostOpInput(self):
1363
    """Increases the size of an instance disk.
1364

1365
    """
1366
    return (self.request_body, {
1367
      "instance_name": self.items[0],
1368
      "disk": int(self.items[1]),
1369
      })
1370

    
1371

    
1372
class R_2_instances_name_console(baserlib.ResourceBase):
1373
  """/2/instances/[instance_name]/console resource.
1374

1375
  """
1376
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1377
  GET_OPCODE = opcodes.OpInstanceConsole
1378

    
1379
  def GET(self):
1380
    """Request information for connecting to instance's console.
1381

1382
    @return: Serialized instance console description, see
1383
             L{objects.InstanceConsole}
1384

1385
    """
1386
    instance_name = self.items[0]
1387
    client = self.GetClient(query=True)
1388

    
1389
    ((console, oper_state), ) = \
1390
        client.QueryInstances([instance_name], ["console", "oper_state"], False)
1391

    
1392
    if not oper_state:
1393
      raise http.HttpServiceUnavailable("Instance console unavailable")
1394

    
1395
    assert isinstance(console, dict)
1396
    return console
1397

    
1398

    
1399
def _GetQueryFields(args):
1400
  """Tries to extract C{fields} query parameter.
1401

1402
  @type args: dictionary
1403
  @rtype: list of string
1404
  @raise http.HttpBadRequest: When parameter can't be found
1405

1406
  """
1407
  try:
1408
    fields = args["fields"]
1409
  except KeyError:
1410
    raise http.HttpBadRequest("Missing 'fields' query argument")
1411

    
1412
  return _SplitQueryFields(fields[0])
1413

    
1414

    
1415
def _SplitQueryFields(fields):
1416
  """Splits fields as given for a query request.
1417

1418
  @type fields: string
1419
  @rtype: list of string
1420

1421
  """
1422
  return [i.strip() for i in fields.split(",")]
1423

    
1424

    
1425
class R_2_query(baserlib.ResourceBase):
1426
  """/2/query/[resource] resource.
1427

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

    
1435
  def _Query(self, fields, qfilter):
1436
    client = self.GetClient()
1437
    return client.Query(self.items[0], fields, qfilter).ToDict()
1438

    
1439
  def GET(self):
1440
    """Returns resource information.
1441

1442
    @return: Query result, see L{objects.QueryResponse}
1443

1444
    """
1445
    return self._Query(_GetQueryFields(self.queryargs), None)
1446

    
1447
  def PUT(self):
1448
    """Submits job querying for resources.
1449

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

1452
    """
1453
    body = self.request_body
1454

    
1455
    baserlib.CheckType(body, dict, "Body contents")
1456

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

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

    
1467
    return self._Query(fields, qfilter)
1468

    
1469

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

1473
  """
1474
  GET_OPCODE = opcodes.OpQueryFields
1475

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

1479
    @return: List of serialized L{objects.QueryFieldDefinition}
1480

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

    
1489
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1490

    
1491

    
1492
class _R_Tags(baserlib.OpcodeResource):
1493
  """Quasiclass for tagging resources.
1494

1495
  Manages tags. When inheriting this class you must define the
1496
  TAG_LEVEL for it.
1497

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

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

1507
    We have to override the default to sort out cluster naming case.
1508

1509
    """
1510
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1511

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

    
1517
  def GET(self):
1518
    """Returns a list of tags.
1519

1520
    Example: ["tag1", "tag2", "tag3"]
1521

1522
    """
1523
    kind = self.TAG_LEVEL
1524

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

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

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

    
1541
    else:
1542
      raise http.HttpBadRequest("Unhandled tag type!")
1543

    
1544
    return list(tags)
1545

    
1546
  def GetPutOpInput(self):
1547
    """Add a set of tags.
1548

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

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

    
1560
  def GetDeleteOpInput(self):
1561
    """Delete a tag.
1562

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

1567
    """
1568
    # Re-use code
1569
    return self.GetPutOpInput()
1570

    
1571

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

1575
  Manages per-instance tags.
1576

1577
  """
1578
  TAG_LEVEL = constants.TAG_INSTANCE
1579

    
1580

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

1584
  Manages per-node tags.
1585

1586
  """
1587
  TAG_LEVEL = constants.TAG_NODE
1588

    
1589

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

1593
  Manages per-nodegroup tags.
1594

1595
  """
1596
  TAG_LEVEL = constants.TAG_NODEGROUP
1597

    
1598

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

1602
  Manages per-network tags.
1603

1604
  """
1605
  TAG_LEVEL = constants.TAG_NETWORK
1606

    
1607

    
1608
class R_2_tags(_R_Tags):
1609
  """ /2/tags resource.
1610

1611
  Manages cluster tags.
1612

1613
  """
1614
  TAG_LEVEL = constants.TAG_CLUSTER