Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 2ee9171a

History | View | Annotate | Download (40.1 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
  GET_OPCODE = opcodes.OpNodeQuery
396

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

400
    """
401
    client = self.GetClient(query=True)
402

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

    
412

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

416
  """
417
  GET_OPCODE = opcodes.OpNodeQuery
418

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

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

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

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

    
432

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

436
  """
437
  POST_OPCODE = opcodes.OpNodePowercycle
438

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

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

    
448

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

452
  """
453
  PUT_OPCODE = opcodes.OpNodeSetParams
454

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

458
    @return: Node role
459

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

    
466
    return _NR_MAP[result[0][0]]
467

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

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

    
474
    role = self.request_body
475

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

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

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

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

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

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

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

    
507

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

511
  """
512
  POST_OPCODE = opcodes.OpNodeEvacuate
513

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

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

    
523

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

527
  """
528
  POST_OPCODE = opcodes.OpNodeMigrate
529

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

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

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

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

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

    
558

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

562
  """
563
  POST_OPCODE = opcodes.OpNodeSetParams
564

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

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

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

    
575

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

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

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

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

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

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

    
601

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

605
  """
606
  PUT_OPCODE = opcodes.OpNodeModifyStorage
607

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

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

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

    
619
    changes = {}
620

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

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

    
632

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

636
  """
637
  PUT_OPCODE = opcodes.OpRepairNodeStorage
638

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

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

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

    
655

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

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

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

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

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

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

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

    
690

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

694
  """
695
  DELETE_OPCODE = opcodes.OpNetworkRemove
696

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

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

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

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

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

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

    
721

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

725
  """
726
  PUT_OPCODE = opcodes.OpNetworkConnect
727

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

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

    
738

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

742
  """
743
  PUT_OPCODE = opcodes.OpNetworkDisconnect
744

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

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

    
755

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

759
  """
760
  PUT_OPCODE = opcodes.OpNetworkSetParams
761

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

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

    
771

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

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

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

785

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

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

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

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

    
807

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

811
  """
812
  DELETE_OPCODE = opcodes.OpGroupRemove
813

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

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

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

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

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

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

    
837

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

841
  """
842
  PUT_OPCODE = opcodes.OpGroupSetParams
843

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

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

    
853

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

857
  """
858
  PUT_OPCODE = opcodes.OpGroupRename
859

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

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

    
870

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

874
  """
875
  PUT_OPCODE = opcodes.OpGroupAssignNodes
876

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

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

    
888

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

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

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

    
909

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

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

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

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

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

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

940
    @return: a job id
941

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

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

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

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

    
959
    _ConvertUsbDevices(data)
960

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

    
965

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

969
  """
970
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
971

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

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

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

    
983
    # Unlike most other RAPI calls, this one is composed of individual opcodes,
984
    # and we have to do the filling ourselves
985
    OPCODE_RENAME = {
986
      "os": "os_type",
987
      "name": "instance_name",
988
    }
989

    
990
    body = objects.FillDict(self.request_body, {
991
      "instances": [
992
        baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
993
                            rename=OPCODE_RENAME)
994
        for inst in self.request_body["instances"]
995
        ],
996
      })
997

    
998
    return (body, {
999
      "dry_run": self.dryRun(),
1000
      })
1001

    
1002

    
1003
class R_2_instances_name(baserlib.OpcodeResource):
1004
  """/2/instances/[instance_name] resource.
1005

1006
  """
1007
  GET_OPCODE = opcodes.OpInstanceQuery
1008
  DELETE_OPCODE = opcodes.OpInstanceRemove
1009

    
1010
  def GET(self):
1011
    """Send information about an instance.
1012

1013
    """
1014
    client = self.GetClient()
1015
    instance_name = self.items[0]
1016

    
1017
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1018
                                            names=[instance_name],
1019
                                            fields=I_FIELDS,
1020
                                            use_locking=self.useLocking())
