Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ e5ff1a47

History | View | Annotate | Download (35.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 objects
60
from ganeti import http
61
from ganeti import constants
62
from ganeti import cli
63
from ganeti import rapi
64
from ganeti import ht
65
from ganeti import compat
66
from ganeti import ssconf
67
from ganeti.rapi import baserlib
68

    
69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
159

    
160
# FIXME: For compatibility we update the beparams/memory field. Needs to be
161
#        removed in Ganeti 2.7
162
def _UpdateBeparams(inst):
163
  """Updates the beparams dict of inst to support the memory field.
164

165
  @param inst: Inst dict
166
  @return: Updated inst dict
167

168
  """
169
  beparams = inst["beparams"]
170
  beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
171

    
172
  return inst
173

    
174

    
175
class R_root(baserlib.ResourceBase):
176
  """/ resource.
177

178
  """
179
  @staticmethod
180
  def GET():
181
    """Supported for legacy reasons.
182

183
    """
184
    return None
185

    
186

    
187
class R_2(R_root):
188
  """/2 resource.
189

190
  """
191

    
192

    
193
class R_version(baserlib.ResourceBase):
194
  """/version resource.
195

196
  This resource should be used to determine the remote API version and
197
  to adapt clients accordingly.
198

199
  """
200
  @staticmethod
201
  def GET():
202
    """Returns the remote API version.
203

204
    """
205
    return constants.RAPI_VERSION
206

    
207

    
208
class R_2_info(baserlib.OpcodeResource):
209
  """/2/info resource.
210

211
  """
212
  GET_OPCODE = opcodes.OpClusterQuery
213

    
214
  def GET(self):
215
    """Returns cluster information.
216

217
    """
218
    client = self.GetClient(query=True)
219
    return client.QueryClusterInfo()
220

    
221

    
222
class R_2_features(baserlib.ResourceBase):
223
  """/2/features resource.
224

225
  """
226
  @staticmethod
227
  def GET():
228
    """Returns list of optional RAPI features implemented.
229

230
    """
231
    return list(ALL_FEATURES)
232

    
233

    
234
class R_2_os(baserlib.OpcodeResource):
235
  """/2/os resource.
236

237
  """
238
  GET_OPCODE = opcodes.OpOsDiagnose
239

    
240
  def GET(self):
241
    """Return a list of all OSes.
242

243
    Can return error 500 in case of a problem.
244

245
    Example: ["debian-etch"]
246

247
    """
248
    cl = self.GetClient()
249
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
250
    job_id = self.SubmitJob([op], cl=cl)
251
    # we use custom feedback function, instead of print we log the status
252
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
253
    diagnose_data = result[0]
254

    
255
    if not isinstance(diagnose_data, list):
256
      raise http.HttpBadGateway(message="Can't get OS list")
257

    
258
    os_names = []
259
    for (name, variants) in diagnose_data:
260
      os_names.extend(cli.CalculateOSNames(name, variants))
261

    
262
    return os_names
263

    
264

    
265
class R_2_redist_config(baserlib.OpcodeResource):
266
  """/2/redistribute-config resource.
267

268
  """
269
  PUT_OPCODE = opcodes.OpClusterRedistConf
270

    
271

    
272
class R_2_cluster_modify(baserlib.OpcodeResource):
273
  """/2/modify resource.
274

275
  """
276
  PUT_OPCODE = opcodes.OpClusterSetParams
277

    
278

    
279
class R_2_jobs(baserlib.ResourceBase):
280
  """/2/jobs resource.
281

282
  """
283
  def GET(self):
284
    """Returns a dictionary of jobs.
285

286
    @return: a dictionary with jobs id and uri.
287

288
    """
289
    client = self.GetClient()
290

    
291
    if self.useBulk():
292
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
293
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
294
    else:
295
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
296
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
297
                                   uri_fields=("id", "uri"))
298

    
299

    
300
class R_2_jobs_id(baserlib.ResourceBase):
301
  """/2/jobs/[job_id] resource.
302

303
  """
304
  def GET(self):
305
    """Returns a job status.
306

307
    @return: a dictionary with job parameters.
308
        The result includes:
309
            - id: job ID as a number
310
            - status: current job status as a string
311
            - ops: involved OpCodes as a list of dictionaries for each
312
              opcodes in the job
313
            - opstatus: OpCodes status as a list
314
            - opresult: OpCodes results as a list of lists
315

316
    """
317
    job_id = self.items[0]
