Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ f2374060

History | View | Annotate | Download (34.4 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
# FIXME: For compatibility we update the beparams/memory field. Needs to be
160
#        removed in Ganeti 2.7
161
def _UpdateBeparams(inst):
162
  """Updates the beparams dict of inst to support the memory field.
163

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

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

    
171
  return inst
172

    
173

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

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

182
    """
183
    return None
184

    
185

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

189
  """
190

    
191

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

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

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

203
    """
204
    return constants.RAPI_VERSION
205

    
206

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

210
  """
211
  GET_OPCODE = opcodes.OpClusterQuery
212

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

216
    """
217
    client = self.GetClient()
218
    return client.QueryClusterInfo()
219

    
220

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

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

229
    """
230
    return list(ALL_FEATURES)
231

    
232

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

236
  """
237
  GET_OPCODE = opcodes.OpOsDiagnose
238

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

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

244
    Example: ["debian-etch"]
245

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

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

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

    
261
    return os_names
262

    
263

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

267
  """
268
  PUT_OPCODE = opcodes.OpClusterRedistConf
269

    
270

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

274
  """
275
  PUT_OPCODE = opcodes.OpClusterSetParams
276

    
277

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

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

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

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

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

    
298

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

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

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

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

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

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

    
330

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

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

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

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

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

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

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

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

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

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

    
372
    (job_info, log_entries) = result
373

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

    
379

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

383
  """
384
  GET_OPCODE = opcodes.OpNodeQuery
385

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

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

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

    
401

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

405
  """
406
  GET_OPCODE = opcodes.OpNodeQuery
407

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

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

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

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

    
421

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

425
  """
426
  POST_OPCODE = opcodes.OpNodePowercycle
427

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

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

    
437

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

441
  """
442
  PUT_OPCODE = opcodes.OpNodeSetParams
443

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

447
    @return: Node role
448

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

    
455
    return _NR_MAP[result[0][0]]
456

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

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

    
463
    role = self.request_body
464

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

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

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

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

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

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

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

    
496

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

500
  """
501
  POST_OPCODE = opcodes.OpNodeEvacuate
502

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

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

    
512

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

516
  """
517
  POST_OPCODE = opcodes.OpNodeMigrate
518

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

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

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

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

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

    
547

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

551
  """
552
  POST_OPCODE = opcodes.OpNodeSetParams
553

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

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

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

    
564

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

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

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

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

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

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

    
590

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

594
  """
595
  PUT_OPCODE = opcodes.OpNodeModifyStorage
596

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

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

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

    
608
    changes = {}
609

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

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

    
621

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

625
  """
626
  PUT_OPCODE = opcodes.OpRepairNodeStorage
627

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

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

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

    
644

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

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

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

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

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

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

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

    
679

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

683
  """
684
  DELETE_OPCODE = opcodes.OpGroupRemove
685

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

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

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

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

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

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

    
709

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

713
  """
714
  PUT_OPCODE = opcodes.OpGroupSetParams
715

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

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

    
725

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

729
  """
730
  PUT_OPCODE = opcodes.OpGroupRename
731

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

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

    
742

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

746
  """
747
  PUT_OPCODE = opcodes.OpGroupAssignNodes
748

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

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

    
760

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

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

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

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

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

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

791
    @return: a job id
792

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

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

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

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

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

    
814

    
815
class R_2_instances_name(baserlib.OpcodeResource):
816
  """/2/instances/[instance_name] resource.
817

818
  """
819
  GET_OPCODE = opcodes.OpInstanceQuery
820
  DELETE_OPCODE = opcodes.OpInstanceRemove
821

    
822
  def GET(self):
823
    """Send information about an instance.
824

825
    """
826
    client = self.GetClient()
827
    instance_name = self.items[0]
828

    
829
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
830
                                            names=[instance_name],
831
                                            fields=I_FIELDS,
832
                                            use_locking=self.useLocking())
833

    
834
    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
835

    
836
  def GetDeleteOpInput(self):
837
    """Delete an instance.
838

839
    """
840
    assert len(self.items) == 1
841
    return ({}, {
842
      "instance_name": self.items[0],
843
      "ignore_failures": False,
844
      "dry_run": self.dryRun(),
845
      })
846

    
847

    
848
class R_2_instances_name_info(baserlib.OpcodeResource):
849
  """/2/instances/[instance_name]/info resource.
850

851
  """
852
  GET_OPCODE = opcodes.OpInstanceQueryData
853

    
854
  def GetGetOpInput(self):
855
    """Request detailed instance information.
856

857
    """
858
    assert len(self.items) == 1
859
    return ({}, {
860
      "instances": [self.items[0]],
861
      "static": bool(self._checkIntVariable("static", default=0)),
862
      })
863

    
864

    
865
class R_2_instances_name_reboot(baserlib.OpcodeResource):
866
  """/2/instances/[instance_name]/reboot resource.
867

868
  Implements an instance reboot.
869

870
  """
871
  POST_OPCODE = opcodes.OpInstanceReboot
872

    
873
  def GetPostOpInput(self):
874
    """Reboot an instance.
875

876
    The URI takes type=[hard|soft|full] and
877
    ignore_secondaries=[False|True] parameters.
878

879
    """
880
    return ({}, {
881
      "instance_name": self.items[0],
882
      "reboot_type":
883
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
884
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
885
      "dry_run": self.dryRun(),
886
      })
887

    
888

    
889
class R_2_instances_name_startup(baserlib.OpcodeResource):
890
  """/2/instances/[instance_name]/startup resource.
891

892
  Implements an instance startup.
893

894
  """
895
  PUT_OPCODE = opcodes.OpInstanceStartup
896

    
897
  def GetPutOpInput(self):
898
    """Startup an instance.
899

900
    The URI takes force=[False|True] parameter to start the instance
901
    if even if secondary disks are failing.
902

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

    
911

    
912
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
913
  """/2/instances/[instance_name]/shutdown resource.
914

915
  Implements an instance shutdown.
916

917
  """
918
  PUT_OPCODE = opcodes.OpInstanceShutdown
919

    
920
  def GetPutOpInput(self):
921
    """Shutdown an instance.
922

923
    """
924
    return (self.request_body, {
925
      "instance_name": self.items[0],
926
      "no_remember": bool(self._checkIntVariable("no_remember")),
927
      "dry_run": self.dryRun(),
928
      })
929

    
930

    
931
def _ParseInstanceReinstallRequest(name, data):
932
  """Parses a request for reinstalling an instance.
933

934
  """
935
  if not isinstance(data, dict):
936
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
937

    
938
  ostype = baserlib.CheckParameter(data, "os", default=None)
939
  start = baserlib.CheckParameter(data, "start", exptype=bool,
940
                                  default=True)
941
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
942

    
943
  ops = [
944
    opcodes.OpInstanceShutdown(instance_name=name),
945
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
946
                                osparams=osparams),
947
    ]
