Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ edd49f9b

History | View | Annotate | Download (34.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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 http
60
from ganeti import constants
61
from ganeti import cli
62
from ganeti import rapi
63
from ganeti import ht
64
from ganeti import compat
65
from ganeti import ssconf
66
from ganeti.rapi import baserlib
67

    
68

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

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

    
92
G_FIELDS = [
93
  "alloc_policy",
94
  "name",
95
  "node_cnt",
96
  "node_list",
97
  "ipolicy",
98
  ] + _COMMON_FIELDS
99

    
100
J_FIELDS_BULK = [
101
  "id", "ops", "status", "summary",
102
  "opstatus",
103
  "received_ts", "start_ts", "end_ts",
104
  ]
105

    
106
J_FIELDS = J_FIELDS_BULK + [
107
  "oplog",
108
  "opresult",
109
  ]
110

    
111
_NR_DRAINED = "drained"
112
_NR_MASTER_CANDIDATE = "master-candidate"
113
_NR_MASTER = "master"
114
_NR_OFFLINE = "offline"
115
_NR_REGULAR = "regular"
116

    
117
_NR_MAP = {
118
  constants.NR_MASTER: _NR_MASTER,
119
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
120
  constants.NR_DRAINED: _NR_DRAINED,
121
  constants.NR_OFFLINE: _NR_OFFLINE,
122
  constants.NR_REGULAR: _NR_REGULAR,
123
  }
124

    
125
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
126

    
127
# Request data version field
128
_REQ_DATA_VERSION = "__version__"
129

    
130
# Feature string for instance creation request data version 1
131
_INST_CREATE_REQV1 = "instance-create-reqv1"
132

    
133
# Feature string for instance reinstall request version 1
134
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
135

    
136
# Feature string for node migration version 1
137
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
138

    
139
# Feature string for node evacuation with LU-generated jobs
140
_NODE_EVAC_RES1 = "node-evac-res1"
141

    
142
ALL_FEATURES = frozenset([
143
  _INST_CREATE_REQV1,
144
  _INST_REINSTALL_REQV1,
145
  _NODE_MIGRATE_REQV1,
146
  _NODE_EVAC_RES1,
147
  ])
148

    
149
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
150
_WFJC_TIMEOUT = 10
151

    
152

    
153
class R_root(baserlib.ResourceBase):
154
  """/ resource.
155

156
  """
157
  @staticmethod
158
  def GET():
159
    """Supported for legacy reasons.
160

161
    """
162
    return None
163

    
164

    
165
class R_2(R_root):
166
  """/2 resource.
167

168
  """
169

    
170

    
171
class R_version(baserlib.ResourceBase):
172
  """/version resource.
173

174
  This resource should be used to determine the remote API version and
175
  to adapt clients accordingly.
176

177
  """
178
  @staticmethod
179
  def GET():
180
    """Returns the remote API version.
181

182
    """
183
    return constants.RAPI_VERSION
184

    
185

    
186
class R_2_info(baserlib.OpcodeResource):
187
  """/2/info resource.
188

189
  """
190
  GET_OPCODE = opcodes.OpClusterQuery
191

    
192
  def GET(self):
193
    """Returns cluster information.
194

195
    """
196
    client = self.GetClient()
197
    return client.QueryClusterInfo()
198

    
199

    
200
class R_2_features(baserlib.ResourceBase):
201
  """/2/features resource.
202

203
  """
204
  @staticmethod
205
  def GET():
206
    """Returns list of optional RAPI features implemented.
207

208
    """
209
    return list(ALL_FEATURES)
210

    
211

    
212
class R_2_os(baserlib.OpcodeResource):
213
  """/2/os resource.
214

215
  """
216
  GET_OPCODE = opcodes.OpOsDiagnose
217

    
218
  def GET(self):
219
    """Return a list of all OSes.
220

221
    Can return error 500 in case of a problem.
222

223
    Example: ["debian-etch"]
224

225
    """
226
    cl = self.GetClient()
227
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
228
    job_id = self.SubmitJob([op], cl=cl)
229
    # we use custom feedback function, instead of print we log the status
230
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
231
    diagnose_data = result[0]
232

    
233
    if not isinstance(diagnose_data, list):
234
      raise http.HttpBadGateway(message="Can't get OS list")
235

    
236
    os_names = []
237
    for (name, variants) in diagnose_data:
238
      os_names.extend(cli.CalculateOSNames(name, variants))
239

    
240
    return os_names
241

    
242

    
243
class R_2_redist_config(baserlib.OpcodeResource):
244
  """/2/redistribute-config resource.
245

246
  """
247
  PUT_OPCODE = opcodes.OpClusterRedistConf
248

    
249

    
250
class R_2_cluster_modify(baserlib.OpcodeResource):
251
  """/2/modify resource.
252

253
  """
254
  PUT_OPCODE = opcodes.OpClusterSetParams
255

    
256

    
257
class R_2_jobs(baserlib.ResourceBase):
258
  """/2/jobs resource.
259

260
  """
261
  def GET(self):
262
    """Returns a dictionary of jobs.
263

264
    @return: a dictionary with jobs id and uri.
265

266
    """
267
    client = self.GetClient()
268

    
269
    if self.useBulk():
270
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
271
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
272
    else:
273
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
274
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
275
                                   uri_fields=("id", "uri"))
