Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 460ef073

History | View | Annotate | Download (32.4 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-msg=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
  ] + _COMMON_FIELDS
98

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

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

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

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

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

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

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

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

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

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

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

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

    
151

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

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

160
    """
161
    return None
162

    
163

    
164
class R_version(baserlib.ResourceBase):
165
  """/version resource.
166

167
  This resource should be used to determine the remote API version and
168
  to adapt clients accordingly.
169

170
  """
171
  @staticmethod
172
  def GET():
173
    """Returns the remote API version.
174

175
    """
176
    return constants.RAPI_VERSION
177

    
178

    
179
class R_2_info(baserlib.ResourceBase):
180
  """/2/info resource.
181

182
  """
183
  def GET(self):
184
    """Returns cluster information.
185

186
    """
187
    client = self.GetClient()
188
    return client.QueryClusterInfo()
189

    
190

    
191
class R_2_features(baserlib.ResourceBase):
192
  """/2/features resource.
193

194
  """
195
  @staticmethod
196
  def GET():
197
    """Returns list of optional RAPI features implemented.
198

199
    """
200
    return list(ALL_FEATURES)
201

    
202

    
203
class R_2_os(baserlib.ResourceBase):
204
  """/2/os resource.
205

206
  """
207
  def GET(self):
208
    """Return a list of all OSes.
209

210
    Can return error 500 in case of a problem.
211

212
    Example: ["debian-etch"]
213

214
    """
215
    cl = self.GetClient()
216
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
217
    job_id = self.SubmitJob([op], cl=cl)
218
    # we use custom feedback function, instead of print we log the status
219
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
220
    diagnose_data = result[0]
221

    
222
    if not isinstance(diagnose_data, list):
223
      raise http.HttpBadGateway(message="Can't get OS list")
224

    
225
    os_names = []
226
    for (name, variants) in diagnose_data:
227
      os_names.extend(cli.CalculateOSNames(name, variants))
228

    
229
    return os_names
230

    
231

    
232
class R_2_redist_config(baserlib.OpcodeResource):
233
  """/2/redistribute-config resource.
234

235
  """
236
  PUT_OPCODE = opcodes.OpClusterRedistConf
237

    
238

    
239
class R_2_cluster_modify(baserlib.OpcodeResource):
240
  """/2/modify resource.
241

242
  """
243
  PUT_OPCODE = opcodes.OpClusterSetParams
244

    
245

    
246
class R_2_jobs(baserlib.ResourceBase):
247
  """/2/jobs resource.
248

249
  """
250
  def GET(self):
251
    """Returns a dictionary of jobs.
252

253
    @return: a dictionary with jobs id and uri.
254

255
    """
256
    client = self.GetClient()
257

    
258
    if self.useBulk():
259
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
260
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
261
    else:
262
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
263
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
264
                                   uri_fields=("id", "uri"))
265

    
266

    
267
class R_2_jobs_id(baserlib.ResourceBase):
268
  """/2/jobs/[job_id] resource.
269

270
  """
271
  def GET(self):
272
    """Returns a job status.
273

274
    @return: a dictionary with job parameters.
275
        The result includes:
276
            - id: job ID as a number
277
            - status: current job status as a string
278
            - ops: involved OpCodes as a list of dictionaries for each
279
              opcodes in the job
280
            - opstatus: OpCodes status as a list
281
            - opresult: OpCodes results as a list of lists
282

283
    """
284
    job_id = self.items[0]
285
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
286
    if result is None:
287
      raise http.HttpNotFound()
288
    return baserlib.MapFields(J_FIELDS, result)
289

    
290
  def DELETE(self):
291
    """Cancel not-yet-started job.
292

293
    """
294
    job_id = self.items[0]
295
    result = self.GetClient().CancelJob(job_id)
296
    return result
297

    
298

    
299
class R_2_jobs_id_wait(baserlib.ResourceBase):
300
  """/2/jobs/[job_id]/wait resource.
301