948

    
949
  if start:
950
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
951

    
952
  return ops
953

    
954

    
955
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
956
  """/2/instances/[instance_name]/reinstall resource.
957

958
  Implements an instance reinstall.
959

960
  """
961
  POST_OPCODE = opcodes.OpInstanceReinstall
962

    
963
  def POST(self):
964
    """Reinstall an instance.
965

966
    The URI takes os=name and nostartup=[0|1] optional
967
    parameters. By default, the instance will be started
968
    automatically.
969

970
    """
971
    if self.request_body:
972
      if self.queryargs:
973
        raise http.HttpBadRequest("Can't combine query and body parameters")
974

    
975
      body = self.request_body
976
    elif self.queryargs:
977
      # Legacy interface, do not modify/extend
978
      body = {
979
        "os": self._checkStringVariable("os"),
980
        "start": not self._checkIntVariable("nostartup"),
981
        }
982
    else:
983
      body = {}
984

    
985
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
986

    
987
    return self.SubmitJob(ops)
988

    
989

    
990
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
991
  """/2/instances/[instance_name]/replace-disks resource.
992

993
  """
994
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
995

    
996
  def GetPostOpInput(self):
997
    """Replaces disks on an instance.
998

999
    """
1000
    static = {
1001
      "instance_name": self.items[0],
1002
      }
1003

    
1004
    if self.request_body:
1005
      data = self.request_body
1006
    elif self.queryargs:
1007
      # Legacy interface, do not modify/extend
1008
      data = {
1009
        "remote_node": self._checkStringVariable("remote_node", default=None),
1010
        "mode": self._checkStringVariable("mode", default=None),
1011
        "disks": self._checkStringVariable("disks", default=None),
1012
        "iallocator": self._checkStringVariable("iallocator", default=None),
1013
        }
1014
    else:
1015
      data = {}
1016

    
1017
    # Parse disks
1018
    try:
1019
      raw_disks = data.pop("disks")
1020
    except KeyError:
1021
      pass
1022
    else:
1023
      if raw_disks:
1024
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1025
          data["disks"] = raw_disks
1026
        else:
1027
          # Backwards compatibility for strings of the format "1, 2, 3"
1028
          try:
1029
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1030
          except (TypeError, ValueError), err:
