Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 003306f9

History | View | Annotate | Download (34.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
            "ndparams",
90
            "group.uuid",
91
            ] + _COMMON_FIELDS
92

    
93
G_FIELDS = [
94
  "alloc_policy",
95
  "name",
96
  "node_cnt",
97
  "node_list",
98
  "ipolicy",
99
  "custom_ipolicy",
100
  "diskparams",
101
  "custom_diskparams",
102
  "ndparams",
103
  "custom_ndparams",
104
  ] + _COMMON_FIELDS
105

    
106
J_FIELDS_BULK = [
107
  "id", "ops", "status", "summary",
108
  "opstatus",
109
  "received_ts", "start_ts", "end_ts",
110
  ]
111

    
112
J_FIELDS = J_FIELDS_BULK + [
113
  "oplog",
114
  "opresult",
115
  ]
116

    
117
_NR_DRAINED = "drained"
118
_NR_MASTER_CANDIDATE = "master-candidate"
119
_NR_MASTER = "master"
120
_NR_OFFLINE = "offline"
121
_NR_REGULAR = "regular"
122

    
123
_NR_MAP = {
124
  constants.NR_MASTER: _NR_MASTER,
125
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
126
  constants.NR_DRAINED: _NR_DRAINED,
127
  constants.NR_OFFLINE: _NR_OFFLINE,
128
  constants.NR_REGULAR: _NR_REGULAR,
129
  }
130

    
131
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
132

    
133
# Request data version field
134
_REQ_DATA_VERSION = "__version__"
135

    
136
# Feature string for instance creation request data version 1
137
_INST_CREATE_REQV1 = "instance-create-reqv1"
138

    
139
# Feature string for instance reinstall request version 1
140
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
141

    
142
# Feature string for node migration version 1
143
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
144

    
145
# Feature string for node evacuation with LU-generated jobs
146
_NODE_EVAC_RES1 = "node-evac-res1"
147

    
148
ALL_FEATURES = frozenset([
149
  _INST_CREATE_REQV1,
150
  _INST_REINSTALL_REQV1,
151
  _NODE_MIGRATE_REQV1,
152
  _NODE_EVAC_RES1,
153
  ])
154

    
155
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
156
_WFJC_TIMEOUT = 10
157

    
158

    
159
class R_root(baserlib.ResourceBase):
160
  """/ resource.
161

162
  """
163
  @staticmethod
164
  def GET():
165
    """Supported for legacy reasons.
166

167
    """
168
    return None
169

    
170

    
171
class R_2(R_root):
172
  """/2 resource.
173

174
  """
175

    
176

    
177
class R_version(baserlib.ResourceBase):
178
  """/version resource.
179

180
  This resource should be used to determine the remote API version and
181
  to adapt clients accordingly.
182

183
  """
184
  @staticmethod
185
  def GET():
186
    """Returns the remote API version.
187

188
    """
189
    return constants.RAPI_VERSION
190

    
191

    
192
class R_2_info(baserlib.OpcodeResource):
193
  """/2/info resource.
194

195
  """
196
  GET_OPCODE = opcodes.OpClusterQuery
197

    
198
  def GET(self):
199
    """Returns cluster information.
200

201
    """
202
    client = self.GetClient()
203
    return client.QueryClusterInfo()
204

    
205

    
206
class R_2_features(baserlib.ResourceBase):
207
  """/2/features resource.
208

209
  """
210
  @staticmethod
211
  def GET():
212
    """Returns list of optional RAPI features implemented.
213

214
    """
215
    return list(ALL_FEATURES)
216

    
217

    
218
class R_2_os(baserlib.OpcodeResource):
219
  """/2/os resource.
220

221
  """
222
  GET_OPCODE = opcodes.OpOsDiagnose
223

    
224
  def GET(self):
225
    """Return a list of all OSes.
226

227
    Can return error 500 in case of a problem.
228

229
    Example: ["debian-etch"]
230

231
    """
232
    cl = self.GetClient()
233
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
234
    job_id = self.SubmitJob([op], cl=cl)
235
    # we use custom feedback function, instead of print we log the status
236
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
237
    diagnose_data = result[0]
238

    
239
    if not isinstance(diagnose_data, list):
240
      raise http.HttpBadGateway(message="Can't get OS list")
241

    
242
    os_names = []
243
    for (name, variants) in diagnose_data:
244
      os_names.extend(cli.CalculateOSNames(name, variants))
245

    
246
    return os_names
247

    
248

    
249
class R_2_redist_config(baserlib.OpcodeResource):
250
  """/2/redistribute-config resource.
251

252
  """