302
  """
303
  # WaitForJobChange provides access to sensitive information and blocks
304
  # machine resources (it's a blocking RAPI call), hence restricting access.
305
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
306

    
307
  def GET(self):
308
    """Waits for job changes.
309

310
    """
311
    job_id = self.items[0]
312

    
313
    fields = self.getBodyParameter("fields")
314
    prev_job_info = self.getBodyParameter("previous_job_info", None)
315
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
316

    
317
    if not isinstance(fields, list):
318
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
319

    
320
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
321
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
322
                                " be a list")
323

    
324
    if not (prev_log_serial is None or
325
            isinstance(prev_log_serial, (int, long))):
326
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
327
                                " be a number")
328

    
329
    client = self.GetClient()
330
    result = client.WaitForJobChangeOnce(job_id, fields,
331
                                         prev_job_info, prev_log_serial,
332
                                         timeout=_WFJC_TIMEOUT)
333
    if not result:
334
      raise http.HttpNotFound()
335

    
336
    if result == constants.JOB_NOTCHANGED:
337
      # No changes
338
      return None
339

    
340
    (job_info, log_entries) = result
341

    
342
    return {
343
      "job_info": job_info,
344
      "log_entries": log_entries,
345
      }
346

    
347

    
348
class R_2_nodes(baserlib.ResourceBase):
349
  """/2/nodes resource.
350

351
  """
352
  def GET(self):
353
    """Returns a list of all nodes.
354

355
    """
356
    client = self.GetClient()
357

    
358
    if self.useBulk():
359
      bulkdata = client.QueryNodes([], N_FIELDS, False)
360
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
361
    else:
362
      nodesdata = client.QueryNodes([], ["name"], False)
363
      nodeslist = [row[0] for row in nodesdata]
364
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
365
                                   uri_fields=("id", "uri"))
366

    
367

    
368
class R_2_nodes_name(baserlib.ResourceBase):
369
  """/2/nodes/[node_name] resource.
370

371
  """
372
  def GET(self):
373
    """Send information about a node.
374

375
    """
376
    node_name = self.items[0]
377
    client = self.GetClient()
378

    
379
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
380
                                            names=[node_name], fields=N_FIELDS,
381
                                            use_locking=self.useLocking())
382

    
383
    return baserlib.MapFields(N_FIELDS, result[0])
384

    
385

    
386
class R_2_nodes_name_role(baserlib.OpcodeResource):
387
  """/2/nodes/[node_name]/role resource.
388

389
  """
390
  PUT_OPCODE = opcodes.OpNodeSetParams
391

    
392
  def GET(self):
393
    """Returns the current node role.
394

395
    @return: Node role
396

397
    """
398
    node_name = self.items[0]
399
    client = self.GetClient()
400
    result = client.QueryNodes(names=[node_name], fields=["role"],
401
                               use_locking=self.useLocking())
402

    
403
    return _NR_MAP[result[0][0]]
404

    
405
  def GetPutOpInput(self):
406
    """Sets the node role.
407

408
    """
409
    baserlib.CheckType(self.request_body, basestring, "Body contents")
410

    
411
    role = self.request_body
412

    
413
    if role == _NR_REGULAR:
414
      candidate = False
415
      offline = False
416
      drained = False
417

    
418
    elif role == _NR_MASTER_CANDIDATE:
419
      candidate = True
420
      offline = drained = None
421

    
422
    elif role == _NR_DRAINED:
423
      drained = True
424
      candidate = offline = None
425

    
426
    elif role == _NR_OFFLINE:
427
      offline = True
428
      candidate = drained = None
429

    
430
    else:
431
      raise http.HttpBadRequest("Can't set '%s' role" % role)
432

    
433
    assert len(self.items) == 1
434

    
435
    return ({}, {
436
      "node_name": self.items[0],
437
      "master_candidate": candidate,
438
      "offline": offline,
439
      "drained": drained,
440
      "force": self.useForce(),
441
      })
442

    
443

    
444
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
445
  """/2/nodes/[node_name]/evacuate resource.
446

447
  """
448
  POST_OPCODE = opcodes.OpNodeEvacuate
449

    
450
  def GetPostOpInput(self):
451
    """Evacuate all instances off a node.