318
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
319
    if result is None:
320
      raise http.HttpNotFound()
321
    return baserlib.MapFields(J_FIELDS, result)
322

    
323
  def DELETE(self):
324
    """Cancel not-yet-started job.
325

326
    """
327
    job_id = self.items[0]
328
    result = self.GetClient().CancelJob(job_id)
329
    return result
330

    
331

    
332
class R_2_jobs_id_wait(baserlib.ResourceBase):
333
  """/2/jobs/[job_id]/wait resource.
334

335
  """
336
  # WaitForJobChange provides access to sensitive information and blocks
337
  # machine resources (it's a blocking RAPI call), hence restricting access.
338
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
339

    
340
  def GET(self):
341
    """Waits for job changes.
342

343
    """
344
    job_id = self.items[0]
345

    
346
    fields = self.getBodyParameter("fields")
347
    prev_job_info = self.getBodyParameter("previous_job_info", None)
348
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
349

    
350
    if not isinstance(fields, list):
351
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
352

    
353
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
354
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
355
                                " be a list")
356

    
357
    if not (prev_log_serial is None or
358
            isinstance(prev_log_serial, (int, long))):
359
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
360
                                " be a number")
361

    
362
    client = self.GetClient()
363
    result = client.WaitForJobChangeOnce(job_id, fields,
364
                                         prev_job_info, prev_log_serial,
365
                                         timeout=_WFJC_TIMEOUT)
366
    if not result:
367
      raise http.HttpNotFound()
368

    
369
    if result == constants.JOB_NOTCHANGED:
370
      # No changes
371
      return None
372

    
373
    (job_info, log_entries) = result
374

    
375
    return {
376
      "job_info": job_info,
377
      "log_entries": log_entries,
378
      }
379

    
380

    
381
class R_2_nodes(baserlib.OpcodeResource):
382
  """/2/nodes resource.
383

384
  """
385
  GET_OPCODE = opcodes.OpNodeQuery
386

    
387
  def GET(self):
388
    """Returns a list of all nodes.
389

390
    """
391
    client = self.GetClient()
392

    
393
    if self.useBulk():
394
      bulkdata = client.QueryNodes([], N_FIELDS, False)
395
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
396
    else:
397
      nodesdata = client.QueryNodes([], ["name"], False)
398
      nodeslist = [row[0] for row in nodesdata]
399
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
400
                                   uri_fields=("id", "uri"))
401

    
402

    
403
class R_2_nodes_name(baserlib.OpcodeResource):
404
  """/2/nodes/[node_name] resource.
405

406
  """
407
  GET_OPCODE = opcodes.OpNodeQuery
408

    
409
  def GET(self):
410
    """Send information about a node.
411

412
    """
413
    node_name = self.items[0]
414
    client = self.GetClient()
415

    
416
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
417
                                            names=[node_name], fields=N_FIELDS,
418
                                            use_locking=self.useLocking())
419

    
420
    return baserlib.MapFields(N_FIELDS, result[0])
421

    
422

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

426
  """
427
  POST_OPCODE = opcodes.OpNodePowercycle
428

    
429
  def GetPostOpInput(self):
430
    """Tries to powercycle a node.
431

432
    """
433
    return (self.request_body, {
434
      "node_name": self.items[0],
435
      "force": self.useForce(),
436
      })
437

    
438

    
439
class R_2_nodes_name_role(baserlib.OpcodeResource):
440
  """/2/nodes/[node_name]/role resource.
441

442
  """
443
  PUT_OPCODE = opcodes.OpNodeSetParams
444

    
445
  def GET(self):
446
    """Returns the current node role.
447

448
    @return: Node role
449

450
    """
451
    node_name = self.items[0]
452
    client = self.GetClient()
453
    result = client.QueryNodes(names=[node_name], fields=["role"],
454
                               use_locking=self.useLocking())
455

    
456
    return _NR_MAP[result[0][0]]
457

    
458
  def GetPutOpInput(self):
459
    """Sets the node role.
460

461
    """
462
    baserlib.CheckType(self.request_body, basestring, "Body contents")
463

    
464
    role = self.request_body
465

    
466
    if role == _NR_REGULAR:
467
      candidate = False
468
      offline = False
469
      drained = False
470

    
471
    elif role == _NR_MASTER_CANDIDATE:
472
      candidate = True
473
      offline = drained = None
474

    
475
    elif role == _NR_DRAINED:
476
      drained = True
477
      candidate = offline = None
478

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

    
483
    else:
484
      raise http.HttpBadRequest("Can't set '%s' role" % role)
485

    
486
    assert len(self.items) == 1
487

    
488
    return ({}, {
489
      "node_name": self.items[0],
490
      "master_candidate": candidate,
491
      "offline": offline,
492
      "drained": drained,
493
      "force": self.useForce(),
494
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
495
      })
496

    
497

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

501
  """