253
  PUT_OPCODE = opcodes.OpClusterRedistConf
254

    
255

    
256
class R_2_cluster_modify(baserlib.OpcodeResource):
257
  """/2/modify resource.
258

259
  """
260
  PUT_OPCODE = opcodes.OpClusterSetParams
261

    
262

    
263
class R_2_jobs(baserlib.ResourceBase):
264
  """/2/jobs resource.
265

266
  """
267
  def GET(self):
268
    """Returns a dictionary of jobs.
269

270
    @return: a dictionary with jobs id and uri.
271

272
    """
273
    client = self.GetClient()
274

    
275
    if self.useBulk():
276
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
277
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
278
    else:
279
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
280
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
281
                                   uri_fields=("id", "uri"))
282

    
283

    
284
class R_2_jobs_id(baserlib.ResourceBase):
285
  """/2/jobs/[job_id] resource.
286

287
  """
288
  def GET(self):
289
    """Returns a job status.
290

291
    @return: a dictionary with job parameters.
292
        The result includes:
293
            - id: job ID as a number
294
            - status: current job status as a string
295
            - ops: involved OpCodes as a list of dictionaries for each
296
              opcodes in the job
297
            - opstatus: OpCodes status as a list
298
            - opresult: OpCodes results as a list of lists
299

300
    """
301
    job_id = self.items[0]
302
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
303
    if result is None:
304
      raise http.HttpNotFound()
305
    return baserlib.MapFields(J_FIELDS, result)
306

    
307
  def DELETE(self):
308
    """Cancel not-yet-started job.
309

310
    """
311
    job_id = self.items[0]
312
    result = self.GetClient().CancelJob(job_id)
313
    return result
314

    
315

    
316
class R_2_jobs_id_wait(baserlib.ResourceBase):
317
  """/2/jobs/[job_id]/wait resource.
318

319
  """
320
  # WaitForJobChange provides access to sensitive information and blocks
321
  # machine resources (it's a blocking RAPI call), hence restricting access.
322
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
323

    
324
  def GET(self):
325
    """Waits for job changes.
326

327
    """
328
    job_id = self.items[0]
329

    
330
    fields = self.getBodyParameter("fields")
331
    prev_job_info = self.getBodyParameter("previous_job_info", None)
332
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
333

    
334
    if not isinstance(fields, list):
335
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
336

    
337
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
338
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
339
                                " be a list")
340

    
341
    if not (prev_log_serial is None or
342
            isinstance(prev_log_serial, (int, long))):
343
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
344
                                " be a number")
345

    
346
    client = self.GetClient()
347
    result = client.WaitForJobChangeOnce(job_id, fields,
348
                                         prev_job_info, prev_log_serial,
349
                                         timeout=_WFJC_TIMEOUT)
350
    if not result:
351
      raise http.HttpNotFound()
352

    
353
    if result == constants.JOB_NOTCHANGED:
354
      # No changes
355
      return None
356

    
357
    (job_info, log_entries) = result
358

    
359
    return {
360
      "job_info": job_info,
361
      "log_entries": log_entries,
362
      }
363

    
364

    
365
class R_2_nodes(baserlib.OpcodeResource):
366
  """/2/nodes resource.
367

368
  """
369
  GET_OPCODE = opcodes.OpNodeQuery
370

    
371
  def GET(self):
372
    """Returns a list of all nodes.
373

374
    """
375
    client = self.GetClient()
376

    
377
    if self.useBulk():
378
      bulkdata = client.QueryNodes([], N_FIELDS, False)
379
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
380
    else:
381
      nodesdata = client.QueryNodes([], ["name"], False)
382
      nodeslist = [row[0] for row in nodesdata]
383
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
384
                                   uri_fields=("id", "uri"))
385

    
386

    
387
class R_2_nodes_name(baserlib.OpcodeResource):
388
  """/2/nodes/[node_name] resource.
389

390
  """
391
  GET_OPCODE = opcodes.OpNodeQuery
392

    
393
  def GET(self):
394
    """Send information about a node.
395

396
    """
397
    node_name = self.items[0]
398
    client = self.GetClient()
399

    
400
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
401
                                            names=[node_name], fields=N_FIELDS,
402
                                            use_locking=self.useLocking())
403

    
404
    return baserlib.MapFields(N_FIELDS, result[0])
405

    
406

    
407
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
408
  """/2/nodes/[node_name]/powercycle resource.
409

410
  """
411
  POST_OPCODE = opcodes.OpNodePowercycle
412

    
413
  def GetPostOpInput(self):