452

453
    """
454
    return (self.request_body, {
455
      "node_name": self.items[0],
456
      "dry_run": self.dryRun(),
457
      })
458

    
459

    
460
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
461
  """/2/nodes/[node_name]/migrate resource.
462

463
  """
464
  POST_OPCODE = opcodes.OpNodeMigrate
465

    
466
  def GetPostOpInput(self):
467
    """Migrate all primary instances from a node.
468

469
    """
470
    if self.queryargs:
471
      # Support old-style requests
472
      if "live" in self.queryargs and "mode" in self.queryargs:
473
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
474
                                  " be passed")
475

    
476
      if "live" in self.queryargs:
477
        if self._checkIntVariable("live", default=1):
478
          mode = constants.HT_MIGRATION_LIVE
479
        else:
480
          mode = constants.HT_MIGRATION_NONLIVE
481
      else:
482
        mode = self._checkStringVariable("mode", default=None)
483

    
484
      data = {
485
        "mode": mode,
486
        }
487
    else:
488
      data = self.request_body
489

    
490
    return (data, {
491
      "node_name": self.items[0],
492
      })
493

    
494

    
495
class R_2_nodes_name_storage(baserlib.OpcodeResource):
496
  """/2/nodes/[node_name]/storage resource.
497

498
  """
499
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
500
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
501
  GET_OPCODE = opcodes.OpNodeQueryStorage
502

    
503
  def GetGetOpInput(self):
504
    """List storage available on a node.
505

506
    """
507
    storage_type = self._checkStringVariable("storage_type", None)
508
    output_fields = self._checkStringVariable("output_fields", None)
509

    
510
    if not output_fields:
511
      raise http.HttpBadRequest("Missing the required 'output_fields'"
512
                                " parameter")
513

    
514
    return ({}, {
515
      "nodes": [self.items[0]],
516
      "storage_type": storage_type,
517
      "output_fields": output_fields.split(","),
518
      })
519

    
520

    
521
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
522
  """/2/nodes/[node_name]/storage/modify resource.
523

524
  """
525
  PUT_OPCODE = opcodes.OpNodeModifyStorage
526

    
527
  def GetPutOpInput(self):
528
    """Modifies a storage volume on a node.
529

530
    """
531
    storage_type = self._checkStringVariable("storage_type", None)
532
    name = self._checkStringVariable("name", None)
533

    
534
    if not name:
535
      raise http.HttpBadRequest("Missing the required 'name'"
536
                                " parameter")
537

    
538
    changes = {}
539

    
540
    if "allocatable" in self.queryargs:
541
      changes[constants.SF_ALLOCATABLE] = \
542
        bool(self._checkIntVariable("allocatable", default=1))
543

    
544
    return ({}, {
545
      "node_name": self.items[0],
546
      "storage_type": storage_type,
547
      "name": name,
548
      "changes": changes,
549
      })
550

    
551

    
552
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
553
  """/2/nodes/[node_name]/storage/repair resource.
554

555
  """
556
  PUT_OPCODE = opcodes.OpRepairNodeStorage
557

    
558
  def GetPutOpInput(self):
559
    """Repairs a storage volume on a node.
560

561
    """
562
    storage_type = self._checkStringVariable("storage_type", None)
563
    name = self._checkStringVariable("name", None)
564
    if not name:
565
      raise http.HttpBadRequest("Missing the required 'name'"
566
                                " parameter")
567

    
568
    return ({}, {
569
      "node_name": self.items[0],
570
      "storage_type": storage_type,
571
      "name": name,
572
      })
573

    
574

    
575
class R_2_groups(baserlib.OpcodeResource):
576
  """/2/groups resource.
577

578
  """
579
  POST_OPCODE = opcodes.OpGroupAdd
580
  POST_RENAME = {
581
    "name": "group_name",
582
    }
583

    
584
  def GetPostOpInput(self):
585
    """Create a node group.
586