276

    
277

    
278
class R_2_jobs_id(baserlib.ResourceBase):
279
  """/2/jobs/[job_id] resource.
280

281
  """
282
  def GET(self):
283
    """Returns a job status.
284

285
    @return: a dictionary with job parameters.
286
        The result includes:
287
            - id: job ID as a number
288
            - status: current job status as a string
289
            - ops: involved OpCodes as a list of dictionaries for each
290
              opcodes in the job
291
            - opstatus: OpCodes status as a list
292
            - opresult: OpCodes results as a list of lists
293

294
    """
295
    job_id = self.items[0]
296
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
297
    if result is None:
298
      raise http.HttpNotFound()
299
    return baserlib.MapFields(J_FIELDS, result)
300

    
301
  def DELETE(self):
302
    """Cancel not-yet-started job.
303

304
    """
305
    job_id = self.items[0]
306
    result = self.GetClient().CancelJob(job_id)
307
    return result
308

    
309

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

313
  """
314
  # WaitForJobChange provides access to sensitive information and blocks
315
  # machine resources (it's a blocking RAPI call), hence restricting access.
316
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
317

    
318
  def GET(self):
319
    """Waits for job changes.
320

321
    """
322
    job_id = self.items[0]
323

    
324
    fields = self.getBodyParameter("fields")
325
    prev_job_info = self.getBodyParameter("previous_job_info", None)
326
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
327

    
328
    if not isinstance(fields, list):
329
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
330

    
331
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
332
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
333
                                " be a list")
334

    
335
    if not (prev_log_serial is None or
336
            isinstance(prev_log_serial, (int, long))):
337
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
338
                                " be a number")
339

    
340
    client = self.GetClient()
341
    result = client.WaitForJobChangeOnce(job_id, fields,
342
                                         prev_job_info, prev_log_serial,
343
                                         timeout=_WFJC_TIMEOUT)
344
    if not result:
345
      raise http.HttpNotFound()
346

    
347
    if result == constants.JOB_NOTCHANGED:
348
      # No changes
349
      return None
350

    
351
    (job_info, log_entries) = result
352

    
353
    return {
354
      "job_info": job_info,
355
      "log_entries": log_entries,
356
      }
357

    
358

    
359
class R_2_nodes(baserlib.OpcodeResource):
360
  """/2/nodes resource.
361

362
  """
363
  GET_OPCODE = opcodes.OpNodeQuery
364

    
365
  def GET(self):
366
    """Returns a list of all nodes.
367

368
    """
369
    client = self.GetClient()
370

    
371
    if self.useBulk():
372
      bulkdata = client.QueryNodes([], N_FIELDS, False)
373
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
374
    else:
375
      nodesdata = client.QueryNodes([], ["name"], False)
376
      nodeslist = [row[0] for row in nodesdata]
377
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
378
                                   uri_fields=("id", "uri"))
379

    
380

    
381
class R_2_nodes_name(baserlib.OpcodeResource):
382
  """/2/nodes/[node_name] resource.
383

384
  """
385
  GET_OPCODE = opcodes.OpNodeQuery
386

    
387
  def GET(self):
388
    """Send information about a node.
389

390
    """
391
    node_name = self.items[0]
392
    client = self.GetClient()
393

    
394
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
395
                                            names=[node_name], fields=N_FIELDS,
396
                                            use_locking=self.useLocking())
397

    
398
    return baserlib.MapFields(N_FIELDS, result[0])
399

    
400

    
401
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
402
  """/2/nodes/[node_name]/powercycle resource.
403

404
  """
405
  POST_OPCODE = opcodes.OpNodePowercycle
406

    
407
  def GetPostOpInput(self):
408
    """Tries to powercycle a node.
409

410
    """
411
    return (self.request_body, {
412
      "node_name": self.items[0],
413
      "force": self.useForce(),
414
      })
415

    
416

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

420
  """
421
  PUT_OPCODE = opcodes.OpNodeSetParams