502
  POST_OPCODE = opcodes.OpNodeEvacuate
503

    
504
  def GetPostOpInput(self):
505
    """Evacuate all instances off a node.
506

507
    """
508
    return (self.request_body, {
509
      "node_name": self.items[0],
510
      "dry_run": self.dryRun(),
511
      })
512

    
513

    
514
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
515
  """/2/nodes/[node_name]/migrate resource.
516

517
  """
518
  POST_OPCODE = opcodes.OpNodeMigrate
519

    
520
  def GetPostOpInput(self):
521
    """Migrate all primary instances from a node.
522

523
    """
524
    if self.queryargs:
525
      # Support old-style requests
526
      if "live" in self.queryargs and "mode" in self.queryargs:
527
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
528
                                  " be passed")
529

    
530
      if "live" in self.queryargs:
531
        if self._checkIntVariable("live", default=1):
532
          mode = constants.HT_MIGRATION_LIVE
533
        else:
534
          mode = constants.HT_MIGRATION_NONLIVE
535
      else:
536
        mode = self._checkStringVariable("mode", default=None)
537

    
538
      data = {
539
        "mode": mode,
540
        }
541
    else:
542
      data = self.request_body
543

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

    
548

    
549
class R_2_nodes_name_modify(baserlib.OpcodeResource):
550
  """/2/nodes/[node_name]/modify resource.
551

552
  """
553
  POST_OPCODE = opcodes.OpNodeSetParams
554

    
555
  def GetPostOpInput(self):
556
    """Changes parameters of a node.
557

558
    """
559
    assert len(self.items) == 1
560

    
561
    return (self.request_body, {
562
      "node_name": self.items[0],
563
      })
564

    
565

    
566
class R_2_nodes_name_storage(baserlib.OpcodeResource):
567
  """/2/nodes/[node_name]/storage resource.
568

569
  """
570
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
571
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
572
  GET_OPCODE = opcodes.OpNodeQueryStorage
573

    
574
  def GetGetOpInput(self):
575
    """List storage available on a node.
576

577
    """
578
    storage_type = self._checkStringVariable("storage_type", None)
579
    output_fields = self._checkStringVariable("output_fields", None)
580

    
581
    if not output_fields:
582
      raise http.HttpBadRequest("Missing the required 'output_fields'"
583
                                " parameter")
584

    
585
    return ({}, {
586
      "nodes": [self.items[0]],
587
      "storage_type": storage_type,
588
      "output_fields": output_fields.split(","),
589
      })
590

    
591

    
592
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
593
  """/2/nodes/[node_name]/storage/modify resource.
594

595
  """
596
  PUT_OPCODE = opcodes.OpNodeModifyStorage
597

    
598
  def GetPutOpInput(self):
599
    """Modifies a storage volume on a node.
600

601
    """
602
    storage_type = self._checkStringVariable("storage_type", None)
603
    name = self._checkStringVariable("name", None)
604

    
605
    if not name:
606
      raise http.HttpBadRequest("Missing the required 'name'"
607
                                " parameter")
608

    
609
    changes = {}
610

    
611
    if "allocatable" in self.queryargs:
612
      changes[constants.SF_ALLOCATABLE] = \
613
        bool(self._checkIntVariable("allocatable", default=1))
614

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

    
622

    
623
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
624
  """/2/nodes/[node_name]/storage/repair resource.
625

626
  """
627
  PUT_OPCODE = opcodes.OpRepairNodeStorage
628

    
629
  def GetPutOpInput(self):
630
    """Repairs a storage volume on a node.
631

632
    """
633
    storage_type = self._checkStringVariable("storage_type", None)
634
    name = self._checkStringVariable("name", None)
635
    if not name:
636
      raise http.HttpBadRequest("Missing the required 'name'"
637
                                " parameter")
638

    
639
    return ({}, {
640
      "node_name": self.items[0],
641
      "storage_type": storage_type,
642
      "name": name,
643
      })
644

    
645

    
646
class R_2_groups(baserlib.OpcodeResource):
647
  """/2/groups resource.
648

649
  """