1031
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1032

    
1033
    return (data, static)
1034

    
1035

    
1036
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1037
  """/2/instances/[instance_name]/activate-disks resource.
1038

1039
  """
1040
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1041

    
1042
  def GetPutOpInput(self):
1043
    """Activate disks for an instance.
1044

1045
    The URI might contain ignore_size to ignore current recorded size.
1046

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

    
1053

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

1057
  """
1058
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1059

    
1060
  def GetPutOpInput(self):
1061
    """Deactivate disks for an instance.
1062

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

    
1068

    
1069
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1070
  """/2/instances/[instance_name]/recreate-disks resource.
1071

1072
  """
1073
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1074

    
1075
  def GetPostOpInput(self):
1076
    """Recreate disks for an instance.
1077

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

    
1083

    
1084
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1085
  """/2/instances/[instance_name]/prepare-export resource.
1086

1087
  """
1088
  PUT_OPCODE = opcodes.OpBackupPrepare
1089

    
1090
  def GetPutOpInput(self):
1091
    """Prepares an export for an instance.
1092

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

    
1099

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

1103
  """
1104
  PUT_OPCODE = opcodes.OpBackupExport
1105
  PUT_RENAME = {
1106
    "destination": "target_node",
1107
    }
1108

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

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

    
1117

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

1121
  """
1122
  PUT_OPCODE = opcodes.OpInstanceMigrate
1123

    
1124
  def GetPutOpInput(self):
1125
    """Migrates an instance.
1126

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

    
1132

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

1136
  """
1137
  PUT_OPCODE = opcodes.OpInstanceFailover
1138

    
1139
  def GetPutOpInput(self):
1140
    """Does a failover 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_rename(baserlib.OpcodeResource):
1149
  """/2/instances/[instance_name]/rename resource.
1150

1151
  """
1152
  PUT_OPCODE = opcodes.OpInstanceRename
1153

    
1154
  def GetPutOpInput(self):
1155
    """Changes the name 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_modify(baserlib.OpcodeResource):
1164
  """/2/instances/[instance_name]/modify resource.
1165

1166
  """
1167
  PUT_OPCODE = opcodes.OpInstanceSetParams
1168

    
1169
  def GetPutOpInput(self):
1170
    """Changes parameters of an instance.
1171

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

    
1177

    
1178
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1179
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1180

1181
  """
1182
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1183

    
1184
  def GetPostOpInput(self):
1185
    """Increases the size of an instance disk.
1186

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

    
1193

    
1194
class R_2_instances_name_console(baserlib.ResourceBase):
1195
  """/2/instances/[instance_name]/console resource.
1196

1197
  """
1198
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1199
  GET_OPCODE = opcodes.OpInstanceConsole
1200

    
1201
  def GET(self):
1202
    """Request information for connecting to instance's console.
1203

1204
    @return: Serialized instance console description, see
1205
             L{objects.InstanceConsole}
1206

1207
    """
1208
    client = self.GetClient()
1209

    
1210
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1211

    
1212
    if console is None:
1213
      raise http.HttpServiceUnavailable("Instance console unavailable")
1214

    
1215
    assert isinstance(console, dict)
1216
    return console
1217

    
1218

    
1219
def _GetQueryFields(args):
1220
  """
1221

1222
  """
1223
  try:
1224
    fields = args["fields"]
1225
  except KeyError:
1226
    raise http.HttpBadRequest("Missing 'fields' query argument")
1227

    
1228
  return _SplitQueryFields(fields[0])
1229

    
1230

    
1231
def _SplitQueryFields(fields):
1232
  """
1233

1234
  """
1235
  return [i.strip() for i in fields.split(",")]
1236

    
1237

    
1238
class R_2_query(baserlib.ResourceBase):
1239
  """/2/query/[resource] resource.
1240

1241
  """
1242
  # Results might contain sensitive information
1243
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1244
  GET_OPCODE = opcodes.OpQuery
1245
  PUT_OPCODE = opcodes.OpQuery
1246

    
1247
  def _Query(self, fields, qfilter):
1248
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1249

    
1250
  def GET(self):
1251
    """Returns resource information.
1252

1253
    @return: Query result, see L{objects.QueryResponse}
1254

1255
    """
1256
    return self._Query(_GetQueryFields(self.queryargs), None)
1257

    
1258
  def PUT(self):
1259
    """Submits job querying for resources.
1260

1261
    @return: Query result, see L{objects.QueryResponse}
1262

1263
    """
1264
    body = self.request_body
1265

    
1266
    baserlib.CheckType(body, dict, "Body contents")
1267

    
1268
    try:
1269
      fields = body["fields"]