422

    
423
  def GET(self):
424
    """Returns the current node role.
425

426
    @return: Node role
427

428
    """
429
    node_name = self.items[0]
430
    client = self.GetClient()
431
    result = client.QueryNodes(names=[node_name], fields=["role"],
432
                               use_locking=self.useLocking())
433

    
434
    return _NR_MAP[result[0][0]]
435

    
436
  def GetPutOpInput(self):
437
    """Sets the node role.
438

439
    """
440
    baserlib.CheckType(self.request_body, basestring, "Body contents")
441

    
442
    role = self.request_body
443

    
444
    if role == _NR_REGULAR:
445
      candidate = False
446
      offline = False
447
      drained = False
448

    
449
    elif role == _NR_MASTER_CANDIDATE:
450
      candidate = True
451
      offline = drained = None
452

    
453
    elif role == _NR_DRAINED:
454
      drained = True
455
      candidate = offline = None
456

    
457
    elif role == _NR_OFFLINE:
458
      offline = True
459
      candidate = drained = None
460

    
461
    else:
462
      raise http.HttpBadRequest("Can't set '%s' role" % role)
463

    
464
    assert len(self.items) == 1
465

    
466
    return ({}, {
467
      "node_name": self.items[0],
468
      "master_candidate": candidate,
469
      "offline": offline,
470
      "drained": drained,
471
      "force": self.useForce(),
472
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
473
      })
474

    
475

    
476
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
477
  """/2/nodes/[node_name]/evacuate resource.
478

479
  """
480
  POST_OPCODE = opcodes.OpNodeEvacuate
481

    
482
  def GetPostOpInput(self):
483
    """Evacuate all instances off a node.
484

485
    """
486
    return (self.request_body, {
487
      "node_name": self.items[0],
488
      "dry_run": self.dryRun(),
489
      })
490

    
491

    
492
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
493
  """/2/nodes/[node_name]/migrate resource.
494

495
  """
496
  POST_OPCODE = opcodes.OpNodeMigrate
497

    
498
  def GetPostOpInput(self):
499
    """Migrate all primary instances from a node.
500

501
    """
502
    if self.queryargs:
503
      # Support old-style requests
504
      if "live" in self.queryargs and "mode" in self.queryargs:
505
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
506
                                  " be passed")
507

    
508
      if "live" in self.queryargs:
509
        if self._checkIntVariable("live", default=1):
510
          mode = constants.HT_MIGRATION_LIVE
511
        else:
512
          mode = constants.HT_MIGRATION_NONLIVE
513
      else:
514
        mode = self._checkStringVariable("mode", default=None)
515

    
516
      data = {
517
        "mode": mode,
518
        }
519
    else:
520
      data = self.request_body
521

    
522
    return (data, {
523
      "node_name": self.items[0],
524
      })
525

    
526

    
527
class R_2_nodes_name_modify(baserlib.OpcodeResource):
528
  """/2/nodes/[node_name]/modify resource.
529

530
  """
531
  POST_OPCODE = opcodes.OpNodeSetParams
532

    
533
  def GetPostOpInput(self):
534
    """Changes parameters of a node.
535

536
    """
537
    assert len(self.items) == 1
538

    
539
    return (self.request_body, {
540
      "node_name": self.items[0],
541
      })
542

    
543

    
544
class R_2_nodes_name_storage(baserlib.OpcodeResource):
545
  """/2/nodes/[node_name]/storage resource.
546

547
  """
548
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
549
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
550
  GET_OPCODE = opcodes.OpNodeQueryStorage
551

    
552
  def GetGetOpInput(self):
553
    """List storage available on a node.
554

555
    """
556
    storage_type = self._checkStringVariable("storage_type", None)
557
    output_fields = self._checkStringVariable("output_fields", None)
558

    
559
    if not output_fields:
560
      raise http.HttpBadRequest("Missing the required 'output_fields'"
561
                                " parameter")
562

    
563
    return ({}, {
564
      "nodes": [self.items[0]],
565
      "storage_type": storage_type,
566
      "output_fields": output_fields.split(","),
567
      })
568

    
569

    
570
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
571
  """/2/nodes/[node_name]/storage/modify resource.
572

573
  """
574
  PUT_OPCODE = opcodes.OpNodeModifyStorage
575

    
576
  def GetPutOpInput(self):
577
    """Modifies a storage volume on a node.
578

579
    """
580
    storage_type = self._checkStringVariable("storage_type", None)
581
    name = self._checkStringVariable("name", None)
582

    
583
    if not name:
584
      raise http.HttpBadRequest("Missing the required 'name'"
585
                                " parameter")