650
  GET_OPCODE = opcodes.OpGroupQuery
651
  POST_OPCODE = opcodes.OpGroupAdd
652
  POST_RENAME = {
653
    "name": "group_name",
654
    }
655

    
656
  def GetPostOpInput(self):
657
    """Create a node group.
658

659
    """
660
    assert not self.items
661
    return (self.request_body, {
662
      "dry_run": self.dryRun(),
663
      })
664

    
665
  def GET(self):
666
    """Returns a list of all node groups.
667

668
    """
669
    client = self.GetClient()
670

    
671
    if self.useBulk():
672
      bulkdata = client.QueryGroups([], G_FIELDS, False)
673
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
674
    else:
675
      data = client.QueryGroups([], ["name"], False)
676
      groupnames = [row[0] for row in data]
677
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
678
                                   uri_fields=("name", "uri"))
679

    
680

    
681
class R_2_groups_name(baserlib.OpcodeResource):
682
  """/2/groups/[group_name] resource.
683

684
  """
685
  DELETE_OPCODE = opcodes.OpGroupRemove
686

    
687
  def GET(self):
688
    """Send information about a node group.
689

690
    """
691
    group_name = self.items[0]
692
    client = self.GetClient()
693

    
694
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
695
                                            names=[group_name], fields=G_FIELDS,
696
                                            use_locking=self.useLocking())
697

    
698
    return baserlib.MapFields(G_FIELDS, result[0])
699

    
700
  def GetDeleteOpInput(self):
701
    """Delete a node group.
702

703
    """
704
    assert len(self.items) == 1
705
    return ({}, {
706
      "group_name": self.items[0],
707
      "dry_run": self.dryRun(),
708
      })
709

    
710

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

714
  """
715
  PUT_OPCODE = opcodes.OpGroupSetParams
716

    
717
  def GetPutOpInput(self):
718
    """Changes some parameters of node group.
719

720
    """
721
    assert self.items
722
    return (self.request_body, {
723
      "group_name": self.items[0],
724
      })
725

    
726

    
727
class R_2_groups_name_rename(baserlib.OpcodeResource):
728
  """/2/groups/[group_name]/rename resource.
729

730
  """
731
  PUT_OPCODE = opcodes.OpGroupRename
732

    
733
  def GetPutOpInput(self):
734
    """Changes the name of a node group.
735

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

    
743

    
744
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
745
  """/2/groups/[group_name]/assign-nodes resource.
746

747
  """
748
  PUT_OPCODE = opcodes.OpGroupAssignNodes
749

    
750
  def GetPutOpInput(self):
751
    """Assigns nodes to a group.
752

753
    """
754
    assert len(self.items) == 1
755
    return (self.request_body, {
756
      "group_name": self.items[0],
757
      "dry_run": self.dryRun(),
758
      "force": self.useForce(),
759
      })
760

    
761

    
762
class R_2_instances(baserlib.OpcodeResource):
763
  """/2/instances resource.
764

765
  """
766
  GET_OPCODE = opcodes.OpInstanceQuery
767
  POST_OPCODE = opcodes.OpInstanceCreate
768
  POST_RENAME = {
769
    "os": "os_type",
770
    "name": "instance_name",
771
    }
772

    
773
  def GET(self):
774
    """Returns a list of all available instances.
775

776
    """
777
    client = self.GetClient()
778

    
779
    use_locking = self.useLocking()
780
    if self.useBulk():
781
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
782
      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
783
    else:
784
      instancesdata = client.QueryInstances([], ["name"], use_locking)
785
      instanceslist = [row[0] for row in instancesdata]
786
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
787
                                   uri_fields=("id", "uri"))
788

    
789
  def GetPostOpInput(self):
790
    """Create an instance.
791

792
    @return: a job id
793

794
    """
795
    baserlib.CheckType(self.request_body, dict, "Body contents")
796

    
797
    # Default to request data version 0
798
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
799

    
800
    if data_version == 0:
801
      raise http.HttpBadRequest("Instance creation request version 0 is no"
802
                                " longer supported")
803
    elif data_version != 1:
804
      raise http.HttpBadRequest("Unsupported request data version %s" %
805
                                data_version)
806

    
807
    data = self.request_body.copy()
808
    # Remove "__version__"
809
    data.pop(_REQ_DATA_VERSION, None)
810

    
811
    return (data, {
812
      "dry_run": self.dryRun(),
813
      })