1270
    except KeyError:
1271
      fields = _GetQueryFields(self.queryargs)
1272

    
1273
    qfilter = body.get("qfilter", None)
1274
    # TODO: remove this after 2.7
1275
    if qfilter is None:
1276
      qfilter = body.get("filter", None)
1277

    
1278
    return self._Query(fields, qfilter)
1279

    
1280

    
1281
class R_2_query_fields(baserlib.ResourceBase):
1282
  """/2/query/[resource]/fields resource.
1283

1284
  """
1285
  GET_OPCODE = opcodes.OpQueryFields
1286

    
1287
  def GET(self):
1288
    """Retrieves list of available fields for a resource.
1289

1290
    @return: List of serialized L{objects.QueryFieldDefinition}
1291

1292
    """
1293
    try:
1294
      raw_fields = self.queryargs["fields"]
1295
    except KeyError:
1296
      fields = None
1297
    else:
1298
      fields = _SplitQueryFields(raw_fields[0])
1299

    
1300
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1301

    
1302

    
1303
class _R_Tags(baserlib.OpcodeResource):
1304
  """Quasiclass for tagging resources.
1305

1306
  Manages tags. When inheriting this class you must define the
1307
  TAG_LEVEL for it.
1308

1309
  """
1310
  TAG_LEVEL = None
1311
  GET_OPCODE = opcodes.OpTagsGet
1312
  PUT_OPCODE = opcodes.OpTagsSet
1313
  DELETE_OPCODE = opcodes.OpTagsDel
1314

    
1315
  def __init__(self, items, queryargs, req, **kwargs):
1316
    """A tag resource constructor.
1317

1318
    We have to override the default to sort out cluster naming case.
1319

1320
    """
1321
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1322

    
1323
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1324
      self.name = None
1325
    else:
1326
      self.name = items[0]
1327

    
1328
  def GET(self):
1329
    """Returns a list of tags.
1330

1331
    Example: ["tag1", "tag2", "tag3"]
1332

1333
    """
1334
    kind = self.TAG_LEVEL
1335

    
1336
    if kind in (constants.TAG_INSTANCE,
1337
                constants.TAG_NODEGROUP,
1338
                constants.TAG_NODE):
1339
      if not self.name:
1340
        raise http.HttpBadRequest("Missing name on tag request")
1341

    
1342
      cl = self.GetClient(query=True)
1343
      tags = list(cl.QueryTags(kind, self.name))
1344

    
1345
    elif kind == constants.TAG_CLUSTER:
1346
      assert not self.name
1347
      # TODO: Use query API?
1348
      ssc = ssconf.SimpleStore()
1349
      tags = ssc.GetClusterTags()
1350

    
1351
    return list(tags)
1352

    
1353
  def GetPutOpInput(self):
1354
    """Add a set of tags.
1355

1356
    The request as a list of strings should be PUT to this URI. And
1357
    you'll have back a job id.
1358

1359
    """
1360
    return ({}, {
1361
      "kind": self.TAG_LEVEL,
1362
      "name": self.name,
1363
      "tags": self.queryargs.get("tag", []),
1364
      "dry_run": self.dryRun(),
1365
      })
1366

    
1367
  def GetDeleteOpInput(self):
1368
    """Delete a tag.
1369

1370
    In order to delete a set of tags, the DELETE
1371
    request should be addressed to URI like:
1372
    /tags?tag=[tag]&tag=[tag]
1373

1374
    """
1375
    # Re-use code
1376
    return self.GetPutOpInput()
1377

    
1378

    
1379
class R_2_instances_name_tags(_R_Tags):
1380
  """ /2/instances/[instance_name]/tags resource.
1381

1382
  Manages per-instance tags.
1383

1384
  """
1385
  TAG_LEVEL = constants.TAG_INSTANCE
1386

    
1387

    
1388
class R_2_nodes_name_tags(_R_Tags):
1389
  """ /2/nodes/[node_name]/tags resource.
1390

1391
  Manages per-node tags.
1392

1393
  """
1394
  TAG_LEVEL = constants.TAG_NODE
1395

    
1396

    
1397
class R_2_groups_name_tags(_R_Tags):
1398
  """ /2/groups/[group_name]/tags resource.
1399

1400
  Manages per-nodegroup tags.
1401

1402
  """
1403
  TAG_LEVEL = constants.TAG_NODEGROUP
1404

    
1405

    
1406
class R_2_tags(_R_Tags):
1407
  """ /2/tags resource.
1408

1409
  Manages cluster tags.
1410

1411
  """
1412
  TAG_LEVEL = constants.TAG_CLUSTER