Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 132cdb87

History | View | Annotate | Download (32.1 kB)

1
#
2
#
3

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

    
21

    
22
"""Remote API resource implementations.
23

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

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

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

35
Quoting from RFC2616, section 9.6::
36

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

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

52
"""
53

    
54
# pylint: disable-msg=C0103
55

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

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

    
68

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

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

    
92
G_FIELDS = [
93
  "alloc_policy",
94
  "name",
95
  "node_cnt",
96
  "node_list",
97
  ] + _COMMON_FIELDS
98

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

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

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

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

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

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

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

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

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

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

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

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

    
151

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

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

160
    """
161
    return None
162

    
163

    
164
class R_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.ResourceBase):
186
  """/2/info resource.
187

188
  """
189
  def GET(self):
190
    """Returns cluster information.
191

192
    """
193
    client = self.GetClient()
194
    return client.QueryClusterInfo()
195

    
196

    
197
class R_2_features(baserlib.ResourceBase):
198
  """/2/features resource.
199

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

205
    """
206
    return list(ALL_FEATURES)
207

    
208

    
209
class R_2_os(baserlib.ResourceBase):
210
  """/2/os resource.
211

212
  """
213
  def GET(self):
214
    """Return a list of all OSes.
215

216
    Can return error 500 in case of a problem.
217

218
    Example: ["debian-etch"]
219

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

    
228
    if not isinstance(diagnose_data, list):
229
      raise http.HttpBadGateway(message="Can't get OS list")
230

    
231
    os_names = []
232
    for (name, variants) in diagnose_data:
233
      os_names.extend(cli.CalculateOSNames(name, variants))
234

    
235
    return os_names
236

    
237

    
238
class R_2_redist_config(baserlib.OpcodeResource):
239
  """/2/redistribute-config resource.
240

241
  """
242
  PUT_OPCODE = opcodes.OpClusterRedistConf
243

    
244

    
245
class R_2_cluster_modify(baserlib.OpcodeResource):
246
  """/2/modify resource.
247

248
  """
249
  PUT_OPCODE = opcodes.OpClusterSetParams
250

    
251

    
252
class R_2_jobs(baserlib.ResourceBase):
253
  """/2/jobs resource.
254

255
  """
256
  def GET(self):
257
    """Returns a dictionary of jobs.
258

259
    @return: a dictionary with jobs id and uri.
260

261
    """
262
    client = self.GetClient()
263

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

    
272

    
273
class R_2_jobs_id(baserlib.ResourceBase):
274
  """/2/jobs/[job_id] resource.
275

276
  """
277
  def GET(self):
278
    """Returns a job status.
279

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

289
    """
290
    job_id = self.items[0]
291
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
292
    if result is None:
293
      raise http.HttpNotFound()
294
    return baserlib.MapFields(J_FIELDS, result)
295

    
296
  def DELETE(self):
297
    """Cancel not-yet-started job.
298

299
    """
300
    job_id = self.items[0]
301
    result = self.GetClient().CancelJob(job_id)
302
    return result
303

    
304

    
305
class R_2_jobs_id_wait(baserlib.ResourceBase):
306
  """/2/jobs/[job_id]/wait resource.
307

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

    
313
  def GET(self):
314
    """Waits for job changes.
315

316
    """
317
    job_id = self.items[0]
318

    
319
    fields = self.getBodyParameter("fields")
320
    prev_job_info = self.getBodyParameter("previous_job_info", None)
321
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
322

    
323
    if not isinstance(fields, list):
324
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
325

    
326
    if not (prev_job_info is None or isinstance(prev_job_info, list)):
327
      raise http.HttpBadRequest("The 'previous_job_info' parameter should"
328
                                " be a list")
329

    
330
    if not (prev_log_serial is None or
331
            isinstance(prev_log_serial, (int, long))):
332
      raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
333
                                " be a number")
334

    
335
    client = self.GetClient()
336
    result = client.WaitForJobChangeOnce(job_id, fields,
337
                                         prev_job_info, prev_log_serial,
338
                                         timeout=_WFJC_TIMEOUT)
339
    if not result:
340
      raise http.HttpNotFound()
341

    
342
    if result == constants.JOB_NOTCHANGED:
343
      # No changes
344
      return None
345

    
346
    (job_info, log_entries) = result
347

    
348
    return {
349
      "job_info": job_info,
350
      "log_entries": log_entries,
351
      }
352

    
353

    
354
class R_2_nodes(baserlib.ResourceBase):
355
  """/2/nodes resource.