587
    """
588
    assert not self.items
589
    return (self.request_body, {
590
      "dry_run": self.dryRun(),
591
      })
592

    
593
  def GET(self):
594
    """Returns a list of all node groups.
595

596
    """
597
    client = self.GetClient()
598

    
599
    if self.useBulk():
600
      bulkdata = client.QueryGroups([], G_FIELDS, False)
601
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
602
    else:
603
      data = client.QueryGroups([], ["name"], False)
604
      groupnames = [row[0] for row in data]
605
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
606
                                   uri_fields=("name", "uri"))
607

    
608

    
609
class R_2_groups_name(baserlib.OpcodeResource):
610
  """/2/groups/[group_name] resource.
611

612
  """
613
  DELETE_OPCODE = opcodes.OpGroupRemove
614

    
615
  def GET(self):
616
    """Send information about a node group.
617

618
    """
619
    group_name = self.items[0]
620
    client = self.GetClient()
621

    
622
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
623
                                            names=[group_name], fields=G_FIELDS,
624
                                            use_locking=self.useLocking())
625

    
626
    return baserlib.MapFields(G_FIELDS, result[0])
627

    
628
  def GetDeleteOpInput(self):
629
    """Delete a node group.
630

631
    """
632
    assert len(self.items) == 1
633
    return ({}, {
634
      "group_name": self.items[0],
635
      "dry_run": self.dryRun(),
636
      })
637

    
638

    
639
class R_2_groups_name_modify(baserlib.OpcodeResource):
640
  """/2/groups/[group_name]/modify resource.
641

642
  """
643
  PUT_OPCODE = opcodes.OpGroupSetParams
644

    
645
  def GetPutOpInput(self):
646
    """Changes some parameters of node group.
647

648
    """
649
    assert self.items
650
    return (self.request_body, {
651
      "group_name": self.items[0],
652
      })
653

    
654

    
655
class R_2_groups_name_rename(baserlib.OpcodeResource):
656
  """/2/groups/[group_name]/rename resource.
657

658
  """
659
  PUT_OPCODE = opcodes.OpGroupRename
660

    
661
  def GetPutOpInput(self):
662
    """Changes the name of a node group.
663

664
    """
665
    assert len(self.items) == 1
666
    return (self.request_body, {
667
      "group_name": self.items[0],
668
      "dry_run": self.dryRun(),
669
      })
670

    
671

    
672
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
673
  """/2/groups/[group_name]/assign-nodes resource.
674

675
  """
676
  PUT_OPCODE = opcodes.OpGroupAssignNodes
677

    
678
  def GetPutOpInput(self):
679
    """Assigns nodes to a group.
680

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

    
689

    
690
def _ParseInstanceCreateRequestVersion1(data, dry_run):
691
  """Parses an instance creation request version 1.
692

693
  @rtype: L{opcodes.OpInstanceCreate}
694
  @return: Instance creation opcode
695

696
  """
697
  override = {
698
    "dry_run": dry_run,
699
    }
700

    
701
  rename = {
702
    "os": "os_type",
703
    "name": "instance_name",
704
    }
705

    
706
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
707
                             rename=rename)
708

    
709

    
710
class R_2_instances(baserlib.ResourceBase):
711
  """/2/instances resource.
712

713
  """
714
  def GET(self):
715
    """Returns a list of all available instances.
716

717
    """
718
    client = self.GetClient()
719

    
720
    use_locking = self.useLocking()
721
    if self.useBulk():
722
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
723
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
724
    else:
725
      instancesdata = client.QueryInstances([], ["name"], use_locking)
726
      instanceslist = [row[0] for row in instancesdata]
727
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
728
                                   uri_fields=("id", "uri"))
729

    
730
  def POST(self):
731
    """Create an instance.
732

733
    @return: a job id
734

735
    """
736
    if not isinstance(self.request_body, dict):
737
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
738

    
739
    # Default to request data version 0
740
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
741

    
742
    if data_version == 0:
743
      raise http.HttpBadRequest("Instance creation request version 0 is no"
744
                                " longer supported")
745
    elif data_version == 1:
746
      data = self.request_body.copy()
747
      # Remove "__version__"