814

    
815

    
816
class R_2_instances_multi_alloc(baserlib.OpcodeResource):
817
  """/2/instances-multi-alloc resource.
818

819
  """
820
  POST_OPCODE = opcodes.OpInstanceMultiAlloc
821

    
822
  def GetPostOpInput(self):
823
    """Try to allocate multiple instances.
824

825
    @return: A dict with submitted jobs, allocatable instances and failed
826
             allocations
827

828
    """
829
    if "instances" not in self.request_body:
830
      raise http.HttpBadRequest("Request is missing required 'instances' field"
831
                                " in body")
832

    
833
    op_id = {
834
      "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
835
      }
836
    body = objects.FillDict(self.request_body, {
837
      "instances": [objects.FillDict(inst, op_id)
838
                    for inst in self.request_body["instances"]],
839
      })
840

    
841
    return (body, {
842
      "dry_run": self.dryRun(),
843
      })
844

    
845

    
846
class R_2_instances_name(baserlib.OpcodeResource):
847
  """/2/instances/[instance_name] resource.
848

849
  """
850
  GET_OPCODE = opcodes.OpInstanceQuery
851
  DELETE_OPCODE = opcodes.OpInstanceRemove
852

    
853
  def GET(self):
854
    """Send information about an instance.
855

856
    """
857
    client = self.GetClient()
858
    instance_name = self.items[0]
859

    
860
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
861
                                            names=[instance_name],
862
                                            fields=I_FIELDS,
863
                                            use_locking=self.useLocking())
864

    
865
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
866

    
867
  def GetDeleteOpInput(self):
868
    """Delete an instance.
869

870
    """
871
    assert len(self.items) == 1
872
    return ({}, {
873
      "instance_name": self.items[0],
874
      "ignore_failures": False,
875
      "dry_run": self.dryRun(),
876
      })
877

    
878

    
879
class R_2_instances_name_info(baserlib.OpcodeResource):
880
  """/2/instances/[instance_name]/info resource.
881

882
  """
883
  GET_OPCODE = opcodes.OpInstanceQueryData
884

    
885
  def GetGetOpInput(self):
886
    """Request detailed instance information.
887

888
    """
889
    assert len(self.items) == 1
890
    return ({}, {
891
      "instances": [self.items[0]],
892
      "static": bool(self._checkIntVariable("static", default=0)),
893
      })
894

    
895

    
896
class R_2_instances_name_reboot(baserlib.OpcodeResource):
897
  """/2/instances/[instance_name]/reboot resource.
898

899
  Implements an instance reboot.
900

901
  """
902
  POST_OPCODE = opcodes.OpInstanceReboot
903

    
904
  def GetPostOpInput(self):
905
    """Reboot an instance.
906

907
    The URI takes type=[hard|soft|full] and
908
    ignore_secondaries=[False|True] parameters.
909

910
    """
911
    return ({}, {
912
      "instance_name": self.items[0],
913
      "reboot_type":
914
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
915
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
916
      "dry_run": self.dryRun(),
917
      })
918

    
919

    
920
class R_2_instances_name_startup(baserlib.OpcodeResource):
921
  """/2/instances/[instance_name]/startup resource.
922

923
  Implements an instance startup.
924

925
  """
926
  PUT_OPCODE = opcodes.OpInstanceStartup
927

    
928
  def GetPutOpInput(self):
929
    """Startup an instance.
930

931
    The URI takes force=[False|True] parameter to start the instance
932
    if even if secondary disks are failing.
933

934
    """
935
    return ({}, {
936
      "instance_name": self.items[0],
937
      "force": self.useForce(),
938
      "dry_run": self.dryRun(),
939
      "no_remember": bool(self._checkIntVariable("no_remember")),
940
      })
941

    
942

    
943
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
944
  """/2/instances/[instance_name]/shutdown resource.
945

946
  Implements an instance shutdown.
947

948
  """
949
  PUT_OPCODE = opcodes.OpInstanceShutdown
950

    
951
  def GetPutOpInput(self):
952
    """Shutdown an instance.
953

954
    """
955
    return (self.request_body, {
956
      "instance_name": self.items[0],
957
      "no_remember": bool(self._checkIntVariable("no_remember")),
958
      "dry_run": self.dryRun(),
959
      })
960

    
961

    
962
def _ParseInstanceReinstallRequest(name, data):
963
  """Parses a request for reinstalling an instance.
964

965
  """