356

357
  """
358
  def GET(self):
359
    """Returns a list of all nodes.
360

361
    """
362
    client = self.GetClient()
363

    
364
    if self.useBulk():
365
      bulkdata = client.QueryNodes([], N_FIELDS, False)
366
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
367
    else:
368
      nodesdata = client.QueryNodes([], ["name"], False)
369
      nodeslist = [row[0] for row in nodesdata]
370
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
371
                                   uri_fields=("id", "uri"))
372

    
373

    
374
class R_2_nodes_name(baserlib.ResourceBase):
375
  """/2/nodes/[node_name] resource.
376

377
  """
378
  def GET(self):
379
    """Send information about a node.
380

381
    """
382
    node_name = self.items[0]
383
    client = self.GetClient()
384

    
385
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
386
                                            names=[node_name], fields=N_FIELDS,
387
                                            use_locking=self.useLocking())
388

    
389
    return baserlib.MapFields(N_FIELDS, result[0])
390

    
391

    
392
class R_2_nodes_name_role(baserlib.OpcodeResource):
393
  """/2/nodes/[node_name]/role resource.
394

395
  """
396
  PUT_OPCODE = opcodes.OpNodeSetParams
397

    
398
  def GET(self):
399
    """Returns the current node role.
400

401
    @return: Node role
402

403
    """
404
    node_name = self.items[0]
405
    client = self.GetClient()
406
    result = client.QueryNodes(names=[node_name], fields=["role"],
407
                               use_locking=self.useLocking())
408

    
409
    return _NR_MAP[result[0][0]]
410

    
411
  def GetPutOpInput(self):
412
    """Sets the node role.
413

414
    """
415
    baserlib.CheckType(self.request_body, basestring, "Body contents")
416

    
417
    role = self.request_body
418

    
419
    if role == _NR_REGULAR:
420
      candidate = False
421
      offline = False
422
      drained = False
423

    
424
    elif role == _NR_MASTER_CANDIDATE:
425
      candidate = True
426
      offline = drained = None
427

    
428
    elif role == _NR_DRAINED:
429
      drained = True
430
      candidate = offline = None
431

    
432
    elif role == _NR_OFFLINE:
433
      offline = True
434
      candidate = drained = None
435

    
436
    else:
437
      raise http.HttpBadRequest("Can't set '%s' role" % role)
438

    
439
    assert len(self.items) == 1
440

    
441
    return ({}, {
442
      "node_name": self.items[0],
443
      "master_candidate": candidate,
444
      "offline": offline,
445
      "drained": drained,
446
      "force": self.useForce(),
447
      })
448

    
449

    
450
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
451
  """/2/nodes/[node_name]/evacuate resource.
452

453
  """
454
  POST_OPCODE = opcodes.OpNodeEvacuate
455

    
456
  def GetPostOpInput(self):
457
    """Evacuate all instances off a node.
458

459
    """
460
    return (self.request_body, {
461
      "node_name": self.items[0],
462
      "dry_run": self.dryRun(),
463
      })
464

    
465

    
466
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
467
  """/2/nodes/[node_name]/migrate resource.
468

469
  """
470
  POST_OPCODE = opcodes.OpNodeMigrate
471

    
472
  def GetPostOpInput(self):
473
    """Migrate all primary instances from a node.
474

475
    """
476
    if self.queryargs:
477
      # Support old-style requests
478
      if "live" in self.queryargs and "mode" in self.queryargs:
479
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
480
                                  " be passed")
481

    
482
      if "live" in self.queryargs:
483
        if self._checkIntVariable("live", default=1):
484
          mode = constants.HT_MIGRATION_LIVE
485
        else:
486
          mode = constants.HT_MIGRATION_NONLIVE
487
      else:
488
        mode = self._checkStringVariable("mode", default=None)
489

    
490
      data = {
491
        "mode": mode,
492
        }
493
    else:
494
      data = self.request_body
495

    
496
    return (data, {
497
      "node_name": self.items[0],
498
      })
499

    
500

    
501
class R_2_nodes_name_storage(baserlib.OpcodeResource):
502
  """/2/nodes/[node_name]/storage resource.
503

504
  """
505
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
506
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
507
  GET_OPCODE = opcodes.OpNodeQueryStorage
508

    
509
  def GetGetOpInput(self):
510
    """List storage available on a node.