586

    
587
    changes = {}
588

    
589
    if "allocatable" in self.queryargs:
590
      changes[constants.SF_ALLOCATABLE] = \
591
        bool(self._checkIntVariable("allocatable", default=1))
592

    
593
    return ({}, {
594
      "node_name": self.items[0],
595
      "storage_type": storage_type,
596
      "name": name,
597
      "changes": changes,
598
      })
599

    
600

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

604
  """
605
  PUT_OPCODE = opcodes.OpRepairNodeStorage
606

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

610
    """
611
    storage_type = self._checkStringVariable("storage_type", None)
612
    name = self._checkStringVariable("name", None)
613
    if not name:
614
      raise http.HttpBadRequest("Missing the required 'name'"
615
                                " parameter")
616

    
617
    return ({}, {
618
      "node_name": self.items[0],
619
      "storage_type": storage_type,
620
      "name": name,
621
      })
622

    
623

    
624
class R_2_groups(baserlib.OpcodeResource):
625
  """/2/groups resource.
626

627
  """
628
  GET_OPCODE = opcodes.OpGroupQuery
629
  POST_OPCODE = opcodes.OpGroupAdd
630
  POST_RENAME = {
631
    "name": "group_name",
632
    }
633

    
634
  def GetPostOpInput(self):
635
    """Create a node group.
636

637
    """
638
    assert not self.items
639
    return (self.request_body, {
640
      "dry_run": self.dryRun(),
641
      })
642

    
643
  def GET(self):
644
    """Returns a list of all node groups.
645

646
    """
647
    client = self.GetClient()
648

    
649
    if self.useBulk():
650
      bulkdata = client.QueryGroups([], G_FIELDS, False)
651
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
652
    else:
653
      data = client.QueryGroups([], ["name"], False)
654
      groupnames = [row[0] for row in data]
655
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
656
                                   uri_fields=("name", "uri"))
657

    
658

    
659
class R_2_groups_name(baserlib.OpcodeResource):
660
  """/2/groups/[group_name] resource.
661

662
  """
663
  DELETE_OPCODE = opcodes.OpGroupRemove
664

    
665
  def GET(self):
666
    """Send information about a node group.
667

668
    """
669
    group_name = self.items[0]
670
    client = self.GetClient()
671

    
672
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
673
                                            names=[group_name], fields=G_FIELDS,
674
                                            use_locking=self.useLocking())
675

    
676
    return baserlib.MapFields(G_FIELDS, result[0])
677

    
678
  def GetDeleteOpInput(self):
679
    """Delete a node group.
680

681
    """
682
    assert len(self.items) == 1
683
    return ({}, {
684
      "group_name": self.items[0],
685
      "dry_run": self.dryRun(),
686
      })
687

    
688

    
689
class R_2_groups_name_modify(baserlib.OpcodeResource):
690
  """/2/groups/[group_name]/modify resource.
691

692
  """
693
  PUT_OPCODE = opcodes.OpGroupSetParams
694

    
695
  def GetPutOpInput(self):
696
    """Changes some parameters of node group.
697

698
    """
699
    assert self.items
700
    return (self.request_body, {
701
      "group_name": self.items[0],
702
      })
703

    
704

    
705
class R_2_groups_name_rename(baserlib.OpcodeResource):
706
  """/2/groups/[group_name]/rename resource.
707

708
  """
709
  PUT_OPCODE = opcodes.OpGroupRename
710

    
711
  def GetPutOpInput(self):
712
    """Changes the name of a node group.
713

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

    
721

    
722
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
723
  """/2/groups/[group_name]/assign-nodes resource.
724

725
  """
726
  PUT_OPCODE = opcodes.OpGroupAssignNodes
727

    
728
  def GetPutOpInput(self):
729
    """Assigns nodes to a group.
730

731
    """
732
    assert len(self.items) == 1
733
    return (self.request_body, {
734
      "group_name": self.items[0],
735
      "dry_run": self.dryRun(),
736
      "force": self.useForce(),
737
      })
738

    
739

    
740
class R_2_instances(baserlib.OpcodeResource):
741
  """/2/instances resource.
742

743
  """
744
  GET_OPCODE = opcodes.OpInstanceQuery
745
  POST_OPCODE = opcodes.OpInstanceCreate
746
  POST_RENAME = {
747
    "os": "os_type",
748
    "name": "instance_name",
749
    }
750

    
751
  def GET(self):
752
    """Returns a list of all available instances.
753