966
  if not isinstance(data, dict):
967
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
968

    
969
  ostype = baserlib.CheckParameter(data, "os", default=None)
970
  start = baserlib.CheckParameter(data, "start", exptype=bool,
971
                                  default=True)
972
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
973

    
974
  ops = [
975
    opcodes.OpInstanceShutdown(instance_name=name),
976
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
977
                                osparams=osparams),
978
    ]
979

    
980
  if start:
981
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
982

    
983
  return ops
984

    
985

    
986
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
987
  """/2/instances/[instance_name]/reinstall resource.
988

989
  Implements an instance reinstall.
990

991
  """
992
  POST_OPCODE = opcodes.OpInstanceReinstall
993

    
994
  def POST(self):
995
    """Reinstall an instance.
996

997
    The URI takes os=name and nostartup=[0|1] optional
998
    parameters. By default, the instance will be started
999
    automatically.
1000

1001
    """
1002
    if self.request_body:
1003
      if self.queryargs:
1004
        raise http.HttpBadRequest("Can't combine query and body parameters")
1005

    
1006
      body = self.request_body
1007
    elif self.queryargs:
1008
      # Legacy interface, do not modify/extend
1009
      body = {
1010
        "os": self._checkStringVariable("os"),
1011
        "start": not self._checkIntVariable("nostartup"),
1012
        }
1013
    else:
1014
      body = {}
1015

    
1016
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1017

    
1018
    return self.SubmitJob(ops)
1019

    
1020

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

1024
  """
1025
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
1026

    
1027
  def GetPostOpInput(self):
1028
    """Replaces disks on an instance.
1029

1030
    """
1031
    static = {
1032
      "instance_name": self.items[0],
1033
      }
1034

    
1035
    if self.request_body:
1036
      data = self.request_body
1037
    elif self.queryargs:
1038
      # Legacy interface, do not modify/extend
1039
      data = {
1040
        "remote_node": self._checkStringVariable("remote_node", default=None),
1041
        "mode": self._checkStringVariable("mode", default=None),
1042
        "disks": self._checkStringVariable("disks", default=None),
1043
        "iallocator": self._checkStringVariable("iallocator", default=None),
1044
        }
1045
    else:
1046
      data = {}
1047

    
1048
    # Parse disks
1049
    try:
1050
      raw_disks = data.pop("disks")
1051
    except KeyError:
1052
      pass
1053
    else:
1054
      if raw_disks:
1055
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1056
          data["disks"] = raw_disks
1057
        else:
1058
          # Backwards compatibility for strings of the format "1, 2, 3"
1059
          try:
1060
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1061
          except (TypeError, ValueError), err:
1062
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1063

    
1064
    return (data, static)
1065

    
1066

    
1067
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1068
  """/2/instances/[instance_name]/activate-disks resource.
1069

1070
  """
1071
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1072

    
1073
  def GetPutOpInput(self):
1074
    """Activate disks for an instance.
1075

1076
    The URI might contain ignore_size to ignore current recorded size.
1077

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

    
1084

    
1085
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1086
  """/2/instances/[instance_name]/deactivate-disks resource.
1087

1088
  """
1089
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1090

    
1091
  def GetPutOpInput(self):
1092
    """Deactivate disks for an instance.
1093

1094
    """
1095
    return ({}, {
1096
      "instance_name": self.items[0],
1097
      })
1098

    
1099

    
1100
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1101
  """/2/instances/[instance_name]/recreate-disks resource.
1102

1103
  """
1104
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1105

    
1106
  def GetPostOpInput(self):
1107
    """Recreate disks for an instance.
1108

1109
    """
1110
    return ({}, {
1111
      "instance_name": self.items[0],
1112
      })
1113

    
1114

    
1115
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1116
  """/2/instances/[instance_name]/prepare-export resource.
1117

1118
  """
1119
  PUT_OPCODE = opcodes.OpBackupPrepare
1120

    
1121
  def GetPutOpInput(self):
1122
    """Prepares an export for an instance.
1123

1124
    """
1125
    return ({}, {
1126
      "instance_name": self.items[0],
1127
      "mode": self._checkStringVariable("mode"),
1128
      })
1129

    
1130

    
1131
class R_2_instances_name_export(baserlib.OpcodeResource):
1132
  """/2/instances/[instance_name]/export resource.
1133

1134
  """
1135
  PUT_OPCODE = opcodes.OpBackupExport