1021

    
1022
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1023

    
1024
  def GetDeleteOpInput(self):
1025
    """Delete an instance.
1026

1027
    """
1028
    assert len(self.items) == 1
1029
    return ({}, {
1030
      "instance_name": self.items[0],
1031
      "ignore_failures": False,
1032
      "dry_run": self.dryRun(),
1033
      })
1034

    
1035

    
1036
class R_2_instances_name_info(baserlib.OpcodeResource):
1037
  """/2/instances/[instance_name]/info resource.
1038

1039
  """
1040
  GET_OPCODE = opcodes.OpInstanceQueryData
1041

    
1042
  def GetGetOpInput(self):
1043
    """Request detailed instance information.
1044

1045
    """
1046
    assert len(self.items) == 1
1047
    return ({}, {
1048
      "instances": [self.items[0]],
1049
      "static": bool(self._checkIntVariable("static", default=0)),
1050
      })
1051

    
1052

    
1053
class R_2_instances_name_reboot(baserlib.OpcodeResource):
1054
  """/2/instances/[instance_name]/reboot resource.
1055

1056
  Implements an instance reboot.
1057

1058
  """
1059
  POST_OPCODE = opcodes.OpInstanceReboot
1060

    
1061
  def GetPostOpInput(self):
1062
    """Reboot an instance.
1063

1064
    The URI takes type=[hard|soft|full] and
1065
    ignore_secondaries=[False|True] parameters.
1066

1067
    """
1068
    return ({}, {
1069
      "instance_name": self.items[0],
1070
      "reboot_type":
1071
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1072
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1073
      "dry_run": self.dryRun(),
1074
      })
1075

    
1076

    
1077
class R_2_instances_name_startup(baserlib.OpcodeResource):
1078
  """/2/instances/[instance_name]/startup resource.
1079

1080
  Implements an instance startup.
1081

1082
  """
1083
  PUT_OPCODE = opcodes.OpInstanceStartup
1084

    
1085
  def GetPutOpInput(self):
1086
    """Startup an instance.
1087

1088
    The URI takes force=[False|True] parameter to start the instance
1089
    if even if secondary disks are failing.
1090

1091
    """
1092
    return ({}, {
1093
      "instance_name": self.items[0],
1094
      "force": self.useForce(),
1095
      "dry_run": self.dryRun(),
1096
      "no_remember": bool(self._checkIntVariable("no_remember")),
1097
      })
1098

    
1099

    
1100
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1101
  """/2/instances/[instance_name]/shutdown resource.
1102

1103
  Implements an instance shutdown.
1104

1105
  """
1106
  PUT_OPCODE = opcodes.OpInstanceShutdown
1107

    
1108
  def GetPutOpInput(self):
1109
    """Shutdown an instance.
1110

1111
    """
1112
    return (self.request_body, {
1113
      "instance_name": self.items[0],
1114
      "no_remember": bool(self._checkIntVariable("no_remember")),
1115
      "dry_run": self.dryRun(),
1116
      })
1117

    
1118

    
1119
def _ParseInstanceReinstallRequest(name, data):
1120
  """Parses a request for reinstalling an instance.
1121

1122
  """
1123
  if not isinstance(data, dict):
1124
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1125

    
1126
  ostype = baserlib.CheckParameter(data, "os", default=None)
1127
  start = baserlib.CheckParameter(data, "start", exptype=bool,
1128
                                  default=True)
1129
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
1130

    
1131
  ops = [
1132
    opcodes.OpInstanceShutdown(instance_name=name),
1133
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1134
                                osparams=osparams),
1135
    ]
1136

    
1137
  if start:
1138
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1139

    
1140
  return ops
1141

    
1142

    
1143
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1144
  """/2/instances/[instance_name]/reinstall resource.
1145

1146
  Implements an instance reinstall.
1147

1148
  """
1149
  POST_OPCODE = opcodes.OpInstanceReinstall
1150

    
1151
  def POST(self):