754
    """
755
    client = self.GetClient()
756

    
757
    use_locking = self.useLocking()
758
    if self.useBulk():
759
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
760
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
761
    else:
762
      instancesdata = client.QueryInstances([], ["name"], use_locking)
763
      instanceslist = [row[0] for row in instancesdata]
764
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
765
                                   uri_fields=("id", "uri"))
766

    
767
  def GetPostOpInput(self):
768
    """Create an instance.
769

770
    @return: a job id
771

772
    """
773
    baserlib.CheckType(self.request_body, dict, "Body contents")
774

    
775
    # Default to request data version 0
776
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
777

    
778
    if data_version == 0:
779
      raise http.HttpBadRequest("Instance creation request version 0 is no"
780
                                " longer supported")
781
    elif data_version != 1:
782
      raise http.HttpBadRequest("Unsupported request data version %s" %
783
                                data_version)
784

    
785
    data = self.request_body.copy()
786
    # Remove "__version__"
787
    data.pop(_REQ_DATA_VERSION, None)
788

    
789
    return (data, {
790
      "dry_run": self.dryRun(),
791
      })
792

    
793

    
794
class R_2_instances_name(baserlib.OpcodeResource):
795
  """/2/instances/[instance_name] resource.
796

797
  """
798
  GET_OPCODE = opcodes.OpInstanceQuery
799
  DELETE_OPCODE = opcodes.OpInstanceRemove
800

    
801
  def GET(self):
802
    """Send information about an instance.
803

804
    """
805
    client = self.GetClient()
806
    instance_name = self.items[0]
807

    
808
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
809
                                            names=[instance_name],
810
                                            fields=I_FIELDS,
811
                                            use_locking=self.useLocking())
812

    
813
    return baserlib.MapFields(I_FIELDS, result[0])
814

    
815
  def GetDeleteOpInput(self):
816
    """Delete an instance.
817

818
    """
819
    assert len(self.items) == 1
820
    return ({}, {
821
      "instance_name": self.items[0],
822
      "ignore_failures": False,
823
      "dry_run": self.dryRun(),
824
      })
825

    
826

    
827
class R_2_instances_name_info(baserlib.OpcodeResource):
828
  """/2/instances/[instance_name]/info resource.
829

830
  """
831
  GET_OPCODE = opcodes.OpInstanceQueryData
832

    
833
  def GetGetOpInput(self):
834
    """Request detailed instance information.
835

836
    """
837
    assert len(self.items) == 1
838
    return ({}, {
839
      "instances": [self.items[0]],
840
      "static": bool(self._checkIntVariable("static", default=0)),
841
      })
842

    
843

    
844
class R_2_instances_name_reboot(baserlib.OpcodeResource):
845
  """/2/instances/[instance_name]/reboot resource.
846

847
  Implements an instance reboot.
848

849
  """
850
  POST_OPCODE = opcodes.OpInstanceReboot
851

    
852
  def GetPostOpInput(self):
853
    """Reboot an instance.
854

855
    The URI takes type=[hard|soft|full] and
856
    ignore_secondaries=[False|True] parameters.
857

858
    """
859
    return ({}, {
860
      "instance_name": self.items[0],
861
      "reboot_type":
862
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
863
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
864
      "dry_run": self.dryRun(),
865
      })
866

    
867

    
868
class R_2_instances_name_startup(baserlib.OpcodeResource):
869
  """/2/instances/[instance_name]/startup resource.
870

871
  Implements an instance startup.
872

873
  """
874
  PUT_OPCODE = opcodes.OpInstanceStartup
875

    
876
  def GetPutOpInput(self):
877
    """Startup an instance.
878

879
    The URI takes force=[False|True] parameter to start the instance
880
    if even if secondary disks are failing.
881

882
    """
883
    return ({}, {
884
      "instance_name": self.items[0],
885
      "force": self.useForce(),
886
      "dry_run": self.dryRun(),
887
      "no_remember": bool(self._checkIntVariable("no_remember")),
888
      })
889

    
890

    
891
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
892
  """/2/instances/[instance_name]/shutdown resource.
893

894
  Implements an instance shutdown.
895

896
  """
897
  PUT_OPCODE = opcodes.OpInstanceShutdown
898

    
899
  def GetPutOpInput(self):
900
    """Shutdown an instance.
901

902
    """
903
    return (self.request_body, {
904
      "instance_name": self.items[0],
905
      "no_remember": bool(self._checkIntVariable("no_remember")),
906
      "dry_run": self.dryRun(),
907
      })
908

    
909

    
910
def _ParseInstanceReinstallRequest(name, data):
911
  """Parses a request for reinstalling an instance.
912