1136
  PUT_RENAME = {
1137
    "destination": "target_node",
1138
    }
1139

    
1140
  def GetPutOpInput(self):
1141
    """Exports an instance.
1142

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

    
1148

    
1149
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1150
  """/2/instances/[instance_name]/migrate resource.
1151

1152
  """
1153
  PUT_OPCODE = opcodes.OpInstanceMigrate
1154

    
1155
  def GetPutOpInput(self):
1156
    """Migrates an instance.
1157

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

    
1163

    
1164
class R_2_instances_name_failover(baserlib.OpcodeResource):
1165
  """/2/instances/[instance_name]/failover resource.
1166

1167
  """
1168
  PUT_OPCODE = opcodes.OpInstanceFailover
1169

    
1170
  def GetPutOpInput(self):
1171
    """Does a failover of an instance.
1172

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

    
1178

    
1179
class R_2_instances_name_rename(baserlib.OpcodeResource):
1180
  """/2/instances/[instance_name]/rename resource.
1181

1182
  """
1183
  PUT_OPCODE = opcodes.OpInstanceRename
1184

    
1185
  def GetPutOpInput(self):
1186
    """Changes the name of an instance.
1187

1188
    """
1189
    return (self.request_body, {
1190
      "instance_name": self.items[0],
1191
      })
1192

    
1193

    
1194
class R_2_instances_name_modify(baserlib.OpcodeResource):
1195
  """/2/instances/[instance_name]/modify resource.
1196

1197
  """
1198
  PUT_OPCODE = opcodes.OpInstanceSetParams
1199

    
1200
  def GetPutOpInput(self):
1201
    """Changes parameters of an instance.
1202

1203
    """
1204
    return (self.request_body, {
1205
      "instance_name": self.items[0],
1206
      })
1207

    
1208

    
1209
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1210
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1211

1212
  """
1213
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1214

    
1215
  def GetPostOpInput(self):
1216
    """Increases the size of an instance disk.
1217

1218
    """
1219
    return (self.request_body, {
1220
      "instance_name": self.items[0],
1221
      "disk": int(self.items[1]),
1222
      })
1223

    
1224

    
1225
class R_2_instances_name_console(baserlib.ResourceBase):
1226
  """/2/instances/[instance_name]/console resource.
1227

1228
  """
1229
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1230
  GET_OPCODE = opcodes.OpInstanceConsole
1231

    
1232
  def GET(self):
1233
    """Request information for connecting to instance's console.
1234

1235
    @return: Serialized instance console description, see
1236
             L{objects.InstanceConsole}
1237

1238
    """
1239
    client = self.GetClient()
1240

    
1241
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1242

    
1243
    if console is None:
1244
      raise http.HttpServiceUnavailable("Instance console unavailable")
1245

    
1246
    assert isinstance(console, dict)
1247
    return console
1248

    
1249

    
1250
def _GetQueryFields(args):
1251
  """
1252

1253
  """
1254
  try:
1255
    fields = args["fields"]
1256
  except KeyError:
1257
    raise http.HttpBadRequest("Missing 'fields' query argument")
1258

    
1259
  return _SplitQueryFields(fields[0])
1260

    
1261

    
1262
def _SplitQueryFields(fields):
1263
  """
1264

1265
  """
1266
  return [i.strip() for i in fields.split(",")]
1267

    
1268

    
1269
class R_2_query(baserlib.ResourceBase):
1270
  """/2/query/[resource] resource.
1271

1272
  """
1273
  # Results might contain sensitive information
1274
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1275
  GET_OPCODE = opcodes.OpQuery
1276
  PUT_OPCODE = opcodes.OpQuery
1277

    
1278
  def _Query(self, fields, qfilter):
1279
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1280

    
1281
  def GET(self):
1282
    """Returns resource information.
1283

1284
    @return: Query result, see L{objects.QueryResponse}
1285

1286
    """
1287
    return self._Query(_GetQueryFields(self.queryargs), None)
1288

    
1289
  def PUT(self):
1290
    """Submits job querying for resources.
1291

1292
    @return: Query result, see L{objects.QueryResponse}
1293

1294
    """
1295
    body = self.request_body
1296

    
1297
    baserlib.CheckType(body, dict, "Body contents")
1298

    
1299
    try:
1300
      fields = body["fields"]
1301
    except KeyError:
1302
      fields = _GetQueryFields(self.queryargs)
1303

    
1304
    qfilter = body.get("qfilter", None)
