Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 42d4d8b9

History | View | Annotate | Download (33.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Remote API resource implementations.
23

24
PUT or POST?
25
============
26

27
According to RFC2616 the main difference between PUT and POST is that
28
POST can create new resources but PUT can only create the resource the
29
URI was pointing to on the PUT request.
30

31
In the context of this module POST on ``/2/instances`` to change an existing
32
entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
33
new instance) with a name specified in the request.
34

35
Quoting from RFC2616, section 9.6::
36

37
  The fundamental difference between the POST and PUT requests is reflected in
38
  the different meaning of the Request-URI. The URI in a POST request
39
  identifies the resource that will handle the enclosed entity. That resource
40
  might be a data-accepting process, a gateway to some other protocol, or a
41
  separate entity that accepts annotations. In contrast, the URI in a PUT
42
  request identifies the entity enclosed with the request -- the user agent
43
  knows what URI is intended and the server MUST NOT attempt to apply the
44
  request to some other resource. If the server desires that the request be
45
  applied to a different URI, it MUST send a 301 (Moved Permanently) response;
46
  the user agent MAY then make its own decision regarding whether or not to
47
  redirect the request.
48

49
So when adding new methods, if they are operating on the URI entity itself,
50
PUT should be prefered over POST.
51

52
"""
53

    
54
# pylint: disable=C0103
55

    
56
# C0103: Invalid name, since the R_* names are not conforming
57

    
58
from ganeti import opcodes
59
from ganeti import http
60
from ganeti import constants
61
from ganeti import cli
62
from ganeti import rapi
63
from ganeti import ht
64
from ganeti import compat
65
from ganeti import ssconf
66
from ganeti.rapi import baserlib
67

    
68

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

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

    
92
G_FIELDS = [
93
  "alloc_policy",
94
  "name",
95
  "node_cnt",
96
  "node_list",
97
  ] + _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_2(R_root):
165
  """/2 resource.
166

167
  """
168

    
169

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

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

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

181
    """
182
    return constants.RAPI_VERSION
183

    
184

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

188
  """
189
  GET_OPCODE = opcodes.OpClusterQuery
190

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

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

    
198

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

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

207
    """
208
    return list(ALL_FEATURES)
209

    
210

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

214
  """
215
  GET_OPCODE = opcodes.OpOsDiagnose
216

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

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

222
    Example: ["debian-etch"]
223

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

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

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

    
239
    return os_names
240

    
241

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

245
  """
246
  PUT_OPCODE = opcodes.OpClusterRedistConf
247

    
248

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

252
  """
253
  PUT_OPCODE = opcodes.OpClusterSetParams
254

    
255

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

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

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

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

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

    
276

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

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

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

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

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

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

    
308

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

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

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

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

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

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

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

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

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

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

    
350
    (job_info, log_entries) = result
351

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

    
357

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

361
  """
362
  GET_OPCODE = opcodes.OpNodeQuery
363

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

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

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

    
379

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

383
  """
384
  GET_OPCODE = opcodes.OpNodeQuery
385

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

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

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

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

    
399

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

403
  """
404
  POST_OPCODE = opcodes.OpNodePowercycle
405

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

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

    
415

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

419
  """
420
  PUT_OPCODE = opcodes.OpNodeSetParams
421

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

425
    @return: Node role
426

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

    
433
    return _NR_MAP[result[0][0]]
434

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

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

    
441
    role = self.request_body
442

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

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

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

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

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

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

    
465
    return ({}, {
466
      "node_name": self.items[0],
467
      "master_candidate": candidate,
468
      "offline": offline,
469
      "drained": drained,
470
      "force": self.useForce(),
471
      })
472

    
473

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

477
  """
478
  POST_OPCODE = opcodes.OpNodeEvacuate
479

    
480
  def GetPostOpInput(self):
481
    """Evacuate all instances off a node.
482

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

    
489

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

493
  """
494
  POST_OPCODE = opcodes.OpNodeMigrate
495

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

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

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

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

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

    
524

    
525
class R_2_nodes_name_storage(baserlib.OpcodeResource):
526
  """/2/nodes/[node_name]/storage resource.
527

528
  """
529
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
530
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
531
  GET_OPCODE = opcodes.OpNodeQueryStorage
532

    
533
  def GetGetOpInput(self):
534
    """List storage available on a node.
535

536
    """
537
    storage_type = self._checkStringVariable("storage_type", None)
538
    output_fields = self._checkStringVariable("output_fields", None)