1152
    """Reinstall an instance.
1153

1154
    The URI takes os=name and nostartup=[0|1] optional
1155
    parameters. By default, the instance will be started
1156
    automatically.
1157

1158
    """
1159
    if self.request_body:
1160
      if self.queryargs:
1161
        raise http.HttpBadRequest("Can't combine query and body parameters")
1162

    
1163
      body = self.request_body
1164
    elif self.queryargs:
1165
      # Legacy interface, do not modify/extend
1166
      body = {
1167
        "os": self._checkStringVariable("os"),
1168
        "start": not self._checkIntVariable("nostartup"),
1169
        }
1170
    else:
1171
      body = {}
1172

    
1173
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1174

    
1175
    return self.SubmitJob(ops)
1176

    
1177

    
1178
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1179
  """/2/instances/[instance_name]/replace-disks resource.
1180

1181
  """
1182
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1183

    
1184
  def GetPostOpInput(self):
1185
    """Replaces disks on an instance.
1186

1187
    """
1188
    static = {
1189
      "instance_name": self.items[0],
1190
      }
1191

    
1192
    if self.request_body:
1193
      data = self.request_body
1194
    elif self.queryargs:
1195
      # Legacy interface, do not modify/extend
1196
      data = {
1197
        "remote_node": self._checkStringVariable("remote_node", default=None),
1198
        "mode": self._checkStringVariable("mode", default=None),
1199
        "disks": self._checkStringVariable("disks", default=None),
1200
        "iallocator": self._checkStringVariable("iallocator", default=None),
1201
        }
1202
    else:
1203
      data = {}
1204

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

    
1221
    return (data, static)
1222

    
1223

    
1224
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1225
  """/2/instances/[instance_name]/activate-disks resource.
1226

1227
  """
1228
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1229

    
1230
  def GetPutOpInput(self):
1231
    """Activate disks for an instance.
1232

1233
    The URI might contain ignore_size to ignore current recorded size.
1234

1235
    """
1236
    return ({}, {
1237
      "instance_name": self.items[0],
1238
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
1239
      })
1240

    
1241

    
1242
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1243
  """/2/instances/[instance_name]/deactivate-disks resource.
1244

1245
  """
1246
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1247

    
1248
  def GetPutOpInput(self):
1249
    """Deactivate disks for an instance.
1250

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

    
1256

    
1257
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1258
  """/2/instances/[instance_name]/recreate-disks resource.
1259

1260
  """
1261
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1262

    
1263
  def GetPostOpInput(self):
1264
    """Recreate disks for an instance.
1265

1266
    """
1267
    return ({}, {
1268
      "instance_name": self.items[0],
1269
      })
1270

    
1271

    
1272
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1273
  """/2/instances/[instance_name]/prepare-export resource.
1274

1275
  """
1276
  PUT_OPCODE = opcodes.OpBackupPrepare
1277

    
1278
  def GetPutOpInput(self):
1279
    """Prepares an export for an instance.
1280

1281
    """
1282
    return ({}, {
1283
      "instance_name": self.items[0],
1284
      "mode": self._checkStringVariable("mode"),
1285
      })
1286

    
1287

    
1288
class R_2_instances_name_export(baserlib.OpcodeResource):
1289
  """/2/instances/[instance_name]/export resource.
1290

1291
  """
1292
  PUT_OPCODE = opcodes.OpBackupExport
1293
  PUT_RENAME = {
1294
    "destination": "target_node",
1295
    }
1296

    
1297
  def GetPutOpInput(self):
1298
    """Exports an instance.
1299

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

    
1305

    
1306
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1307
  """/2/instances/[instance_name]/migrate resource.
1308

1309
  """
1310
  PUT_OPCODE = opcodes.OpInstanceMigrate
1311

    
1312
  def GetPutOpInput(self):
1313
    """Migrates an instance.
1314

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

    
1320

    
1321
class R_2_instances_name_failover(baserlib.OpcodeResource):
1322
  """/2/instances/[instance_name]/failover resource.