1305
    # TODO: remove this after 2.7
1306
    if qfilter is None:
1307
      qfilter = body.get("filter", None)
1308

    
1309
    return self._Query(fields, qfilter)
1310

    
1311

    
1312
class R_2_query_fields(baserlib.ResourceBase):
1313
  """/2/query/[resource]/fields resource.
1314

1315
  """
1316
  GET_OPCODE = opcodes.OpQueryFields
1317

    
1318
  def GET(self):
1319
    """Retrieves list of available fields for a resource.
1320

1321
    @return: List of serialized L{objects.QueryFieldDefinition}
1322

1323
    """
1324
    try:
1325
      raw_fields = self.queryargs["fields"]
1326
    except KeyError:
1327
      fields = None
1328
    else:
1329
      fields = _SplitQueryFields(raw_fields[0])
1330

    
1331
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1332

    
1333

    
1334
class _R_Tags(baserlib.OpcodeResource):
1335
  """Quasiclass for tagging resources.
1336

1337
  Manages tags. When inheriting this class you must define the
1338
  TAG_LEVEL for it.
1339

1340
  """
1341
  TAG_LEVEL = None
1342
  GET_OPCODE = opcodes.OpTagsGet
1343
  PUT_OPCODE = opcodes.OpTagsSet
1344
  DELETE_OPCODE = opcodes.OpTagsDel
1345

    
1346
  def __init__(self, items, queryargs, req, **kwargs):
1347
    """A tag resource constructor.
1348

1349
    We have to override the default to sort out cluster naming case.
1350

1351
    """
1352
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1353

    
1354
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1355
      self.name = None
1356
    else:
1357
      self.name = items[0]
1358

    
1359
  def GET(self):
1360
    """Returns a list of tags.
1361

1362
    Example: ["tag1", "tag2", "tag3"]
1363

1364
    """
1365
    kind = self.TAG_LEVEL
1366

    
1367
    if kind in (constants.TAG_INSTANCE,
1368
                constants.TAG_NODEGROUP,
1369
                constants.TAG_NODE):
1370
      if not self.name:
1371
        raise http.HttpBadRequest("Missing name on tag request")
1372

    
1373
      cl = self.GetClient(query=True)
1374
      tags = list(cl.QueryTags(kind, self.name))
1375

    
1376
    elif kind == constants.TAG_CLUSTER:
1377
      assert not self.name
1378
      # TODO: Use query API?
1379
      ssc = ssconf.SimpleStore()
1380
      tags = ssc.GetClusterTags()
1381

    
1382
    return list(tags)
1383

    
1384
  def GetPutOpInput(self):
1385
    """Add a set of tags.
1386

1387
    The request as a list of strings should be PUT to this URI. And
1388
    you'll have back a job id.
1389

1390
    """
1391
    return ({}, {
1392
      "kind": self.TAG_LEVEL,
1393
      "name": self.name,
1394
      "tags": self.queryargs.get("tag", []),
1395
      "dry_run": self.dryRun(),
1396
      })
1397

    
1398
  def GetDeleteOpInput(self):
1399
    """Delete a tag.
1400

1401
    In order to delete a set of tags, the DELETE
1402
    request should be addressed to URI like:
1403
    /tags?tag=[tag]&tag=[tag]
1404

1405
    """
1406
    # Re-use code
1407
    return self.GetPutOpInput()
1408

    
1409

    
1410
class R_2_instances_name_tags(_R_Tags):
1411
  """ /2/instances/[instance_name]/tags resource.
1412

1413
  Manages per-instance tags.
1414

1415
  """
1416
  TAG_LEVEL = constants.TAG_INSTANCE
1417

    
1418

    
1419
class R_2_nodes_name_tags(_R_Tags):
1420
  """ /2/nodes/[node_name]/tags resource.
1421

1422
  Manages per-node tags.
1423

1424
  """
1425
  TAG_LEVEL = constants.TAG_NODE
1426

    
1427

    
1428
class R_2_groups_name_tags(_R_Tags):
1429
  """ /2/groups/[group_name]/tags resource.
1430

1431
  Manages per-nodegroup tags.
1432

1433
  """
1434
  TAG_LEVEL = constants.TAG_NODEGROUP
1435

    
1436

    
1437
class R_2_tags(_R_Tags):
1438
  """ /2/tags resource.
1439

1440
  Manages cluster tags.
1441

1442
  """
1443
  TAG_LEVEL = constants.TAG_CLUSTER