539

    
540
    if not output_fields:
541
      raise http.HttpBadRequest("Missing the required 'output_fields'"
542
                                " parameter")
543

    
544
    return ({}, {
545
      "nodes": [self.items[0]],
546
      "storage_type": storage_type,
547
      "output_fields": output_fields.split(","),
548
      })
549

    
550

    
551
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
552
  """/2/nodes/[node_name]/storage/modify resource.
553

554
  """
555
  PUT_OPCODE = opcodes.OpNodeModifyStorage
556

    
557
  def GetPutOpInput(self):
558
    """Modifies a storage volume on a node.
559

560
    """
561
    storage_type = self._checkStringVariable("storage_type", None)
562
    name = self._checkStringVariable("name", None)
563

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

    
568
    changes = {}
569

    
570
    if "allocatable" in self.queryargs:
571
      changes[constants.SF_ALLOCATABLE] = \
572
        bool(self._checkIntVariable("allocatable", default=1))
573

    
574
    return ({}, {
575
      "node_name": self.items[0],
576
      "storage_type": storage_type,
577
      "name": name,
578
      "changes": changes,
579
      })
580

    
581

    
582
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
583
  """/2/nodes/[node_name]/storage/repair resource.
584

585
  """
586
  PUT_OPCODE = opcodes.OpRepairNodeStorage
587

    
588
  def GetPutOpInput(self):
589
    """Repairs a storage volume on a node.
590

591
    """
592
    storage_type = self._checkStringVariable("storage_type", None)
593
    name = self._checkStringVariable("name", None)
594
    if not name:
595
      raise http.HttpBadRequest("Missing the required 'name'"
596
                                " parameter")
597

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

    
604

    
605
class R_2_groups(baserlib.OpcodeResource):
606
  """/2/groups resource.
607

608
  """
609
  GET_OPCODE = opcodes.OpGroupQuery
610
  POST_OPCODE = opcodes.OpGroupAdd
611
  POST_RENAME = {
612
    "name": "group_name",
613
    }
614

    
615
  def GetPostOpInput(self):
616
    """Create a node group.
617

618
    """
619
    assert not self.items
620
    return (self.request_body, {
621
      "dry_run": self.dryRun(),
622
      })
623

    
624
  def GET(self):
625
    """Returns a list of all node groups.
626

627
    """
628
    client = self.GetClient()
629

    
630
    if self.useBulk():
631
      bulkdata = client.QueryGroups([], G_FIELDS, False)
632
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
633
    else:
634
      data = client.QueryGroups([], ["name"], False)
635
      groupnames = [row[0] for row in data]
636
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
637
                                   uri_fields=("name", "uri"))
638

    
639

    
640
class R_2_groups_name(baserlib.OpcodeResource):
641
  """/2/groups/[group_name] resource.
642

643
  """
644
  DELETE_OPCODE = opcodes.OpGroupRemove
645

    
646
  def GET(self):
647
    """Send information about a node group.
648

649
    """
650
    group_name = self.items[0]
651
    client = self.GetClient()
652

    
653
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
654
                                            names=[group_name], fields=G_FIELDS,
655
                                            use_locking=self.useLocking())
656

    
657
    return baserlib.MapFields(G_FIELDS, result[0])
658

    
659
  def GetDeleteOpInput(self):
660
    """Delete a node group.
661

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

    
669

    
670
class R_2_groups_name_modify(baserlib.OpcodeResource):
671
  """/2/groups/[group_name]/modify resource.
672

673
  """
674
  PUT_OPCODE = opcodes.OpGroupSetParams
675

    
676
  def GetPutOpInput(self):
677
    """Changes some parameters of node group.
678

679
    """
680
    assert self.items
681
    return (self.request_body, {
682
      "group_name": self.items[0],
683
      })
684

    
685

    
686
class R_2_groups_name_rename(baserlib.OpcodeResource):
687
  """/2/groups/[group_name]/rename resource.
688

689
  """
690
  PUT_OPCODE = opcodes.OpGroupRename
691

    
692
  def GetPutOpInput(self):
693
    """Changes the name of a node group.
694

695
    """
696
    assert len(self.items) == 1
697
    return (self.request_body, {
698
      "group_name": self.items[0],
699
      "dry_run": self.dryRun(),
700
      })
701

    
702

    
703
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
704
  """/2/groups/[group_name]/assign-nodes resource.
705

706
  """
707
  PUT_OPCODE = opcodes.OpGroupAssignNodes
708

    
709
  def GetPutOpInput(self):
710
    """Assigns nodes to a group.