414
    """Tries to powercycle a node.
415

416
    """
417
    return (self.request_body, {
418
      "node_name": self.items[0],
419
      "force": self.useForce(),
420
      })
421

    
422

    
423
class R_2_nodes_name_role(baserlib.OpcodeResource):
424
  """/2/nodes/[node_name]/role resource.
425

426
  """
427
  PUT_OPCODE = opcodes.OpNodeSetParams
428

    
429
  def GET(self):
430
    """Returns the current node role.
431

432
    @return: Node role
433

434
    """
435
    node_name = self.items[0]
436
    client = self.GetClient()
437
    result = client.QueryNodes(names=[node_name], fields=["role"],
438
                               use_locking=self.useLocking())
439

    
440
    return _NR_MAP[result[0][0]]
441

    
442
  def GetPutOpInput(self):
443
    """Sets the node role.
444

445
    """
446
    baserlib.CheckType(self.request_body, basestring, "Body contents")
447

    
448
    role = self.request_body
449

    
450
    if role == _NR_REGULAR:
451
      candidate = False
452
      offline = False
453
      drained = False
454

    
455
    elif role == _NR_MASTER_CANDIDATE:
456
      candidate = True
457
      offline = drained = None
458

    
459
    elif role == _NR_DRAINED:
460
      drained = True
461
      candidate = offline = None
462

    
463
    elif role == _NR_OFFLINE:
464
      offline = True
465
      candidate = drained = None
466

    
467
    else:
468
      raise http.HttpBadRequest("Can't set '%s' role" % role)
469

    
470
    assert len(self.items) == 1
471

    
472
    return ({}, {
473
      "node_name": self.items[0],
474
      "master_candidate": candidate,
475
      "offline": offline,
476
      "drained": drained,
477
      "force": self.useForce(),
478
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
479
      })
480

    
481

    
482
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
483
  """/2/nodes/[node_name]/evacuate resource.
484

485
  """
486
  POST_OPCODE = opcodes.OpNodeEvacuate
487

    
488
  def GetPostOpInput(self):
489
    """Evacuate all instances off a node.
490

491
    """
492
    return (self.request_body, {
493
      "node_name": self.items[0],
494
      "dry_run": self.dryRun(),
495
      })
496

    
497

    
498
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
499
  """/2/nodes/[node_name]/migrate resource.
500

501
  """
502
  POST_OPCODE = opcodes.OpNodeMigrate
503

    
504
  def GetPostOpInput(self):
505
    """Migrate all primary instances from a node.
506

507
    """
508
    if self.queryargs:
509
      # Support old-style requests
510
      if "live" in self.queryargs and "mode" in self.queryargs:
511
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
512
                                  " be passed")
513

    
514
      if "live" in self.queryargs:
515
        if self._checkIntVariable("live", default=1):
516
          mode = constants.HT_MIGRATION_LIVE
517
        else:
518
          mode = constants.HT_MIGRATION_NONLIVE
519
      else:
520
        mode = self._checkStringVariable("mode", default=None)
521

    
522
      data = {
523
        "mode": mode,
524
        }
525
    else:
526
      data = self.request_body
527

    
528
    return (data, {
529
      "node_name": self.items[0],
530
      })
531

    
532

    
533
class R_2_nodes_name_modify(baserlib.OpcodeResource):
534
  """/2/nodes/[node_name]/modify resource.
535

536
  """
537
  POST_OPCODE = opcodes.OpNodeSetParams
538

    
539
  def GetPostOpInput(self):
540
    """Changes parameters of a node.
541

542
    """
543
    assert len(self.items) == 1
544

    
545
    return (self.request_body, {
546
      "node_name": self.items[0],
547
      })
548

    
549

    
550
class R_2_nodes_name_storage(baserlib.OpcodeResource):
551
  """/2/nodes/[node_name]/storage resource.
552

553
  """
554
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
555
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
556
  GET_OPCODE = opcodes.OpNodeQueryStorage
557

    
558
  def GetGetOpInput(self):
559
    """List storage available on a node.
560

561
    """
562
    storage_type = self._checkStringVariable("storage_type", None)
563
    output_fields = self._checkStringVariable("output_fields", None)
564

    
565
    if not output_fields:
566
      raise http.HttpBadRequest("Missing the required 'output_fields'"
567
                                " parameter")
568

    
569
    return ({}, {
570
      "nodes": [self.items[0]],
571
      "storage_type": storage_type,
572
      "output_fields": output_fields.split(","),
573
      })
574

    
575

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

579
  """
580
  PUT_OPCODE = opcodes.OpNodeModifyStorage
581

    
582
  def GetPutOpInput(self):
583
    """Modifies a storage volume on a node.