511

512
    """
513
    storage_type = self._checkStringVariable("storage_type", None)
514
    output_fields = self._checkStringVariable("output_fields", None)
515

    
516
    if not output_fields:
517
      raise http.HttpBadRequest("Missing the required 'output_fields'"
518
                                " parameter")
519

    
520
    return ({}, {
521
      "nodes": [self.items[0]],
522
      "storage_type": storage_type,
523
      "output_fields": output_fields.split(","),
524
      })
525

    
526

    
527
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
528
  """/2/nodes/[node_name]/storage/modify resource.
529

530
  """
531
  PUT_OPCODE = opcodes.OpNodeModifyStorage
532

    
533
  def GetPutOpInput(self):
534
    """Modifies a storage volume on a node.
535

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

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

    
544
    changes = {}
545

    
546
    if "allocatable" in self.queryargs:
547
      changes[constants.SF_ALLOCATABLE] = \
548
        bool(self._checkIntVariable("allocatable", default=1))
549

    
550
    return ({}, {
551
      "node_name": self.items[0],
552
      "storage_type": storage_type,
553
      "name": name,
554
      "changes": changes,
555
      })
556

    
557

    
558
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
559
  """/2/nodes/[node_name]/storage/repair resource.
560

561
  """
562
  PUT_OPCODE = opcodes.OpRepairNodeStorage
563

    
564
  def GetPutOpInput(self):
565
    """Repairs a storage volume on a node.
566

567
    """
568
    storage_type = self._checkStringVariable("storage_type", None)
569
    name = self._checkStringVariable("name", None)
570
    if not name:
571
      raise http.HttpBadRequest("Missing the required 'name'"
572
                                " parameter")
573

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

    
580

    
581
class R_2_groups(baserlib.OpcodeResource):
582
  """/2/groups resource.
583

584
  """
585
  POST_OPCODE = opcodes.OpGroupAdd
586
  POST_RENAME = {
587
    "name": "group_name",
588
    }
589

    
590
  def GetPostOpInput(self):
591
    """Create a node group.
592

593
    """
594
    assert not self.items
595
    return (self.request_body, {
596
      "dry_run": self.dryRun(),
597
      })
598

    
599
  def GET(self):
600
    """Returns a list of all node groups.
601

602
    """
603
    client = self.GetClient()
604

    
605
    if self.useBulk():
606
      bulkdata = client.QueryGroups([], G_FIELDS, False)
607
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
608
    else:
609
      data = client.QueryGroups([], ["name"], False)
610
      groupnames = [row[0] for row in data]
611
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
612
                                   uri_fields=("name", "uri"))
613

    
614

    
615
class R_2_groups_name(baserlib.OpcodeResource):
616
  """/2/groups/[group_name] resource.
617

618
  """
619
  DELETE_OPCODE = opcodes.OpGroupRemove
620

    
621
  def GET(self):
622
    """Send information about a node group.
623

624
    """
625
    group_name = self.items[0]
626
    client = self.GetClient()
627

    
628
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
629
                                            names=[group_name], fields=G_FIELDS,
630
                                            use_locking=self.useLocking())
631

    
632
    return baserlib.MapFields(G_FIELDS, result[0])
633

    
634
  def GetDeleteOpInput(self):
635
    """Delete a node group.
636

637
    """
638
    assert len(self.items) == 1
639
    return ({}, {
640
      "group_name": self.items[0],
641
      "dry_run": self.dryRun(),
642
      })
643

    
644

    
645
class R_2_groups_name_modify(baserlib.OpcodeResource):
646
  """/2/groups/[group_name]/modify resource.
647

648
  """
649
  PUT_OPCODE = opcodes.OpGroupSetParams
650

    
651
  def GetPutOpInput(self):
652
    """Changes some parameters of node group.
653

654
    """
655
    assert self.items
656
    return (self.request_body, {
657
      "group_name": self.items[0],
658
      })
659

    
660

    
661
class R_2_groups_name_rename(baserlib.OpcodeResource):
662
  """/2/groups/[group_name]/rename resource.
663

664
  """
665
  PUT_OPCODE = opcodes.OpGroupRename
666

    
667
  def GetPutOpInput(self):
668
    """Changes the name of a node group.
669

670
    """
671
    assert len(self.items) == 1
672
    return (self.request_body, {
673
      "group_name": self.items[0],
674
      "dry_run": self.dryRun(),
675
      })