748
      data.pop(_REQ_DATA_VERSION, None)
749
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
750
    else:
751
      raise http.HttpBadRequest("Unsupported request data version %s" %
752
                                data_version)
753

    
754
    return self.SubmitJob([op])
755

    
756

    
757
class R_2_instances_name(baserlib.OpcodeResource):
758
  """/2/instances/[instance_name] resource.
759

760
  """
761
  DELETE_OPCODE = opcodes.OpInstanceRemove
762

    
763
  def GET(self):
764
    """Send information about an instance.
765

766
    """
767
    client = self.GetClient()
768
    instance_name = self.items[0]
769

    
770
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
771
                                            names=[instance_name],
772
                                            fields=I_FIELDS,
773
                                            use_locking=self.useLocking())
774

    
775
    return baserlib.MapFields(I_FIELDS, result[0])
776

    
777
  def GetDeleteOpInput(self):
778
    """Delete an instance.
779

780
    """
781
    assert len(self.items) == 1
782
    return ({}, {
783
      "instance_name": self.items[0],
784
      "ignore_failures": False,
785
      "dry_run": self.dryRun(),
786
      })
787

    
788

    
789
class R_2_instances_name_info(baserlib.OpcodeResource):
790
  """/2/instances/[instance_name]/info resource.
791

792
  """
793
  GET_OPCODE = opcodes.OpInstanceQueryData
794

    
795
  def GetGetOpInput(self):
796
    """Request detailed instance information.
797

798
    """
799
    assert len(self.items) == 1
800
    return ({}, {
801
      "instances": [self.items[0]],
802
      "static": bool(self._checkIntVariable("static", default=0)),
803
      })
804

    
805

    
806
class R_2_instances_name_reboot(baserlib.OpcodeResource):
807
  """/2/instances/[instance_name]/reboot resource.
808

809
  Implements an instance reboot.
810

811
  """
812
  POST_OPCODE = opcodes.OpInstanceReboot
813

    
814
  def GetPostOpInput(self):
815
    """Reboot an instance.
816

817
    The URI takes type=[hard|soft|full] and
818
    ignore_secondaries=[False|True] parameters.
819

820
    """
821
    return ({}, {
822
      "instance_name": self.items[0],
823
      "reboot_type":
824
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
825
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
826
      "dry_run": self.dryRun(),
827
      })
828

    
829

    
830
class R_2_instances_name_startup(baserlib.OpcodeResource):
831
  """/2/instances/[instance_name]/startup resource.
832

833
  Implements an instance startup.
834

835
  """
836
  PUT_OPCODE = opcodes.OpInstanceStartup
837

    
838
  def GetPutOpInput(self):
839
    """Startup an instance.
840

841
    The URI takes force=[False|True] parameter to start the instance
842
    if even if secondary disks are failing.
843

844
    """
845
    return ({}, {
846
      "instance_name": self.items[0],
847
      "force": self.useForce(),
848
      "dry_run": self.dryRun(),
849
      "no_remember": bool(self._checkIntVariable("no_remember")),
850
      })
851

    
852

    
853
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
854
  """/2/instances/[instance_name]/shutdown resource.
855

856
  Implements an instance shutdown.
857

858
  """
859
  PUT_OPCODE = opcodes.OpInstanceShutdown
860

    
861
  def GetPutOpInput(self):
862
    """Shutdown an instance.
863

864
    """
865
    return (self.request_body, {
866
      "instance_name": self.items[0],
867
      "no_remember": bool(self._checkIntVariable("no_remember")),
868
      "dry_run": self.dryRun(),
869
      })
870

    
871

    
872
def _ParseInstanceReinstallRequest(name, data):
873
  """Parses a request for reinstalling an instance.
874

875
  """
876
  if not isinstance(data, dict):
877
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
878

    
879
  ostype = baserlib.CheckParameter(data, "os", default=None)
880
  start = baserlib.CheckParameter(data, "start", exptype=bool,
881
                                  default=True)
882
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
883

    
884
  ops = [
885
    opcodes.OpInstanceShutdown(instance_name=name),
886
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
887
                                osparams=osparams),