584

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

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

    
593
    changes = {}
594

    
595
    if "allocatable" in self.queryargs:
596
      changes[constants.SF_ALLOCATABLE] = \
597
        bool(self._checkIntVariable("allocatable", default=1))
598

    
599
    return ({}, {
600
      "node_name": self.items[0],
601
      "storage_type": storage_type,
602
      "name": name,
603
      "changes": changes,
604
      })
605

    
606

    
607
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
608
  """/2/nodes/[node_name]/storage/repair resource.
609

610
  """
611
  PUT_OPCODE = opcodes.OpRepairNodeStorage
612

    
613
  def GetPutOpInput(self):
614
    """Repairs a storage volume on a node.
615

616
    """
617
    storage_type = self._checkStringVariable("storage_type", None)
618
    name = self._checkStringVariable("name", None)
619
    if not name:
620
      raise http.HttpBadRequest("Missing the required 'name'"
621
                                " parameter")
622

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

    
629

    
630
class R_2_groups(baserlib.OpcodeResource):
631
  """/2/groups resource.
632

633
  """
634
  GET_OPCODE = opcodes.OpGroupQuery
635
  POST_OPCODE = opcodes.OpGroupAdd
636
  POST_RENAME = {
637
    "name": "group_name",
638
    }
639

    
640
  def GetPostOpInput(self):
641
    """Create a node group.
642

643
    """
644
    assert not self.items
645
    return (self.request_body, {
646
      "dry_run": self.dryRun(),
647
      })
648

    
649
  def GET(self):
650
    """Returns a list of all node groups.
651

652
    """
653
    client = self.GetClient()
654

    
655
    if self.useBulk():
656
      bulkdata = client.QueryGroups([], G_FIELDS, False)
657
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
658
    else:
659
      data = client.QueryGroups([], ["name"], False)
660
      groupnames = [row[0] for row in data]
661
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
662
                                   uri_fields=("name", "uri"))
663

    
664

    
665
class R_2_groups_name(baserlib.OpcodeResource):
666
  """/2/groups/[group_name] resource.
667

668
  """
669
  DELETE_OPCODE = opcodes.OpGroupRemove
670

    
671
  def GET(self):
672
    """Send information about a node group.
673

674
    """
675
    group_name = self.items[0]
676
    client = self.GetClient()
677

    
678
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
679
                                            names=[group_name], fields=G_FIELDS,
680
                                            use_locking=self.useLocking())
681

    
682
    return baserlib.MapFields(G_FIELDS, result[0])
683

    
684
  def GetDeleteOpInput(self):
685
    """Delete a node group.
686

687
    """
688
    assert len(self.items) == 1
689
    return ({}, {
690
      "group_name": self.items[0],
691
      "dry_run": self.dryRun(),
692
      })
693

    
694

    
695
class R_2_groups_name_modify(baserlib.OpcodeResource):
696
  """/2/groups/[group_name]/modify resource.
697

698
  """
699
  PUT_OPCODE = opcodes.OpGroupSetParams
700

    
701
  def GetPutOpInput(self):
702
    """Changes some parameters of node group.
703

704
    """
705
    assert self.items
706
    return (self.request_body, {
707
      "group_name": self.items[0],
708
      })
709

    
710

    
711
class R_2_groups_name_rename(baserlib.OpcodeResource):
712
  """/2/groups/[group_name]/rename resource.
713

714
  """
715
  PUT_OPCODE = opcodes.OpGroupRename
716

    
717
  def GetPutOpInput(self):
718
    """Changes the name of a node group.
719

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

    
727

    
728
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
729
  """/2/groups/[group_name]/assign-nodes resource.
730

731
  """
732
  PUT_OPCODE = opcodes.OpGroupAssignNodes
733

    
734
  def GetPutOpInput(self):
735
    """Assigns nodes to a group.
736

737
    """
738
    assert len(self.items) == 1
739
    return (self.request_body, {
740
      "group_name": self.items[0],
741
      "dry_run": self.dryRun(),
742
      "force": self.useForce(),
743
      })
744

    
745

    
746
class R_2_instances(baserlib.OpcodeResource):
747
  """/2/instances resource.
748

749
  """
750
  GET_OPCODE = opcodes.OpInstanceQuery
751
  POST_OPCODE = opcodes.OpInstanceCreate
752
  POST_RENAME = {
753
    "os": "os_type",
754
    "name": "instance_name",
755
    }
756

    
757
  def GET(self):
758
    """Returns a list of all available instances.
759