711

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

    
720

    
721
class R_2_instances(baserlib.OpcodeResource):
722
  """/2/instances resource.
723

724
  """
725
  GET_OPCODE = opcodes.OpInstanceQuery
726
  POST_OPCODE = opcodes.OpInstanceCreate
727
  POST_RENAME = {
728
    "os": "os_type",
729
    "name": "instance_name",
730
    }
731

    
732
  def GET(self):
733
    """Returns a list of all available instances.
734

735
    """
736
    client = self.GetClient()
737

    
738
    use_locking = self.useLocking()
739
    if self.useBulk():
740
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
741
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
742
    else:
743
      instancesdata = client.QueryInstances([], ["name"], use_locking)
744
      instanceslist = [row[0] for row in instancesdata]
745
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
746
                                   uri_fields=("id", "uri"))
747

    
748
  def GetPostOpInput(self):
749
    """Create an instance.
750

751
    @return: a job id
752

753
    """
754
    baserlib.CheckType(self.request_body, dict, "Body contents")
755

    
756
    # Default to request data version 0
757
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
758

    
759
    if data_version == 0:
760
      raise http.HttpBadRequest("Instance creation request version 0 is no"
761
                                " longer supported")
762
    elif data_version != 1:
763
      raise http.HttpBadRequest("Unsupported request data version %s" %
764
                                data_version)
765

    
766
    data = self.request_body.copy()
767
    # Remove "__version__"
768
    data.pop(_REQ_DATA_VERSION, None)
769

    
770
    return (data, {
771
      "dry_run": self.dryRun(),
772
      })
773

    
774

    
775
class R_2_instances_name(baserlib.OpcodeResource):
776
  """/2/instances/[instance_name] resource.
777

778
  """
779
  GET_OPCODE = opcodes.OpInstanceQuery
780
  DELETE_OPCODE = opcodes.OpInstanceRemove
781

    
782
  def GET(self):
783
    """Send information about an instance.
784

785
    """
786
    client = self.GetClient()
787
    instance_name = self.items[0]
788

    
789
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
790
                                            names=[instance_name],
791
                                            fields=I_FIELDS,
792
                                            use_locking=self.useLocking())
793

    
794
    return baserlib.MapFields(I_FIELDS, result[0])
795

    
796
  def GetDeleteOpInput(self):
797
    """Delete an instance.
798

799
    """
800
    assert len(self.items) == 1
801
    return ({}, {
802
      "instance_name": self.items[0],
803
      "ignore_failures": False,
804
      "dry_run": self.dryRun(),
805
      })
806

    
807

    
808
class R_2_instances_name_info(baserlib.OpcodeResource):
809
  """/2/instances/[instance_name]/info resource.
810

811
  """
812
  GET_OPCODE = opcodes.OpInstanceQueryData
813

    
814
  def GetGetOpInput(self):
815
    """Request detailed instance information.
816

817
    """
818
    assert len(self.items) == 1
819
    return ({}, {
820
      "instances": [self.items[0]],
821
      "static": bool(self._checkIntVariable("static", default=0)),
822
      })
823

    
824

    
825
class R_2_instances_name_reboot(baserlib.OpcodeResource):
826
  """/2/instances/[instance_name]/reboot resource.
827

828
  Implements an instance reboot.
829

830
  """
831
  POST_OPCODE = opcodes.OpInstanceReboot
832

    
833
  def GetPostOpInput(self):
834
    """Reboot an instance.
835

836
    The URI takes type=[hard|soft|full] and
837
    ignore_secondaries=[False|True] parameters.
838

839
    """
840
    return ({}, {
841
      "instance_name": self.items[0],
842
      "reboot_type":
843
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
844
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
845
      "dry_run": self.dryRun(),
846
      })
847

    
848

    
849
class R_2_instances_name_startup(baserlib.OpcodeResource):
850
  """/2/instances/[instance_name]/startup resource.
851

852
  Implements an instance startup.
853

854
  """
855
  PUT_OPCODE = opcodes.OpInstanceStartup
856

    
857
  def GetPutOpInput(self):
858
    """Startup an instance.
859

860
    The URI takes force=[False|True] parameter to start the instance
861
    if even if secondary disks are failing.
862

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

    
871

    
872
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
873
  """/2/instances/[instance_name]/shutdown resource.
874

875
  Implements an instance shutdown.
876