1323

1324
  """
1325
  PUT_OPCODE = opcodes.OpInstanceFailover
1326

    
1327
  def GetPutOpInput(self):
1328
    """Does a failover of an instance.
1329

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

    
1335

    
1336
class R_2_instances_name_rename(baserlib.OpcodeResource):
1337
  """/2/instances/[instance_name]/rename resource.
1338

1339
  """
1340
  PUT_OPCODE = opcodes.OpInstanceRename
1341

    
1342
  def GetPutOpInput(self):
1343
    """Changes the name of an instance.
1344

1345
    """
1346
    return (self.request_body, {
1347
      "instance_name": self.items[0],
1348
      })
1349

    
1350

    
1351
class R_2_instances_name_modify(baserlib.OpcodeResource):
1352
  """/2/instances/[instance_name]/modify resource.
1353

1354
  """
1355
  PUT_OPCODE = opcodes.OpInstanceSetParams
1356

    
1357
  def GetPutOpInput(self):
1358
    """Changes parameters of an instance.
1359

1360
    """
1361
    data = self.request_body.copy()
1362
    _ConvertUsbDevices(data)
1363

    
1364
    return (data, {
1365
      "instance_name": self.items[0],
1366
      })
1367

    
1368

    
1369
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1370
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1371

1372
  """
1373
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1374

    
1375
  def GetPostOpInput(self):
1376
    """Increases the size of an instance disk.
1377

1378
    """
1379
    return (self.request_body, {
1380
      "instance_name": self.items[0],
1381
      "disk": int(self.items[1]),
1382
      })
1383

    
1384

    
1385
class R_2_instances_name_console(baserlib.ResourceBase):
1386
  """/2/instances/[instance_name]/console resource.
1387

1388
  """
1389
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1390
  GET_OPCODE = opcodes.OpInstanceConsole
1391

    
1392
  def GET(self):
1393
    """Request information for connecting to instance's console.
1394

1395
    @return: Serialized instance console description, see
1396
             L{objects.InstanceConsole}
1397

1398
    """
1399
    client = self.GetClient()
1400

    
1401
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1402

    
1403
    if console is None:
1404
      raise http.HttpServiceUnavailable("Instance console unavailable")
1405

    
1406
    assert isinstance(console, dict)
1407
    return console
1408

    
1409

    
1410
def _GetQueryFields(args):
1411
  """Tries to extract C{fields} query parameter.
1412

1413
  @type args: dictionary
1414
  @rtype: list of string
1415
  @raise http.HttpBadRequest: When parameter can't be found
1416

1417
  """
1418
  try:
1419
    fields = args["fields"]
1420
  except KeyError:
1421
    raise http.HttpBadRequest("Missing 'fields' query argument")
1422

    
1423
  return _SplitQueryFields(fields[0])
1424

    
1425

    
1426
def _SplitQueryFields(fields):
1427
  """Splits fields as given for a query request.
1428

1429
  @type fields: string
1430
  @rtype: list of string
1431

1432
  """
1433
  return [i.strip() for i in fields.split(",")]
1434

    
1435

    
1436
class R_2_query(baserlib.ResourceBase):
1437
  """/2/query/[resource] resource.
1438

1439
  """
1440
  # Results might contain sensitive information
1441
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1442
  PUT_ACCESS = GET_ACCESS
1443
  GET_OPCODE = opcodes.OpQuery
1444
  PUT_OPCODE = opcodes.OpQuery
1445

    
1446
  def _Query(self, fields, qfilter):
1447
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1448

    
1449
  def GET(self):
1450
    """Returns resource information.
1451

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

1454
    """
1455
    return self._Query(_GetQueryFields(self.queryargs), None)
1456

    
1457
  def PUT(self):
1458
    """Submits job querying for resources.
1459

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