760
    """
761
    client = self.GetClient()
762

    
763
    use_locking = self.useLocking()
764
    if self.useBulk():
765
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
766
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
767
    else:
768
      instancesdata = client.QueryInstances([], ["name"], use_locking)
769
      instanceslist = [row[0] for row in instancesdata]
770
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
771
                                   uri_fields=("id", "uri"))
772

    
773
  def GetPostOpInput(self):
774
    """Create an instance.
775

776
    @return: a job id
777

778
    """
779
    baserlib.CheckType(self.request_body, dict, "Body contents")
780

    
781
    # Default to request data version 0
782
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
783

    
784
    if data_version == 0:
785
      raise http.HttpBadRequest("Instance creation request version 0 is no"
786
                                " longer supported")
787
    elif data_version != 1:
788
      raise http.HttpBadRequest("Unsupported request data version %s" %
789
                                data_version)
790

    
791
    data = self.request_body.copy()
792
    # Remove "__version__"
793
    data.pop(_REQ_DATA_VERSION, None)
794

    
795
    return (data, {
796
      "dry_run": self.dryRun(),
797
      })
798

    
799

    
800
class R_2_instances_name(baserlib.OpcodeResource):
801
  """/2/instances/[instance_name] resource.
802

803
  """
804
  GET_OPCODE = opcodes.OpInstanceQuery
805
  DELETE_OPCODE = opcodes.OpInstanceRemove
806

    
807
  def GET(self):
808
    """Send information about an instance.
809

810
    """
811
    client = self.GetClient()
812
    instance_name = self.items[0]
813

    
814
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
815
                                            names=[instance_name],
816
                                            fields=I_FIELDS,
817
                                            use_locking=self.useLocking())
818

    
819
    return baserlib.MapFields(I_FIELDS, result[0])
820

    
821
  def GetDeleteOpInput(self):
822
    """Delete an instance.
823

824
    """
825
    assert len(self.items) == 1
826
    return ({}, {
827
      "instance_name": self.items[0],
828
      "ignore_failures": False,
829
      "dry_run": self.dryRun(),
830
      })
831

    
832

    
833
class R_2_instances_name_info(baserlib.OpcodeResource):
834
  """/2/instances/[instance_name]/info resource.
835

836
  """
837
  GET_OPCODE = opcodes.OpInstanceQueryData
838

    
839
  def GetGetOpInput(self):
840
    """Request detailed instance information.
841

842
    """
843
    assert len(self.items) == 1
844
    return ({}, {
845
      "instances": [self.items[0]],
846
      "static": bool(self._checkIntVariable("static", default=0)),
847
      })
848

    
849

    
850
class R_2_instances_name_reboot(baserlib.OpcodeResource):
851
  """/2/instances/[instance_name]/reboot resource.
852

853
  Implements an instance reboot.
854

855
  """
856
  POST_OPCODE = opcodes.OpInstanceReboot
857

    
858
  def GetPostOpInput(self):
859
    """Reboot an instance.
860

861
    The URI takes type=[hard|soft|full] and
862
    ignore_secondaries=[False|True] parameters.
863

864
    """
865
    return ({}, {
866
      "instance_name": self.items[0],
867
      "reboot_type":
868
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
869
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
870
      "dry_run": self.dryRun(),
871
      })
872

    
873

    
874
class R_2_instances_name_startup(baserlib.OpcodeResource):
875
  """/2/instances/[instance_name]/startup resource.
876

877
  Implements an instance startup.
878

879
  """
880
  PUT_OPCODE = opcodes.OpInstanceStartup
881

    
882
  def GetPutOpInput(self):
883
    """Startup an instance.
884

885
    The URI takes force=[False|True] parameter to start the instance
886
    if even if secondary disks are failing.
887

888
    """
889
    return ({}, {
890
      "instance_name": self.items[0],
891
      "force": self.useForce(),
892
      "dry_run": self.dryRun(),
893
      "no_remember": bool(self._checkIntVariable("no_remember")),
894
      })
895

    
896

    
897
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
898
  """/2/instances/[instance_name]/shutdown resource.
899

900
  Implements an instance shutdown.
901

902
  """
903
  PUT_OPCODE = opcodes.OpInstanceShutdown
904

    
905
  def GetPutOpInput(self):
906
    """Shutdown an instance.
907

908
    """
909
    return (self.request_body, {
910
      "instance_name": self.items[0],
911
      "no_remember": bool(self._checkIntVariable("no_remember")),
912
      "dry_run": self.dryRun(),
913
      })
914

    
915

    
916
def _ParseInstanceReinstallRequest(name, data):
917
  """Parses a request for reinstalling an instance.