913
  """
914
  if not isinstance(data, dict):
915
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
916

    
917
  ostype = baserlib.CheckParameter(data, "os", default=None)
918
  start = baserlib.CheckParameter(data, "start", exptype=bool,
919
                                  default=True)
920
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
921

    
922
  ops = [
923
    opcodes.OpInstanceShutdown(instance_name=name),
924
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
925
                                osparams=osparams),
926
    ]
927

    
928
  if start:
929
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
930

    
931
  return ops
932

    
933

    
934
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
935
  """/2/instances/[instance_name]/reinstall resource.
936

937
  Implements an instance reinstall.
938

939
  """
940
  POST_OPCODE = opcodes.OpInstanceReinstall
941

    
942
  def POST(self):
943
    """Reinstall an instance.
944

945
    The URI takes os=name and nostartup=[0|1] optional
946
    parameters. By default, the instance will be started
947
    automatically.
948

949
    """
950
    if self.request_body:
951
      if self.queryargs:
952
        raise http.HttpBadRequest("Can't combine query and body parameters")
953

    
954
      body = self.request_body
955
    elif self.queryargs:
956
      # Legacy interface, do not modify/extend
957
      body = {
958
        "os": self._checkStringVariable("os"),
959
        "start": not self._checkIntVariable("nostartup"),
960
        }
961
    else:
962
      body = {}
963

    
964
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
965

    
966
    return self.SubmitJob(ops)
967

    
968

    
969
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
970
  """/2/instances/[instance_name]/replace-disks resource.
971

972
  """
973
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
974

    
975
  def GetPostOpInput(self):
976
    """Replaces disks on an instance.
977

978
    """
979
    static = {
980
      "instance_name": self.items[0],
981
      }
982

    
983
    if self.request_body:
984
      data = self.request_body
985
    elif self.queryargs:
986
      # Legacy interface, do not modify/extend
987
      data = {
988
        "remote_node": self._checkStringVariable("remote_node", default=None),
989
        "mode": self._checkStringVariable("mode", default=None),
990
        "disks": self._checkStringVariable("disks", default=None),
991
        "iallocator": self._checkStringVariable("iallocator", default=None),
992
        }
993
    else:
994
      data = {}
995

    
996
    # Parse disks
997
    try:
998
      raw_disks = data.pop("disks")
999
    except KeyError:
1000
      pass
1001
    else:
1002
      if raw_disks:
1003
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1004
          data["disks"] = raw_disks
1005
        else:
1006
          # Backwards compatibility for strings of the format "1, 2, 3"
1007
          try:
1008
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1009
          except (TypeError, ValueError), err:
1010
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1011

    
1012
    return (data, static)
1013

    
1014

    
1015
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1016
  """/2/instances/[instance_name]/activate-disks resource.
1017

1018
  """
1019
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1020

    
1021
  def GetPutOpInput(self):
1022
    """Activate disks for an instance.
1023

1024
    The URI might contain ignore_size to ignore current recorded size.
1025

1026
    """
1027
    return ({}, {
1028
      "instance_name": self.items[0],
1029
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
1030
      })
1031

    
1032

    
1033
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1034
  """/2/instances/[instance_name]/deactivate-disks resource.
1035

1036
  """
1037
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1038

    
1039
  def GetPutOpInput(self):
1040
    """Deactivate disks for an instance.
1041

1042
    """
1043
    return ({}, {
1044
      "instance_name": self.items[0],
1045
      })
1046

    
1047

    
1048
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1049
  """/2/instances/[instance_name]/recreate-disks resource.
1050

1051
  """
1052
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1053

    
1054
  def GetPostOpInput(self):
1055
    """Recreate disks for an instance.
1056

1057
    """
1058
    return ({}, {
1059
      "instance_name": self.items[0],
1060
      })
1061

    
1062

    
1063
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1064
  """/2/instances/[instance_name]/prepare-export resource.
1065

1066
  """
1067
  PUT_OPCODE = opcodes.OpBackupPrepare
1068

    
1069
  def GetPutOpInput(self):
1070
    """Prepares an export for an instance.
1071

1072
    """
1073
    return ({}, {
1074
      "instance_name": self.items[0],
1075
      "mode": self._checkStringVariable("mode"),
1076
      })
1077

    
1078

    
1079
class R_2_instances_name_export(baserlib.OpcodeResource):
1080
  """/2/instances/[instance_name]/export resource.
1081

1082
  """
1083
  PUT_OPCODE = opcodes.OpBackupExport
1084
  PUT_RENAME = {
1085
    "destination": "target_node",
1086
    }
1087

    
1088
  def GetPutOpInput(self):
1089
    """Exports an instance.
1090

1091
    """
1092
    return (self.request_body, {
1093
      "instance_name": self.items[0],
1094
      })
1095

    
1096

    
1097
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1098
  """/2/instances/[instance_name]/migrate resource.