1462
    """
1463
    body = self.request_body
1464

    
1465
    baserlib.CheckType(body, dict, "Body contents")
1466

    
1467
    try:
1468
      fields = body["fields"]
1469
    except KeyError:
1470
      fields = _GetQueryFields(self.queryargs)
1471

    
1472
    qfilter = body.get("qfilter", None)
1473
    # TODO: remove this after 2.7
1474
    if qfilter is None:
1475
      qfilter = body.get("filter", None)
1476

    
1477
    return self._Query(fields, qfilter)
1478

    
1479

    
1480
class R_2_query_fields(baserlib.ResourceBase):
1481
  """/2/query/[resource]/fields resource.
1482

1483
  """
1484
  GET_OPCODE = opcodes.OpQueryFields
1485

    
1486
  def GET(self):
1487
    """Retrieves list of available fields for a resource.
1488

1489
    @return: List of serialized L{objects.QueryFieldDefinition}
1490

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

    
1499
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1500

    
1501

    
1502
class _R_Tags(baserlib.OpcodeResource):
1503
  """Quasiclass for tagging resources.
1504

1505
  Manages tags. When inheriting this class you must define the
1506
  TAG_LEVEL for it.
1507

1508
  """
1509
  TAG_LEVEL = None
1510
  GET_OPCODE = opcodes.OpTagsGet
1511
  PUT_OPCODE = opcodes.OpTagsSet
1512
  DELETE_OPCODE = opcodes.OpTagsDel
1513

    
1514
  def __init__(self, items, queryargs, req, **kwargs):
1515
    """A tag resource constructor.
1516

1517
    We have to override the default to sort out cluster naming case.
1518

1519
    """
1520
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1521

    
1522
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1523
      self.name = None
1524
    else:
1525
      self.name = items[0]
1526

    
1527
  def GET(self):
1528
    """Returns a list of tags.
1529

1530
    Example: ["tag1", "tag2", "tag3"]
1531

1532
    """
1533
    kind = self.TAG_LEVEL
1534

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

    
1542
      cl = self.GetClient(query=True)
1543
      tags = list(cl.QueryTags(kind, self.name))
1544

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

    
1551
    else:
1552
      raise http.HttpBadRequest("Unhandled tag type!")
1553

    
1554
    return list(tags)
1555

    
1556
  def GetPutOpInput(self):
1557
    """Add a set of tags.
1558

1559
    The request as a list of strings should be PUT to this URI. And
1560
    you'll have back a job id.
1561

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

    
1570
  def GetDeleteOpInput(self):
1571
    """Delete a tag.
1572

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

1577
    """
1578
    # Re-use code
1579
    return self.GetPutOpInput()
1580

    
1581

    
1582
class R_2_instances_name_tags(_R_Tags):
1583
  """ /2/instances/[instance_name]/tags resource.
1584

1585
  Manages per-instance tags.
1586

1587
  """
1588
  TAG_LEVEL = constants.TAG_INSTANCE
1589

    
1590

    
1591
class R_2_nodes_name_tags(_R_Tags):
1592
  """ /2/nodes/[node_name]/tags resource.
1593

1594
  Manages per-node tags.
1595

1596
  """
1597
  TAG_LEVEL = constants.TAG_NODE
1598

    
1599

    
1600
class R_2_groups_name_tags(_R_Tags):
1601
  """ /2/groups/[group_name]/tags resource.
1602

1603
  Manages per-nodegroup tags.
1604

1605
  """
1606
  TAG_LEVEL = constants.TAG_NODEGROUP
1607

    
1608

    
1609
class R_2_networks_name_tags(_R_Tags):
1610
  """ /2/networks/[network_name]/tags resource.
1611

1612
  Manages per-network tags.
1613

1614
  """
1615
  TAG_LEVEL = constants.TAG_NETWORK
1616

    
1617

    
1618
class R_2_tags(_R_Tags):
1619
  """ /2/tags resource.
1620

1621
  Manages cluster tags.
1622

1623
  """
1624
  TAG_LEVEL = constants.TAG_CLUSTER