918

919
  """
920
  if not isinstance(data, dict):
921
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
922

    
923
  ostype = baserlib.CheckParameter(data, "os", default=None)
924
  start = baserlib.CheckParameter(data, "start", exptype=bool,
925
                                  default=True)
926
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
927

    
928
  ops = [
929
    opcodes.OpInstanceShutdown(instance_name=name),
930
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
931
                                osparams=osparams),
932
    ]
933

    
934
  if start:
935
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
936

    
937
  return ops
938

    
939

    
940
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
941
  """/2/instances/[instance_name]/reinstall resource.
942

943
  Implements an instance reinstall.
944

945
  """
946
  POST_OPCODE = opcodes.OpInstanceReinstall
947

    
948
  def POST(self):
949
    """Reinstall an instance.
950

951
    The URI takes os=name and nostartup=[0|1] optional
952
    parameters. By default, the instance will be started
953
    automatically.
954

955
    """
956
    if self.request_body:
957
      if self.queryargs:
958
        raise http.HttpBadRequest("Can't combine query and body parameters")
959

    
960
      body = self.request_body
961
    elif self.queryargs:
962
      # Legacy interface, do not modify/extend
963
      body = {
964
        "os": self._checkStringVariable("os"),
965
        "start": not self._checkIntVariable("nostartup"),
966
        }
967
    else:
968
      body = {}
969

    
970
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
971

    
972
    return self.SubmitJob(ops)
973

    
974

    
975
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
976
  """/2/instances/[instance_name]/replace-disks resource.
977

978
  """
979
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
980

    
981
  def GetPostOpInput(self):
982
    """Replaces disks on an instance.
983

984
    """
985
    static = {
986
      "instance_name": self.items[0],
987
      }
988

    
989
    if self.request_body:
990
      data = self.request_body
991
    elif self.queryargs:
992
      # Legacy interface, do not modify/extend
993
      data = {
994
        "remote_node": self._checkStringVariable("remote_node", default=None),
995
        "mode": self._checkStringVariable("mode", default=None),
996
        "disks": self._checkStringVariable("disks", default=None),
997
        "iallocator": self._checkStringVariable("iallocator", default=None),
998
        }
999
    else:
1000
      data = {}
1001

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

    
1018
    return (data, static)
1019

    
1020

    
1021
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1022
  """/2/instances/[instance_name]/activate-disks resource.
1023

1024
  """
1025
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1026

    
1027
  def GetPutOpInput(self):
1028
    """Activate disks for an instance.
1029

1030
    The URI might contain ignore_size to ignore current recorded size.
1031

1032
    """
1033
    return ({}, {
1034
      "instance_name": self.items[0],
1035
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
1036
      })
1037

    
1038

    
1039
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1040
  """/2/instances/[instance_name]/deactivate-disks resource.
1041

1042
  """
1043
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1044

    
1045
  def GetPutOpInput(self):
1046
    """Deactivate disks for an instance.
1047

1048
    """
1049
    return ({}, {
1050
      "instance_name": self.items[0],
1051
      })
1052

    
1053

    
1054
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1055
  """/2/instances/[instance_name]/recreate-disks resource.
1056

1057
  """
1058
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1059

    
1060
  def GetPostOpInput(self):
1061
    """Recreate disks for an instance.
1062

1063
    """
1064
    return ({}, {
1065
      "instance_name": self.items[0],
1066
      })
1067

    
1068

    
1069
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1070
  """/2/instances/[instance_name]/prepare-export resource.
1071

1072
  """
1073
  PUT_OPCODE = opcodes.OpBackupPrepare
1074

    
1075
  def GetPutOpInput(self):
1076
    """Prepares an export for an instance.
1077

1078
    """
1079
    return ({}, {
1080
      "instance_name": self.items[0],
1081
      "mode": self._checkStringVariable("mode"),
1082
      })
1083

    
1084

    
1085
class R_2_instances_name_export(baserlib.OpcodeResource):
1086
  """/2/instances/[instance_name]/export resource.
1087

1088
  """
1089
  PUT_OPCODE = opcodes.OpBackupExport
1090
  PUT_RENAME = {
1091
    "destination": "target_node",
1092
    }
1093

    
1094
  def GetPutOpInput(self):
1095
    """Exports an instance.
1096

1097
    """
1098
    return (self.request_body, {
1099
      "instance_name": self.items[0],
1100
      })
1101

    
1102

    
1103
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1104
  """/2/instances/[instance_name]/migrate resource.
1105