888
    ]
889

    
890
  if start:
891
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
892

    
893
  return ops
894

    
895

    
896
class R_2_instances_name_reinstall(baserlib.ResourceBase):
897
  """/2/instances/[instance_name]/reinstall resource.
898

899
  Implements an instance reinstall.
900

901
  """
902
  def POST(self):
903
    """Reinstall an instance.
904

905
    The URI takes os=name and nostartup=[0|1] optional
906
    parameters. By default, the instance will be started
907
    automatically.
908

909
    """
910
    if self.request_body:
911
      if self.queryargs:
912
        raise http.HttpBadRequest("Can't combine query and body parameters")
913

    
914
      body = self.request_body
915
    elif self.queryargs:
916
      # Legacy interface, do not modify/extend
917
      body = {
918
        "os": self._checkStringVariable("os"),
919
        "start": not self._checkIntVariable("nostartup"),
920
        }
921
    else:
922
      body = {}
923

    
924
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
925

    
926
    return self.SubmitJob(ops)
927

    
928

    
929
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
930
  """/2/instances/[instance_name]/replace-disks resource.
931

932
  """
933
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
934

    
935
  def GetPostOpInput(self):
936
    """Replaces disks on an instance.
937

938
    """
939
    data = self.request_body.copy()
940
    static = {
941
      "instance_name": self.items[0],
942
      }
943

    
944
    # Parse disks
945
    try:
946
      raw_disks = data["disks"]
947
    except KeyError:
948
      pass
949
    else:
950
      if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
951
        # Backwards compatibility for strings of the format "1, 2, 3"
952
        try:
953
          data["disks"] = [int(part) for part in raw_disks.split(",")]
954
        except (TypeError, ValueError), err:
955
          raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
956

    
957
    return (data, static)
958

    
959

    
960
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
961
  """/2/instances/[instance_name]/activate-disks resource.
962

963
  """
964
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
965

    
966
  def GetPutOpInput(self):
967
    """Activate disks for an instance.
968

969
    The URI might contain ignore_size to ignore current recorded size.
970

971
    """
972
    return ({}, {
973
      "instance_name": self.items[0],
974
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
975
      })
976

    
977

    
978
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
979
  """/2/instances/[instance_name]/deactivate-disks resource.
980

981
  """
982
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
983

    
984
  def GetPutOpInput(self):
985
    """Deactivate disks for an instance.
986

987
    """
988
    return ({}, {
989
      "instance_name": self.items[0],
990
      })
991

    
992

    
993
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
994
  """/2/instances/[instance_name]/prepare-export resource.
995

996
  """
997
  PUT_OPCODE = opcodes.OpBackupPrepare
998

    
999
  def GetPutOpInput(self):
1000
    """Prepares an export for an instance.
1001

1002
    """
1003
    return ({}, {
1004
      "instance_name": self.items[0],
1005
      "mode": self._checkStringVariable("mode"),
1006
      })
1007

    
1008

    
1009
class R_2_instances_name_export(baserlib.OpcodeResource):
1010
  """/2/instances/[instance_name]/export resource.
1011

1012
  """
1013
  PUT_OPCODE = opcodes.OpBackupExport
1014
  PUT_RENAME = {
1015
    "destination": "target_node",
1016
    }
1017

    
1018
  def GetPutOpInput(self):
1019
    """Exports an instance.
1020

1021
    """
1022
    return (self.request_body, {
1023
      "instance_name": self.items[0],
1024
      })
1025

    
1026

    
1027
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1028
  """/2/instances/[instance_name]/migrate resource.
1029

1030
  """
1031
  PUT_OPCODE = opcodes.OpInstanceMigrate
1032

    
1033
  def GetPutOpInput(self):
1034
    """Migrates an instance.
1035

1036
    """
1037
    return (self.request_body, {
1038
      "instance_name": self.items[0],
1039
      })
1040

    
1041

    
1042
class R_2_instances_name_failover(baserlib.OpcodeResource):
1043
  """/2/instances/[instance_name]/failover resource.
1044

1045
  """