1099

1100
  """
1101
  PUT_OPCODE = opcodes.OpInstanceMigrate
1102

    
1103
  def GetPutOpInput(self):
1104
    """Migrates an instance.
1105

1106
    """
1107
    return (self.request_body, {
1108
      "instance_name": self.items[0],
1109
      })
1110

    
1111

    
1112
class R_2_instances_name_failover(baserlib.OpcodeResource):
1113
  """/2/instances/[instance_name]/failover resource.
1114

1115
  """
1116
  PUT_OPCODE = opcodes.OpInstanceFailover
1117

    
1118
  def GetPutOpInput(self):
1119
    """Does a failover of an instance.
1120

1121
    """
1122
    return (self.request_body, {
1123
      "instance_name": self.items[0],
1124
      })
1125

    
1126

    
1127
class R_2_instances_name_rename(baserlib.OpcodeResource):
1128
  """/2/instances/[instance_name]/rename resource.
1129

1130
  """
1131
  PUT_OPCODE = opcodes.OpInstanceRename
1132

    
1133
  def GetPutOpInput(self):
1134
    """Changes the name of an instance.
1135

1136
    """
1137
    return (self.request_body, {
1138
      "instance_name": self.items[0],
1139
      })
1140

    
1141

    
1142
class R_2_instances_name_modify(baserlib.OpcodeResource):
1143
  """/2/instances/[instance_name]/modify resource.
1144

1145
  """
1146
  PUT_OPCODE = opcodes.OpInstanceSetParams
1147

    
1148
  def GetPutOpInput(self):
1149
    """Changes parameters of an instance.
1150

1151
    """
1152
    return (self.request_body, {
1153
      "instance_name": self.items[0],
1154
      })
1155

    
1156

    
1157
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1158
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1159

1160
  """
1161
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1162

    
1163
  def GetPostOpInput(self):
1164
    """Increases the size of an instance disk.
1165

1166
    """
1167
    return (self.request_body, {
1168
      "instance_name": self.items[0],
1169
      "disk": int(self.items[1]),
1170
      })
1171

    
1172

    
1173
class R_2_instances_name_console(baserlib.ResourceBase):
1174
  """/2/instances/[instance_name]/console resource.
1175

1176
  """
1177
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1178
  GET_OPCODE = opcodes.OpInstanceConsole
1179

    
1180
  def GET(self):
1181
    """Request information for connecting to instance's console.
1182

1183
    @return: Serialized instance console description, see
1184
             L{objects.InstanceConsole}
1185

1186
    """
1187
    client = self.GetClient()
1188

    
1189
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1190

    
1191
    if console is None:
1192
      raise http.HttpServiceUnavailable("Instance console unavailable")
1193

    
1194
    assert isinstance(console, dict)
1195
    return console
1196

    
1197

    
1198
def _GetQueryFields(args):
1199
  """
1200

1201
  """
1202
  try:
1203
    fields = args["fields"]
1204
  except KeyError:
1205
    raise http.HttpBadRequest("Missing 'fields' query argument")
1206

    
1207
  return _SplitQueryFields(fields[0])
1208

    
1209

    
1210
def _SplitQueryFields(fields):
1211
  """
1212

1213
  """
1214
  return [i.strip() for i in fields.split(",")]
1215

    
1216

    
1217
class R_2_query(baserlib.ResourceBase):
1218
  """/2/query/[resource] resource.
1219

1220
  """
1221
  # Results might contain sensitive information
1222
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1223
  GET_OPCODE = opcodes.OpQuery
1224
  PUT_OPCODE = opcodes.OpQuery
1225

    
1226
  def _Query(self, fields, qfilter):
1227
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1228

    
1229
  def GET(self):
1230
    """Returns resource information.
1231

1232
    @return: Query result, see L{objects.QueryResponse}
1233

1234
    """
1235
    return self._Query(_GetQueryFields(self.queryargs), None)
1236

    
1237
  def PUT(self):
1238
    """Submits job querying for resources.
1239

1240
    @return: Query result, see L{objects.QueryResponse}
1241

1242
    """
1243
    body = self.request_body
1244

    
1245
    baserlib.CheckType(body, dict, "Body contents")
1246

    
1247
    try:
1248
      fields = body["fields"]
1249
    except KeyError:
1250
      fields = _GetQueryFields(self.queryargs)
1251

    
1252
    qfilter = body.get("qfilter", None)
1253
    # TODO: remove this after 2.7
1254
    if qfilter is None:
1255
      qfilter = body.get("filter", None)
1256

    
1257
    return self._Query(fields, qfilter)