1106
  """
1107
  PUT_OPCODE = opcodes.OpInstanceMigrate
1108

    
1109
  def GetPutOpInput(self):
1110
    """Migrates an instance.
1111

1112
    """
1113
    return (self.request_body, {
1114
      "instance_name": self.items[0],
1115
      })
1116

    
1117

    
1118
class R_2_instances_name_failover(baserlib.OpcodeResource):
1119
  """/2/instances/[instance_name]/failover resource.
1120

1121
  """
1122
  PUT_OPCODE = opcodes.OpInstanceFailover
1123

    
1124
  def GetPutOpInput(self):
1125
    """Does a failover of an instance.
1126

1127
    """
1128
    return (self.request_body, {
1129
      "instance_name": self.items[0],
1130
      })
1131

    
1132

    
1133
class R_2_instances_name_rename(baserlib.OpcodeResource):
1134
  """/2/instances/[instance_name]/rename resource.
1135

1136
  """
1137
  PUT_OPCODE = opcodes.OpInstanceRename
1138

    
1139
  def GetPutOpInput(self):
1140
    """Changes the name of an instance.
1141

1142
    """
1143
    return (self.request_body, {
1144
      "instance_name": self.items[0],
1145
      })
1146

    
1147

    
1148
class R_2_instances_name_modify(baserlib.OpcodeResource):
1149
  """/2/instances/[instance_name]/modify resource.
1150

1151
  """
1152
  PUT_OPCODE = opcodes.OpInstanceSetParams
1153

    
1154
  def GetPutOpInput(self):
1155
    """Changes parameters of an instance.
1156

1157
    """
1158
    return (self.request_body, {
1159
      "instance_name": self.items[0],
1160
      })
1161

    
1162

    
1163
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1164
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1165

1166
  """
1167
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1168

    
1169
  def GetPostOpInput(self):
1170
    """Increases the size of an instance disk.
1171

1172
    """
1173
    return (self.request_body, {
1174
      "instance_name": self.items[0],
1175
      "disk": int(self.items[1]),
1176
      })
1177

    
1178

    
1179
class R_2_instances_name_console(baserlib.ResourceBase):
1180
  """/2/instances/[instance_name]/console resource.
1181

1182
  """
1183
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1184
  GET_OPCODE = opcodes.OpInstanceConsole
1185

    
1186
  def GET(self):
1187
    """Request information for connecting to instance's console.
1188

1189
    @return: Serialized instance console description, see
1190
             L{objects.InstanceConsole}
1191

1192
    """
1193
    client = self.GetClient()
1194

    
1195
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1196

    
1197
    if console is None:
1198
      raise http.HttpServiceUnavailable("Instance console unavailable")
1199

    
1200
    assert isinstance(console, dict)
1201
    return console
1202

    
1203

    
1204
def _GetQueryFields(args):
1205
  """
1206

1207
  """
1208
  try:
1209
    fields = args["fields"]
1210
  except KeyError:
1211
    raise http.HttpBadRequest("Missing 'fields' query argument")
1212

    
1213
  return _SplitQueryFields(fields[0])
1214

    
1215

    
1216
def _SplitQueryFields(fields):
1217
  """
1218

1219
  """
1220
  return [i.strip() for i in fields.split(",")]
1221

    
1222

    
1223
class R_2_query(baserlib.ResourceBase):
1224
  """/2/query/[resource] resource.
1225

1226
  """
1227
  # Results might contain sensitive information
1228
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1229
  GET_OPCODE = opcodes.OpQuery
1230
  PUT_OPCODE = opcodes.OpQuery
1231

    
1232
  def _Query(self, fields, qfilter):
1233
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1234

    
1235
  def GET(self):
1236
    """Returns resource information.
1237

1238
    @return: Query result, see L{objects.QueryResponse}
1239

1240
    """
1241
    return self._Query(_GetQueryFields(self.queryargs), None)
1242

    
1243
  def PUT(self):
1244
    """Submits job querying for resources.
1245

1246
    @return: Query result, see L{objects.QueryResponse}
1247

1248
    """
1249
    body = self.request_body
1250

    
1251
    baserlib.CheckType(body, dict, "Body contents")
1252

    
1253
    try:
1254
      fields = body["fields"]
1255
    except KeyError:
1256
      fields = _GetQueryFields(self.queryargs)
1257

    
1258
    qfilter = body.get("qfilter", None)
1259
    # TODO: remove this after 2.7
1260
    if qfilter is None:
1261
      qfilter = body.get("filter", None)
1262

    
1263
    return self._Query(fields, qfilter)