676

    
677

    
678
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
679
  """/2/groups/[group_name]/assign-nodes resource.
680

681
  """
682
  PUT_OPCODE = opcodes.OpGroupAssignNodes
683

    
684
  def GetPutOpInput(self):
685
    """Assigns nodes to a group.
686

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

    
695

    
696
class R_2_instances(baserlib.OpcodeResource):
697
  """/2/instances resource.
698

699
  """
700
  POST_OPCODE = opcodes.OpInstanceCreate
701
  POST_RENAME = {
702
    "os": "os_type",
703
    "name": "instance_name",
704
    }
705

    
706
  def GET(self):
707
    """Returns a list of all available instances.
708

709
    """
710
    client = self.GetClient()
711

    
712
    use_locking = self.useLocking()
713
    if self.useBulk():
714
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
715
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
716
    else:
717
      instancesdata = client.QueryInstances([], ["name"], use_locking)
718
      instanceslist = [row[0] for row in instancesdata]
719
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
720
                                   uri_fields=("id", "uri"))
721

    
722
  def GetPostOpInput(self):
723
    """Create an instance.
724

725
    @return: a job id
726

727
    """
728
    baserlib.CheckType(self.request_body, dict, "Body contents")
729

    
730
    # Default to request data version 0
731
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
732

    
733
    if data_version == 0:
734
      raise http.HttpBadRequest("Instance creation request version 0 is no"
735
                                " longer supported")
736
    elif data_version != 1:
737
      raise http.HttpBadRequest("Unsupported request data version %s" %
738
                                data_version)
739

    
740
    data = self.request_body.copy()
741
    # Remove "__version__"
742
    data.pop(_REQ_DATA_VERSION, None)
743

    
744
    return (data, {
745
      "dry_run": self.dryRun(),
746
      })
747

    
748

    
749
class R_2_instances_name(baserlib.OpcodeResource):
750
  """/2/instances/[instance_name] resource.
751

752
  """
753
  DELETE_OPCODE = opcodes.OpInstanceRemove
754

    
755
  def GET(self):
756
    """Send information about an instance.
757

758
    """
759
    client = self.GetClient()
760
    instance_name = self.items[0]
761

    
762
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
763
                                            names=[instance_name],
764
                                            fields=I_FIELDS,
765
                                            use_locking=self.useLocking())
766

    
767
    return baserlib.MapFields(I_FIELDS, result[0])
768

    
769
  def GetDeleteOpInput(self):
770
    """Delete an instance.
771

772
    """
773
    assert len(self.items) == 1
774
    return ({}, {
775
      "instance_name": self.items[0],
776
      "ignore_failures": False,
777
      "dry_run": self.dryRun(),
778
      })
779

    
780

    
781
class R_2_instances_name_info(baserlib.OpcodeResource):
782
  """/2/instances/[instance_name]/info resource.
783

784
  """
785
  GET_OPCODE = opcodes.OpInstanceQueryData
786

    
787
  def GetGetOpInput(self):
788
    """Request detailed instance information.
789

790
    """
791
    assert len(self.items) == 1
792
    return ({}, {
793
      "instances": [self.items[0]],
794
      "static": bool(self._checkIntVariable("static", default=0)),
795
      })
796

    
797

    
798
class R_2_instances_name_reboot(baserlib.OpcodeResource):
799
  """/2/instances/[instance_name]/reboot resource.
800

801
  Implements an instance reboot.
802

803
  """
804
  POST_OPCODE = opcodes.OpInstanceReboot
805

    
806
  def GetPostOpInput(self):
807
    """Reboot an instance.
808

809
    The URI takes type=[hard|soft|full] and
810
    ignore_secondaries=[False|True] parameters.
811

812
    """
813
    return ({}, {
814
      "instance_name": self.items[0],
815
      "reboot_type":
816
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
817
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
818
      "dry_run": self.dryRun(),
819
      })
820

    
821

    
822
class R_2_instances_name_startup(baserlib.OpcodeResource):
823
  """/2/instances/[instance_name]/startup resource.
824

825
  Implements an instance startup.
826

827
  """
828
  PUT_OPCODE = opcodes.OpInstanceStartup
829

    
830
  def GetPutOpInput(self):
831
    """Startup an instance.
832

833
    The URI takes force=[False|True] parameter to start the instance
834
    if even if secondary disks are failing.
835