1046
  PUT_OPCODE = opcodes.OpInstanceFailover
1047

    
1048
  def GetPutOpInput(self):
1049
    """Does a failover of an instance.
1050

1051
    """
1052
    return (self.request_body, {
1053
      "instance_name": self.items[0],
1054
      })
1055

    
1056

    
1057
class R_2_instances_name_rename(baserlib.OpcodeResource):
1058
  """/2/instances/[instance_name]/rename resource.
1059

1060
  """
1061
  PUT_OPCODE = opcodes.OpInstanceRename
1062

    
1063
  def GetPutOpInput(self):
1064
    """Changes the name of an instance.
1065

1066
    """
1067
    return (self.request_body, {
1068
      "instance_name": self.items[0],
1069
      })
1070

    
1071

    
1072
class R_2_instances_name_modify(baserlib.OpcodeResource):
1073
  """/2/instances/[instance_name]/modify resource.
1074

1075
  """
1076
  PUT_OPCODE = opcodes.OpInstanceSetParams
1077

    
1078
  def GetPutOpInput(self):
1079
    """Changes parameters of an instance.
1080

1081
    """
1082
    return (self.request_body, {
1083
      "instance_name": self.items[0],
1084
      })
1085

    
1086

    
1087
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1088
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1089

1090
  """
1091
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1092

    
1093
  def GetPostOpInput(self):
1094
    """Increases the size of an instance disk.
1095

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

    
1102

    
1103
class R_2_instances_name_console(baserlib.ResourceBase):
1104
  """/2/instances/[instance_name]/console resource.
1105

1106
  """
1107
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1108

    
1109
  def GET(self):
1110
    """Request information for connecting to instance's console.
1111

1112
    @return: Serialized instance console description, see
1113
             L{objects.InstanceConsole}
1114

1115
    """
1116
    client = self.GetClient()
1117

    
1118
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1119

    
1120
    if console is None:
1121
      raise http.HttpServiceUnavailable("Instance console unavailable")
1122

    
1123
    assert isinstance(console, dict)
1124
    return console
1125

    
1126

    
1127
def _GetQueryFields(args):
1128
  """
1129

1130
  """
1131
  try:
1132
    fields = args["fields"]
1133
  except KeyError:
1134
    raise http.HttpBadRequest("Missing 'fields' query argument")
1135

    
1136
  return _SplitQueryFields(fields[0])
1137

    
1138

    
1139
def _SplitQueryFields(fields):
1140
  """
1141

1142
  """
1143
  return [i.strip() for i in fields.split(",")]
1144

    
1145

    
1146
class R_2_query(baserlib.ResourceBase):
1147
  """/2/query/[resource] resource.
1148

1149
  """
1150
  # Results might contain sensitive information
1151
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1152

    
1153
  def _Query(self, fields, filter_):
1154
    return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1155

    
1156
  def GET(self):
1157
    """Returns resource information.
1158

1159
    @return: Query result, see L{objects.QueryResponse}
1160

1161
    """
1162
    return self._Query(_GetQueryFields(self.queryargs), None)
1163

    
1164
  def PUT(self):
1165
    """Submits job querying for resources.
1166

1167
    @return: Query result, see L{objects.QueryResponse}
1168

1169
    """
1170
    body = self.request_body
1171

    
1172
    baserlib.CheckType(body, dict, "Body contents")
1173

    
1174
    try:
1175
      fields = body["fields"]
1176
    except KeyError:
1177
      fields = _GetQueryFields(self.queryargs)
1178

    
1179
    return self._Query(fields, self.request_body.get("filter", None))
1180

    
1181

    
1182
class R_2_query_fields(baserlib.ResourceBase):
1183
  """/2/query/[resource]/fields resource.
1184

1185
  """
1186
  def GET(self):
1187
    """Retrieves list of available fields for a resource.
1188

1189
    @return: List of serialized L{objects.QueryFieldDefinition}
1190

1191
    """
1192
    try:
1193
      raw_fields = self.queryargs["fields"]