877
  """
878
  PUT_OPCODE = opcodes.OpInstanceShutdown
879

    
880
  def GetPutOpInput(self):
881
    """Shutdown an instance.
882

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

    
890

    
891
def _ParseInstanceReinstallRequest(name, data):
892
  """Parses a request for reinstalling an instance.
893

894
  """
895
  if not isinstance(data, dict):
896
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
897

    
898
  ostype = baserlib.CheckParameter(data, "os", default=None)
899
  start = baserlib.CheckParameter(data, "start", exptype=bool,
900
                                  default=True)
901
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
902

    
903
  ops = [
904
    opcodes.OpInstanceShutdown(instance_name=name),
905
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
906
                                osparams=osparams),
907
    ]
908

    
909
  if start:
910
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
911

    
912
  return ops
913

    
914

    
915
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
916
  """/2/instances/[instance_name]/reinstall resource.
917

918
  Implements an instance reinstall.
919

920
  """
921
  POST_OPCODE = opcodes.OpInstanceReinstall
922

    
923
  def POST(self):
924
    """Reinstall an instance.
925

926
    The URI takes os=name and nostartup=[0|1] optional
927
    parameters. By default, the instance will be started
928
    automatically.
929

930
    """
931
    if self.request_body:
932
      if self.queryargs:
933
        raise http.HttpBadRequest("Can't combine query and body parameters")
934

    
935
      body = self.request_body
936
    elif self.queryargs:
937
      # Legacy interface, do not modify/extend
938
      body = {
939
        "os": self._checkStringVariable("os"),
940
        "start": not self._checkIntVariable("nostartup"),
941
        }
942
    else:
943
      body = {}
944

    
945
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
946

    
947
    return self.SubmitJob(ops)
948

    
949

    
950
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
951
  """/2/instances/[instance_name]/replace-disks resource.
952

953
  """
954
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
955

    
956
  def GetPostOpInput(self):
957
    """Replaces disks on an instance.
958

959
    """
960
    data = self.request_body.copy()
961
    static = {
962
      "instance_name": self.items[0],
963
      }
964

    
965
    # Parse disks
966
    try:
967
      raw_disks = data["disks"]
968
    except KeyError:
969
      pass
970
    else:
971
      if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
972
        # Backwards compatibility for strings of the format "1, 2, 3"
973
        try:
974
          data["disks"] = [int(part) for part in raw_disks.split(",")]
975
        except (TypeError, ValueError), err:
976
          raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
977

    
978
    return (data, static)
979

    
980

    
981
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
982
  """/2/instances/[instance_name]/activate-disks resource.
983

984
  """
985
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
986

    
987
  def GetPutOpInput(self):
988
    """Activate disks for an instance.
989

990
    The URI might contain ignore_size to ignore current recorded size.
991

992
    """
993
    return ({}, {
994
      "instance_name": self.items[0],
995
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
996
      })
997

    
998

    
999
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1000
  """/2/instances/[instance_name]/deactivate-disks resource.
1001

1002
  """
1003
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1004

    
1005
  def GetPutOpInput(self):
1006
    """Deactivate disks for an instance.
1007

1008
    """
1009
    return ({}, {
1010
      "instance_name": self.items[0],
1011
      })
1012

    
1013

    
1014
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1015
  """/2/instances/[instance_name]/recreate-disks resource.
1016

1017
  """
1018
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1019

    
1020
  def GetPostOpInput(self):
1021
    """Recreate disks for an instance.
1022

1023
    """
1024
    return ({}, {
1025
      "instance_name": self.items[0],
1026
      })
1027

    
1028

    
1029
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1030
  """/2/instances/[instance_name]/prepare-export resource.
1031

1032
  """
1033
  PUT_OPCODE = opcodes.OpBackupPrepare
1034

    
1035
  def GetPutOpInput(self):
1036
    """Prepares an export for an instance.
1037

1038
    """
1039
    return ({}, {
1040
      "instance_name": self.items[0],
1041
      "mode": self._checkStringVariable("mode"),
1042
      })
1043

    
1044

    
1045
class R_2_instances_name_export(baserlib.OpcodeResource):
1046
  """/2/instances/[instance_name]/export resource.
1047

1048
  """
1049
  PUT_OPCODE = opcodes.OpBackupExport
1050
  PUT_RENAME = {
1051
    "destination": "target_node",
1052
    }
1053

    
1054
  def GetPutOpInput(self):
1055
    """Exports an instance.
1056

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

    
1062

    
1063
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1064
  """/2/instances/[instance_name]/migrate resource.