836
    """
837
    return ({}, {
838
      "instance_name": self.items[0],
839
      "force": self.useForce(),
840
      "dry_run": self.dryRun(),
841
      "no_remember": bool(self._checkIntVariable("no_remember")),
842
      })
843

    
844

    
845
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
846
  """/2/instances/[instance_name]/shutdown resource.
847

848
  Implements an instance shutdown.
849

850
  """
851
  PUT_OPCODE = opcodes.OpInstanceShutdown
852

    
853
  def GetPutOpInput(self):
854
    """Shutdown an instance.
855

856
    """
857
    return (self.request_body, {
858
      "instance_name": self.items[0],
859
      "no_remember": bool(self._checkIntVariable("no_remember")),
860
      "dry_run": self.dryRun(),
861
      })
862

    
863

    
864
def _ParseInstanceReinstallRequest(name, data):
865
  """Parses a request for reinstalling an instance.
866

867
  """
868
  if not isinstance(data, dict):
869
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
870

    
871
  ostype = baserlib.CheckParameter(data, "os", default=None)
872
  start = baserlib.CheckParameter(data, "start", exptype=bool,
873
                                  default=True)
874
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
875

    
876
  ops = [
877
    opcodes.OpInstanceShutdown(instance_name=name),
878
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
879
                                osparams=osparams),
880
    ]
881

    
882
  if start:
883
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
884

    
885
  return ops
886

    
887

    
888
class R_2_instances_name_reinstall(baserlib.ResourceBase):
889
  """/2/instances/[instance_name]/reinstall resource.
890

891
  Implements an instance reinstall.
892

893
  """
894
  def POST(self):
895
    """Reinstall an instance.
896

897
    The URI takes os=name and nostartup=[0|1] optional
898
    parameters. By default, the instance will be started
899
    automatically.
900

901
    """
902
    if self.request_body:
903
      if self.queryargs:
904
        raise http.HttpBadRequest("Can't combine query and body parameters")
905

    
906
      body = self.request_body
907
    elif self.queryargs:
908
      # Legacy interface, do not modify/extend
909
      body = {
910
        "os": self._checkStringVariable("os"),
911
        "start": not self._checkIntVariable("nostartup"),
912
        }
913
    else:
914
      body = {}
915

    
916
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
917

    
918
    return self.SubmitJob(ops)
919

    
920

    
921
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
922
  """/2/instances/[instance_name]/replace-disks resource.
923

924
  """
925
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
926

    
927
  def GetPostOpInput(self):
928
    """Replaces disks on an instance.
929

930
    """
931
    data = self.request_body.copy()
932
    static = {
933
      "instance_name": self.items[0],
934
      }
935

    
936
    # Parse disks
937
    try:
938
      raw_disks = data["disks"]
939
    except KeyError:
940
      pass
941
    else:
942
      if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
943
        # Backwards compatibility for strings of the format "1, 2, 3"
944
        try:
945
          data["disks"] = [int(part) for part in raw_disks.split(",")]
946
        except (TypeError, ValueError), err:
947
          raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
948

    
949
    return (data, static)
950

    
951

    
952
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
953
  """/2/instances/[instance_name]/activate-disks resource.
954

955
  """
956
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
957

    
958
  def GetPutOpInput(self):
959
    """Activate disks for an instance.
960

961
    The URI might contain ignore_size to ignore current recorded size.
962

963
    """
964
    return ({}, {
965
      "instance_name": self.items[0],
966
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
967
      })
968

    
969

    
970
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
971
  """/2/instances/[instance_name]/deactivate-disks resource.
972

973
  """
974
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
975

    
976
  def GetPutOpInput(self):
977
    """Deactivate disks for an instance.
978

979
    """
980
    return ({}, {
981
      "instance_name": self.items[0],
982
      })
983

    
984

    
985
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
986
  """/2/instances/[instance_name]/prepare-export resource.
987

988
  """
989
  PUT_OPCODE = opcodes.OpBackupPrepare
990

    
991
  def GetPutOpInput(self):
992
    """Prepares an export for an instance.
993

994
    """
995
    return ({}, {
996
      "instance_name": self.items[0],
997
      "mode": self._checkStringVariable("mode"),
998
      })
999

    
1000

    
1001
class R_2_instances_name_export(baserlib.OpcodeResource):
1002
  """/2/instances/[instance_name]/export resource.
1003