1264

    
1265

    
1266
class R_2_query_fields(baserlib.ResourceBase):
1267
  """/2/query/[resource]/fields resource.
1268

1269
  """
1270
  GET_OPCODE = opcodes.OpQueryFields
1271

    
1272
  def GET(self):
1273
    """Retrieves list of available fields for a resource.
1274

1275
    @return: List of serialized L{objects.QueryFieldDefinition}
1276

1277
    """
1278
    try:
1279
      raw_fields = self.queryargs["fields"]
1280
    except KeyError:
1281
      fields = None
1282
    else:
1283
      fields = _SplitQueryFields(raw_fields[0])
1284

    
1285
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1286

    
1287

    
1288
class _R_Tags(baserlib.OpcodeResource):
1289
  """Quasiclass for tagging resources.
1290

1291
  Manages tags. When inheriting this class you must define the
1292
  TAG_LEVEL for it.
1293

1294
  """
1295
  TAG_LEVEL = None
1296
  GET_OPCODE = opcodes.OpTagsGet
1297
  PUT_OPCODE = opcodes.OpTagsSet
1298
  DELETE_OPCODE = opcodes.OpTagsDel
1299

    
1300
  def __init__(self, items, queryargs, req, **kwargs):
1301
    """A tag resource constructor.
1302

1303
    We have to override the default to sort out cluster naming case.
1304

1305
    """
1306
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1307

    
1308
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1309
      self.name = None
1310
    else:
1311
      self.name = items[0]
1312

    
1313
  def GET(self):
1314
    """Returns a list of tags.
1315

1316
    Example: ["tag1", "tag2", "tag3"]
1317

1318
    """
1319
    kind = self.TAG_LEVEL
1320

    
1321
    if kind in (constants.TAG_INSTANCE,
1322
                constants.TAG_NODEGROUP,
1323
                constants.TAG_NODE):
1324
      if not self.name:
1325
        raise http.HttpBadRequest("Missing name on tag request")
1326

    
1327
      cl = self.GetClient()
1328
      if kind == constants.TAG_INSTANCE:
1329
        fn = cl.QueryInstances
1330
      elif kind == constants.TAG_NODEGROUP:
1331
        fn = cl.QueryGroups
1332
      else:
1333
        fn = cl.QueryNodes
1334
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1335
      if not result or not result[0]:
1336
        raise http.HttpBadGateway("Invalid response from tag query")
1337
      tags = result[0][0]
1338

    
1339
    elif kind == constants.TAG_CLUSTER:
1340
      assert not self.name
1341
      # TODO: Use query API?
1342
      ssc = ssconf.SimpleStore()
1343
      tags = ssc.GetClusterTags()
1344

    
1345
    return list(tags)
1346

    
1347
  def GetPutOpInput(self):
1348
    """Add a set of tags.
1349

1350
    The request as a list of strings should be PUT to this URI. And
1351
    you'll have back a job id.
1352

1353
    """
1354
    return ({}, {
1355
      "kind": self.TAG_LEVEL,
1356
      "name": self.name,
1357
      "tags": self.queryargs.get("tag", []),
1358
      "dry_run": self.dryRun(),
1359
      })
1360

    
1361
  def GetDeleteOpInput(self):
1362
    """Delete a tag.
1363

1364
    In order to delete a set of tags, the DELETE
1365
    request should be addressed to URI like:
1366
    /tags?tag=[tag]&tag=[tag]
1367

1368
    """
1369
    # Re-use code
1370
    return self.GetPutOpInput()
1371

    
1372

    
1373
class R_2_instances_name_tags(_R_Tags):
1374
  """ /2/instances/[instance_name]/tags resource.
1375

1376
  Manages per-instance tags.
1377

1378
  """
1379
  TAG_LEVEL = constants.TAG_INSTANCE
1380

    
1381

    
1382
class R_2_nodes_name_tags(_R_Tags):
1383
  """ /2/nodes/[node_name]/tags resource.
1384

1385
  Manages per-node tags.
1386

1387
  """
1388
  TAG_LEVEL = constants.TAG_NODE
1389

    
1390

    
1391
class R_2_groups_name_tags(_R_Tags):
1392
  """ /2/groups/[group_name]/tags resource.
1393

1394
  Manages per-nodegroup tags.
1395

1396
  """
1397
  TAG_LEVEL = constants.TAG_NODEGROUP
1398

    
1399

    
1400
class R_2_tags(_R_Tags):
1401
  """ /2/tags resource.
1402

1403
  Manages cluster tags.
1404

1405
  """
1406
  TAG_LEVEL = constants.TAG_CLUSTER