1258

    
1259

    
1260
class R_2_query_fields(baserlib.ResourceBase):
1261
  """/2/query/[resource]/fields resource.
1262

1263
  """
1264
  GET_OPCODE = opcodes.OpQueryFields
1265

    
1266
  def GET(self):
1267
    """Retrieves list of available fields for a resource.
1268

1269
    @return: List of serialized L{objects.QueryFieldDefinition}
1270

1271
    """
1272
    try:
1273
      raw_fields = self.queryargs["fields"]
1274
    except KeyError:
1275
      fields = None
1276
    else:
1277
      fields = _SplitQueryFields(raw_fields[0])
1278

    
1279
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1280

    
1281

    
1282
class _R_Tags(baserlib.OpcodeResource):
1283
  """ Quasiclass for tagging resources
1284

1285
  Manages tags. When inheriting this class you must define the
1286
  TAG_LEVEL for it.
1287

1288
  """
1289
  TAG_LEVEL = None
1290
  GET_OPCODE = opcodes.OpTagsGet
1291
  PUT_OPCODE = opcodes.OpTagsSet
1292
  DELETE_OPCODE = opcodes.OpTagsDel
1293

    
1294
  def __init__(self, items, queryargs, req, **kwargs):
1295
    """A tag resource constructor.
1296

1297
    We have to override the default to sort out cluster naming case.
1298

1299
    """
1300
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1301

    
1302
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1303
      self.name = None
1304
    else:
1305
      self.name = items[0]
1306

    
1307
  def GET(self):
1308
    """Returns a list of tags.
1309

1310
    Example: ["tag1", "tag2", "tag3"]
1311

1312
    """
1313
    kind = self.TAG_LEVEL
1314

    
1315
    if kind in (constants.TAG_INSTANCE,
1316
                constants.TAG_NODEGROUP,
1317
                constants.TAG_NODE):
1318
      if not self.name:
1319
        raise http.HttpBadRequest("Missing name on tag request")
1320

    
1321
      cl = self.GetClient()
1322
      if kind == constants.TAG_INSTANCE:
1323
        fn = cl.QueryInstances
1324
      elif kind == constants.TAG_NODEGROUP:
1325
        fn = cl.QueryGroups
1326
      else:
1327
        fn = cl.QueryNodes
1328
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1329
      if not result or not result[0]:
1330
        raise http.HttpBadGateway("Invalid response from tag query")
1331
      tags = result[0][0]
1332

    
1333
    elif kind == constants.TAG_CLUSTER:
1334
      assert not self.name
1335
      # TODO: Use query API?
1336
      ssc = ssconf.SimpleStore()
1337
      tags = ssc.GetClusterTags()
1338

    
1339
    return list(tags)
1340

    
1341
  def GetPutOpInput(self):
1342
    """Add a set of tags.
1343

1344
    The request as a list of strings should be PUT to this URI. And
1345
    you'll have back a job id.
1346

1347
    """
1348
    return ({}, {
1349
      "kind": self.TAG_LEVEL,
1350
      "name": self.name,
1351
      "tags": self.queryargs.get("tag", []),
1352
      "dry_run": self.dryRun(),
1353
      })
1354

    
1355
  def GetDeleteOpInput(self):
1356
    """Delete a tag.
1357

1358
    In order to delete a set of tags, the DELETE
1359
    request should be addressed to URI like:
1360
    /tags?tag=[tag]&tag=[tag]
1361

1362
    """
1363
    # Re-use code
1364
    return self.GetPutOpInput()
1365

    
1366

    
1367
class R_2_instances_name_tags(_R_Tags):
1368
  """ /2/instances/[instance_name]/tags resource.
1369

1370
  Manages per-instance tags.
1371

1372
  """
1373
  TAG_LEVEL = constants.TAG_INSTANCE
1374

    
1375

    
1376
class R_2_nodes_name_tags(_R_Tags):
1377
  """ /2/nodes/[node_name]/tags resource.
1378

1379
  Manages per-node tags.
1380

1381
  """
1382
  TAG_LEVEL = constants.TAG_NODE
1383

    
1384

    
1385
class R_2_groups_name_tags(_R_Tags):
1386
  """ /2/groups/[group_name]/tags resource.
1387

1388
  Manages per-nodegroup tags.
1389

1390
  """
1391
  TAG_LEVEL = constants.TAG_NODEGROUP
1392

    
1393

    
1394
class R_2_tags(_R_Tags):
1395
  """ /2/tags resource.
1396

1397
  Manages cluster tags.
1398

1399
  """
1400
  TAG_LEVEL = constants.TAG_CLUSTER