1004
  """
1005
  PUT_OPCODE = opcodes.OpBackupExport
1006
  PUT_RENAME = {
1007
    "destination": "target_node",
1008
    }
1009

    
1010
  def GetPutOpInput(self):
1011
    """Exports an instance.
1012

1013
    """
1014
    return (self.request_body, {
1015
      "instance_name": self.items[0],
1016
      })
1017

    
1018

    
1019
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1020
  """/2/instances/[instance_name]/migrate resource.
1021

1022
  """
1023
  PUT_OPCODE = opcodes.OpInstanceMigrate
1024

    
1025
  def GetPutOpInput(self):
1026
    """Migrates an instance.
1027

1028
    """
1029
    return (self.request_body, {
1030
      "instance_name": self.items[0],
1031
      })
1032

    
1033

    
1034
class R_2_instances_name_failover(baserlib.OpcodeResource):
1035
  """/2/instances/[instance_name]/failover resource.
1036

1037
  """
1038
  PUT_OPCODE = opcodes.OpInstanceFailover
1039

    
1040
  def GetPutOpInput(self):
1041
    """Does a failover of an instance.
1042

1043
    """
1044
    return (self.request_body, {
1045
      "instance_name": self.items[0],
1046
      })
1047

    
1048

    
1049
class R_2_instances_name_rename(baserlib.OpcodeResource):
1050
  """/2/instances/[instance_name]/rename resource.
1051

1052
  """
1053
  PUT_OPCODE = opcodes.OpInstanceRename
1054

    
1055
  def GetPutOpInput(self):
1056
    """Changes the name of an instance.
1057

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

    
1063

    
1064
class R_2_instances_name_modify(baserlib.OpcodeResource):
1065
  """/2/instances/[instance_name]/modify resource.
1066

1067
  """
1068
  PUT_OPCODE = opcodes.OpInstanceSetParams
1069

    
1070
  def GetPutOpInput(self):
1071
    """Changes parameters of an instance.
1072

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

    
1078

    
1079
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1080
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1081

1082
  """
1083
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1084

    
1085
  def GetPostOpInput(self):
1086
    """Increases the size of an instance disk.
1087

1088
    """
1089
    return (self.request_body, {
1090
      "instance_name": self.items[0],
1091
      "disk": int(self.items[1]),
1092
      })
1093

    
1094

    
1095
class R_2_instances_name_console(baserlib.ResourceBase):
1096
  """/2/instances/[instance_name]/console resource.
1097

1098
  """
1099
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1100

    
1101
  def GET(self):
1102
    """Request information for connecting to instance's console.
1103

1104
    @return: Serialized instance console description, see
1105
             L{objects.InstanceConsole}
1106

1107
    """
1108
    client = self.GetClient()
1109

    
1110
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1111

    
1112
    if console is None:
1113
      raise http.HttpServiceUnavailable("Instance console unavailable")
1114

    
1115
    assert isinstance(console, dict)
1116
    return console
1117

    
1118

    
1119
def _GetQueryFields(args):
1120
  """
1121

1122
  """
1123
  try:
1124
    fields = args["fields"]
1125
  except KeyError:
1126
    raise http.HttpBadRequest("Missing 'fields' query argument")
1127

    
1128
  return _SplitQueryFields(fields[0])
1129

    
1130

    
1131
def _SplitQueryFields(fields):
1132
  """
1133

1134
  """
1135
  return [i.strip() for i in fields.split(",")]
1136

    
1137

    
1138
class R_2_query(baserlib.ResourceBase):
1139
  """/2/query/[resource] resource.
1140

1141
  """
1142
  # Results might contain sensitive information
1143
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1144

    
1145
  def _Query(self, fields, filter_):
1146
    return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1147

    
1148
  def GET(self):
1149
    """Returns resource information.
1150

1151
    @return: Query result, see L{objects.QueryResponse}
1152

1153
    """
1154
    return self._Query(_GetQueryFields(self.queryargs), None)
1155

    
1156
  def PUT(self):
1157
    """Submits job querying for resources.
1158

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

1161
    """
1162
    body = self.request_body
1163

    
1164
    baserlib.CheckType(body, dict, "Body contents")
1165

    
1166
    try:
1167
      fields = body["fields"]
1168
    except KeyError:
1169
      fields = _GetQueryFields(self.queryargs)
1170

    
1171
    return self._Query(fields, self.request_body.get("filter", None))