1194
    except KeyError:
1195
      fields = None
1196
    else:
1197
      fields = _SplitQueryFields(raw_fields[0])
1198

    
1199
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1200

    
1201

    
1202
class _R_Tags(baserlib.OpcodeResource):
1203
  """ Quasiclass for tagging resources
1204

1205
  Manages tags. When inheriting this class you must define the
1206
  TAG_LEVEL for it.
1207

1208
  """
1209
  TAG_LEVEL = None
1210
  PUT_OPCODE = opcodes.OpTagsSet
1211
  DELETE_OPCODE = opcodes.OpTagsDel
1212

    
1213
  def __init__(self, items, queryargs, req, **kwargs):
1214
    """A tag resource constructor.
1215

1216
    We have to override the default to sort out cluster naming case.
1217

1218
    """
1219
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1220

    
1221
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1222
      self.name = None
1223
    else:
1224
      self.name = items[0]
1225

    
1226
  def GET(self):
1227
    """Returns a list of tags.
1228

1229
    Example: ["tag1", "tag2", "tag3"]
1230

1231
    """
1232
    kind = self.TAG_LEVEL
1233

    
1234
    if kind in (constants.TAG_INSTANCE,
1235
                constants.TAG_NODEGROUP,
1236
                constants.TAG_NODE):
1237
      if not self.name:
1238
        raise http.HttpBadRequest("Missing name on tag request")
1239

    
1240
      cl = self.GetClient()
1241
      if kind == constants.TAG_INSTANCE:
1242
        fn = cl.QueryInstances
1243
      elif kind == constants.TAG_NODEGROUP:
1244
        fn = cl.QueryGroups
1245
      else:
1246
        fn = cl.QueryNodes
1247
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1248
      if not result or not result[0]:
1249
        raise http.HttpBadGateway("Invalid response from tag query")
1250
      tags = result[0][0]
1251

    
1252
    elif kind == constants.TAG_CLUSTER:
1253
      assert not self.name
1254
      # TODO: Use query API?
1255
      ssc = ssconf.SimpleStore()
1256
      tags = ssc.GetClusterTags()
1257

    
1258
    return list(tags)
1259

    
1260
  def GetPutOpInput(self):
1261
    """Add a set of tags.
1262

1263
    The request as a list of strings should be PUT to this URI. And
1264
    you'll have back a job id.
1265

1266
    """
1267
    return ({}, {
1268
      "kind": self.TAG_LEVEL,
1269
      "name": self.name,
1270
      "tags": self.queryargs.get("tag", []),
1271
      "dry_run": self.dryRun(),
1272
      })
1273

    
1274
  def GetDeleteOpInput(self):
1275
    """Delete a tag.
1276

1277
    In order to delete a set of tags, the DELETE
1278
    request should be addressed to URI like:
1279
    /tags?tag=[tag]&tag=[tag]
1280

1281
    """
1282
    # Re-use code
1283
    return self.GetPutOpInput()
1284

    
1285

    
1286
class R_2_instances_name_tags(_R_Tags):
1287
  """ /2/instances/[instance_name]/tags resource.
1288

1289
  Manages per-instance tags.
1290

1291
  """
1292
  TAG_LEVEL = constants.TAG_INSTANCE
1293

    
1294

    
1295
class R_2_nodes_name_tags(_R_Tags):
1296
  """ /2/nodes/[node_name]/tags resource.
1297

1298
  Manages per-node tags.
1299

1300
  """
1301
  TAG_LEVEL = constants.TAG_NODE
1302

    
1303

    
1304
class R_2_groups_name_tags(_R_Tags):
1305
  """ /2/groups/[group_name]/tags resource.
1306

1307
  Manages per-nodegroup tags.
1308

1309
  """
1310
  TAG_LEVEL = constants.TAG_NODEGROUP
1311

    
1312

    
1313
class R_2_tags(_R_Tags):
1314
  """ /2/tags resource.
1315

1316
  Manages cluster tags.
1317

1318
  """
1319
  TAG_LEVEL = constants.TAG_CLUSTER