1065

1066
  """
1067
  PUT_OPCODE = opcodes.OpInstanceMigrate
1068

    
1069
  def GetPutOpInput(self):
1070
    """Migrates an instance.
1071

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

    
1077

    
1078
class R_2_instances_name_failover(baserlib.OpcodeResource):
1079
  """/2/instances/[instance_name]/failover resource.
1080

1081
  """
1082
  PUT_OPCODE = opcodes.OpInstanceFailover
1083

    
1084
  def GetPutOpInput(self):
1085
    """Does a failover of an instance.
1086

1087
    """
1088
    return (self.request_body, {
1089
      "instance_name": self.items[0],
1090
      })
1091

    
1092

    
1093
class R_2_instances_name_rename(baserlib.OpcodeResource):
1094
  """/2/instances/[instance_name]/rename resource.
1095

1096
  """
1097
  PUT_OPCODE = opcodes.OpInstanceRename
1098

    
1099
  def GetPutOpInput(self):
1100
    """Changes the name of an instance.
1101

1102
    """
1103
    return (self.request_body, {
1104
      "instance_name": self.items[0],
1105
      })
1106

    
1107

    
1108
class R_2_instances_name_modify(baserlib.OpcodeResource):
1109
  """/2/instances/[instance_name]/modify resource.
1110

1111
  """
1112
  PUT_OPCODE = opcodes.OpInstanceSetParams
1113

    
1114
  def GetPutOpInput(self):
1115
    """Changes parameters of an instance.
1116

1117
    """
1118
    return (self.request_body, {
1119
      "instance_name": self.items[0],
1120
      })
1121

    
1122

    
1123
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1124
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1125

1126
  """
1127
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1128

    
1129
  def GetPostOpInput(self):
1130
    """Increases the size of an instance disk.
1131

1132
    """
1133
    return (self.request_body, {
1134
      "instance_name": self.items[0],
1135
      "disk": int(self.items[1]),
1136
      })
1137

    
1138

    
1139
class R_2_instances_name_console(baserlib.ResourceBase):
1140
  """/2/instances/[instance_name]/console resource.
1141

1142
  """
1143
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1144
  GET_OPCODE = opcodes.OpInstanceConsole
1145

    
1146
  def GET(self):
1147
    """Request information for connecting to instance's console.
1148

1149
    @return: Serialized instance console description, see
1150
             L{objects.InstanceConsole}
1151

1152
    """
1153
    client = self.GetClient()
1154

    
1155
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1156

    
1157
    if console is None:
1158
      raise http.HttpServiceUnavailable("Instance console unavailable")
1159

    
1160
    assert isinstance(console, dict)
1161
    return console
1162

    
1163

    
1164
def _GetQueryFields(args):
1165
  """
1166

1167
  """
1168
  try:
1169
    fields = args["fields"]
1170
  except KeyError:
1171
    raise http.HttpBadRequest("Missing 'fields' query argument")
1172

    
1173
  return _SplitQueryFields(fields[0])
1174

    
1175

    
1176
def _SplitQueryFields(fields):
1177
  """
1178

1179
  """
1180
  return [i.strip() for i in fields.split(",")]
1181

    
1182

    
1183
class R_2_query(baserlib.ResourceBase):
1184
  """/2/query/[resource] resource.
1185

1186
  """
1187
  # Results might contain sensitive information
1188
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1189
  GET_OPCODE = opcodes.OpQuery
1190
  PUT_OPCODE = opcodes.OpQuery
1191

    
1192
  def _Query(self, fields, filter_):
1193
    return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1194

    
1195
  def GET(self):
1196
    """Returns resource information.
1197

1198
    @return: Query result, see L{objects.QueryResponse}
1199

1200
    """
1201
    return self._Query(_GetQueryFields(self.queryargs), None)
1202

    
1203
  def PUT(self):
1204
    """Submits job querying for resources.
1205

1206
    @return: Query result, see L{objects.QueryResponse}
1207

1208
    """
1209
    body = self.request_body
1210

    
1211
    baserlib.CheckType(body, dict, "Body contents")
1212

    
1213
    try:
1214
      fields = body["fields"]
1215
    except KeyError:
1216
      fields = _GetQueryFields(self.queryargs)
1217

    
1218
    return self._Query(fields, self.request_body.get("filter", None))
1219

    
1220

    
1221
class R_2_query_fields(baserlib.ResourceBase):
1222
  """/2/query/[resource]/fields resource.