1172

    
1173

    
1174
class R_2_query_fields(baserlib.ResourceBase):
1175
  """/2/query/[resource]/fields resource.
1176

1177
  """
1178
  def GET(self):
1179
    """Retrieves list of available fields for a resource.
1180

1181
    @return: List of serialized L{objects.QueryFieldDefinition}
1182

1183
    """
1184
    try:
1185
      raw_fields = self.queryargs["fields"]
1186
    except KeyError:
1187
      fields = None
1188
    else:
1189
      fields = _SplitQueryFields(raw_fields[0])
1190

    
1191
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1192

    
1193

    
1194
class _R_Tags(baserlib.OpcodeResource):
1195
  """ Quasiclass for tagging resources
1196

1197
  Manages tags. When inheriting this class you must define the
1198
  TAG_LEVEL for it.
1199

1200
  """
1201
  TAG_LEVEL = None
1202
  PUT_OPCODE = opcodes.OpTagsSet
1203
  DELETE_OPCODE = opcodes.OpTagsDel
1204

    
1205
  def __init__(self, items, queryargs, req, **kwargs):
1206
    """A tag resource constructor.
1207

1208
    We have to override the default to sort out cluster naming case.
1209

1210
    """
1211
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1212

    
1213
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1214
      self.name = None
1215
    else:
1216
      self.name = items[0]
1217

    
1218
  def GET(self):
1219
    """Returns a list of tags.
1220

1221
    Example: ["tag1", "tag2", "tag3"]
1222

1223
    """
1224
    kind = self.TAG_LEVEL
1225

    
1226
    if kind in (constants.TAG_INSTANCE,
1227
                constants.TAG_NODEGROUP,
1228
                constants.TAG_NODE):
1229
      if not self.name:
1230
        raise http.HttpBadRequest("Missing name on tag request")
1231

    
1232
      cl = self.GetClient()
1233
      if kind == constants.TAG_INSTANCE:
1234
        fn = cl.QueryInstances
1235
      elif kind == constants.TAG_NODEGROUP:
1236
        fn = cl.QueryGroups
1237
      else:
1238
        fn = cl.QueryNodes
1239
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1240
      if not result or not result[0]:
1241
        raise http.HttpBadGateway("Invalid response from tag query")
1242
      tags = result[0][0]
1243

    
1244
    elif kind == constants.TAG_CLUSTER:
1245
      assert not self.name
1246
      # TODO: Use query API?
1247
      ssc = ssconf.SimpleStore()
1248
      tags = ssc.GetClusterTags()
1249

    
1250
    return list(tags)
1251

    
1252
  def GetPutOpInput(self):
1253
    """Add a set of tags.
1254

1255
    The request as a list of strings should be PUT to this URI. And
1256
    you'll have back a job id.
1257

1258
    """
1259
    return ({}, {
1260
      "kind": self.TAG_LEVEL,
1261
      "name": self.name,
1262
      "tags": self.queryargs.get("tag", []),
1263
      "dry_run": self.dryRun(),
1264
      })
1265

    
1266
  def GetDeleteOpInput(self):
1267
    """Delete a tag.
1268

1269
    In order to delete a set of tags, the DELETE
1270
    request should be addressed to URI like:
1271
    /tags?tag=[tag]&tag=[tag]
1272

1273
    """
1274
    # Re-use code
1275
    return self.GetPutOpInput()
1276

    
1277

    
1278
class R_2_instances_name_tags(_R_Tags):
1279
  """ /2/instances/[instance_name]/tags resource.
1280

1281
  Manages per-instance tags.
1282

1283
  """
1284
  TAG_LEVEL = constants.TAG_INSTANCE
1285

    
1286

    
1287
class R_2_nodes_name_tags(_R_Tags):
1288
  """ /2/nodes/[node_name]/tags resource.
1289

1290
  Manages per-node tags.
1291

1292
  """
1293
  TAG_LEVEL = constants.TAG_NODE
1294

    
1295

    
1296
class R_2_groups_name_tags(_R_Tags):
1297
  """ /2/groups/[group_name]/tags resource.
1298

1299
  Manages per-nodegroup tags.
1300

1301
  """
1302
  TAG_LEVEL = constants.TAG_NODEGROUP
1303

    
1304

    
1305
class R_2_tags(_R_Tags):
1306
  """ /2/tags resource.
1307

1308
  Manages cluster tags.
1309

1310
  """
1311
  TAG_LEVEL = constants.TAG_CLUSTER