1223

1224
  """
1225
  GET_OPCODE = opcodes.OpQueryFields
1226

    
1227
  def GET(self):
1228
    """Retrieves list of available fields for a resource.
1229

1230
    @return: List of serialized L{objects.QueryFieldDefinition}
1231

1232
    """
1233
    try:
1234
      raw_fields = self.queryargs["fields"]
1235
    except KeyError:
1236
      fields = None
1237
    else:
1238
      fields = _SplitQueryFields(raw_fields[0])
1239

    
1240
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1241

    
1242

    
1243
class _R_Tags(baserlib.OpcodeResource):
1244
  """ Quasiclass for tagging resources
1245

1246
  Manages tags. When inheriting this class you must define the
1247
  TAG_LEVEL for it.
1248

1249
  """
1250
  TAG_LEVEL = None
1251
  GET_OPCODE = opcodes.OpTagsGet
1252
  PUT_OPCODE = opcodes.OpTagsSet
1253
  DELETE_OPCODE = opcodes.OpTagsDel
1254

    
1255
  def __init__(self, items, queryargs, req, **kwargs):
1256
    """A tag resource constructor.
1257

1258
    We have to override the default to sort out cluster naming case.
1259

1260
    """
1261
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1262

    
1263
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1264
      self.name = None
1265
    else:
1266
      self.name = items[0]
1267

    
1268
  def GET(self):
1269
    """Returns a list of tags.
1270

1271
    Example: ["tag1", "tag2", "tag3"]
1272

1273
    """
1274
    kind = self.TAG_LEVEL
1275

    
1276
    if kind in (constants.TAG_INSTANCE,
1277
                constants.TAG_NODEGROUP,
1278
                constants.TAG_NODE):
1279
      if not self.name:
1280
        raise http.HttpBadRequest("Missing name on tag request")
1281

    
1282
      cl = self.GetClient()
1283
      if kind == constants.TAG_INSTANCE:
1284
        fn = cl.QueryInstances
1285
      elif kind == constants.TAG_NODEGROUP:
1286
        fn = cl.QueryGroups
1287
      else:
1288
        fn = cl.QueryNodes
1289
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1290
      if not result or not result[0]:
1291
        raise http.HttpBadGateway("Invalid response from tag query")
1292
      tags = result[0][0]
1293

    
1294
    elif kind == constants.TAG_CLUSTER:
1295
      assert not self.name
1296
      # TODO: Use query API?
1297
      ssc = ssconf.SimpleStore()
1298
      tags = ssc.GetClusterTags()
1299

    
1300
    return list(tags)
1301

    
1302
  def GetPutOpInput(self):
1303
    """Add a set of tags.
1304

1305
    The request as a list of strings should be PUT to this URI. And
1306
    you'll have back a job id.
1307

1308
    """
1309
    return ({}, {
1310
      "kind": self.TAG_LEVEL,
1311
      "name": self.name,
1312
      "tags": self.queryargs.get("tag", []),
1313
      "dry_run": self.dryRun(),
1314
      })
1315

    
1316
  def GetDeleteOpInput(self):
1317
    """Delete a tag.
1318

1319
    In order to delete a set of tags, the DELETE
1320
    request should be addressed to URI like:
1321
    /tags?tag=[tag]&tag=[tag]
1322

1323
    """
1324
    # Re-use code
1325
    return self.GetPutOpInput()
1326

    
1327

    
1328
class R_2_instances_name_tags(_R_Tags):
1329
  """ /2/instances/[instance_name]/tags resource.
1330

1331
  Manages per-instance tags.
1332

1333
  """
1334
  TAG_LEVEL = constants.TAG_INSTANCE
1335

    
1336

    
1337
class R_2_nodes_name_tags(_R_Tags):
1338
  """ /2/nodes/[node_name]/tags resource.
1339

1340
  Manages per-node tags.
1341

1342
  """
1343
  TAG_LEVEL = constants.TAG_NODE
1344

    
1345

    
1346
class R_2_groups_name_tags(_R_Tags):
1347
  """ /2/groups/[group_name]/tags resource.
1348

1349
  Manages per-nodegroup tags.
1350

1351
  """
1352
  TAG_LEVEL = constants.TAG_NODEGROUP
1353

    
1354

    
1355
class R_2_tags(_R_Tags):
1356
  """ /2/tags resource.
1357

1358
  Manages cluster tags.
1359

1360
  """
1361
  TAG_LEVEL = constants.TAG_CLUSTER