Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 3bd0f3d8

History | View | Annotate | Download (34.2 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
  ] + _COMMON_FIELDS
100

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

    
107
J_FIELDS = J_FIELDS_BULK + [
108
  "oplog",
109
  "opresult",
110
  ]
111

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

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

    
126
assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
127

    
128
# Request data version field
129
_REQ_DATA_VERSION = "__version__"
130

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

    
134
# Feature string for instance reinstall request version 1
135
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
136

    
137
# Feature string for node migration version 1
138
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
139

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

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

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

    
153

    
154
class R_root(baserlib.ResourceBase):
155
  """/ resource.
156

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

162
    """
163
    return None
164

    
165

    
166
class R_2(R_root):
167
  """/2 resource.
168

169
  """
170

    
171

    
172
class R_version(baserlib.ResourceBase):
173
  """/version resource.
174

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

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

183
    """
184
    return constants.RAPI_VERSION
185

    
186

    
187
class R_2_info(baserlib.OpcodeResource):
188
  """/2/info resource.
189

190
  """
191
  GET_OPCODE = opcodes.OpClusterQuery
192

    
193
  def GET(self):
194
    """Returns cluster information.
195

196
    """
197
    client = self.GetClient()
198
    return client.QueryClusterInfo()
199

    
200

    
201
class R_2_features(baserlib.ResourceBase):
202
  """/2/features resource.
203

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

209
    """
210
    return list(ALL_FEATURES)
211

    
212

    
213
class R_2_os(baserlib.OpcodeResource):
214
  """/2/os resource.
215

216
  """
217
  GET_OPCODE = opcodes.OpOsDiagnose
218

    
219
  def GET(self):
220
    """Return a list of all OSes.
221

222
    Can return error 500 in case of a problem.
223

224
    Example: ["debian-etch"]
225

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

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

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

    
241
    return os_names
242

    
243

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

247
  """
248
  PUT_OPCODE = opcodes.OpClusterRedistConf
249

    
250

    
251
class R_2_cluster_modify(baserlib.OpcodeResource):
252
  """/2/modify resource.
253

254
  """
255
  PUT_OPCODE = opcodes.OpClusterSetParams
256

    
257

    
258
class R_2_jobs(baserlib.ResourceBase):
259
  """/2/jobs resource.
260

261
  """
262
  def GET(self):
263
    """Returns a dictionary of jobs.
264

265
    @return: a dictionary with jobs id and uri.
266

267
    """
268
    client = self.GetClient()
269

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

    
278

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

282
  """
283
  def GET(self):
284
    """Returns a job status.
285

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

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

    
302
  def DELETE(self):
303
    """Cancel not-yet-started job.
304

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

    
310

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

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

    
319
  def GET(self):
320
    """Waits for job changes.
321

322
    """
323
    job_id = self.items[0]
324

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

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

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

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

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

    
348
    if result == constants.JOB_NOTCHANGED:
349
      # No changes
350
      return None
351

    
352
    (job_info, log_entries) = result
353

    
354
    return {
355
      "job_info": job_info,
356
      "log_entries": log_entries,
357
      }
358

    
359

    
360
class R_2_nodes(baserlib.OpcodeResource):
361
  """/2/nodes resource.
362

363
  """
364
  GET_OPCODE = opcodes.OpNodeQuery
365

    
366
  def GET(self):
367
    """Returns a list of all nodes.
368

369
    """
370
    client = self.GetClient()
371

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

    
381

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

385
  """
386
  GET_OPCODE = opcodes.OpNodeQuery
387

    
388
  def GET(self):
389
    """Send information about a node.
390

391
    """
392
    node_name = self.items[0]
393
    client = self.GetClient()
394

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

    
399
    return baserlib.MapFields(N_FIELDS, result[0])
400

    
401

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

405
  """
406
  POST_OPCODE = opcodes.OpNodePowercycle
407

    
408
  def GetPostOpInput(self):
409
    """Tries to powercycle a node.
410

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

    
417

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

421
  """
422
  PUT_OPCODE = opcodes.OpNodeSetParams
423

    
424
  def GET(self):
425
    """Returns the current node role.
426

427
    @return: Node role
428

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

    
435
    return _NR_MAP[result[0][0]]
436

    
437
  def GetPutOpInput(self):
438
    """Sets the node role.
439

440
    """
441
    baserlib.CheckType(self.request_body, basestring, "Body contents")
442

    
443
    role = self.request_body
444

    
445
    if role == _NR_REGULAR:
446
      candidate = False
447
      offline = False
448
      drained = False
449

    
450
    elif role == _NR_MASTER_CANDIDATE:
451
      candidate = True
452
      offline = drained = None
453

    
454
    elif role == _NR_DRAINED:
455
      drained = True
456
      candidate = offline = None
457

    
458
    elif role == _NR_OFFLINE:
459
      offline = True
460
      candidate = drained = None
461

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

    
465
    assert len(self.items) == 1
466

    
467
    return ({}, {
468
      "node_name": self.items[0],
469
      "master_candidate": candidate,
470
      "offline": offline,
471
      "drained": drained,
472
      "force": self.useForce(),
473
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
474
      })
475

    
476

    
477
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
478
  """/2/nodes/[node_name]/evacuate resource.
479

480
  """
481
  POST_OPCODE = opcodes.OpNodeEvacuate
482

    
483
  def GetPostOpInput(self):
484
    """Evacuate all instances off a node.
485

486
    """
487
    return (self.request_body, {
488
      "node_name": self.items[0],
489
      "dry_run": self.dryRun(),
490
      })
491

    
492

    
493
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
494
  """/2/nodes/[node_name]/migrate resource.
495

496
  """
497
  POST_OPCODE = opcodes.OpNodeMigrate
498

    
499
  def GetPostOpInput(self):
500
    """Migrate all primary instances from a node.
501

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

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

    
517
      data = {
518
        "mode": mode,
519
        }
520
    else:
521
      data = self.request_body
522

    
523
    return (data, {
524
      "node_name": self.items[0],
525
      })
526

    
527

    
528
class R_2_nodes_name_modify(baserlib.OpcodeResource):
529
  """/2/nodes/[node_name]/modify resource.
530

531
  """
532
  POST_OPCODE = opcodes.OpNodeSetParams
533

    
534
  def GetPostOpInput(self):
535
    """Changes parameters of a node.
536

537
    """
538
    assert len(self.items) == 1
539

    
540
    return (self.request_body, {
541
      "node_name": self.items[0],
542
      })
543

    
544

    
545
class R_2_nodes_name_storage(baserlib.OpcodeResource):
546
  """/2/nodes/[node_name]/storage resource.
547

548
  """
549
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
550
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
551
  GET_OPCODE = opcodes.OpNodeQueryStorage
552

    
553
  def GetGetOpInput(self):
554
    """List storage available on a node.
555

556
    """
557
    storage_type = self._checkStringVariable("storage_type", None)
558
    output_fields = self._checkStringVariable("output_fields", None)
559

    
560
    if not output_fields:
561
      raise http.HttpBadRequest("Missing the required 'output_fields'"
562
                                " parameter")
563

    
564
    return ({}, {
565
      "nodes": [self.items[0]],
566
      "storage_type": storage_type,
567
      "output_fields": output_fields.split(","),
568
      })
569

    
570

    
571
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
572
  """/2/nodes/[node_name]/storage/modify resource.
573

574
  """
575
  PUT_OPCODE = opcodes.OpNodeModifyStorage
576

    
577
  def GetPutOpInput(self):
578
    """Modifies a storage volume on a node.
579

580
    """
581
    storage_type = self._checkStringVariable("storage_type", None)
582
    name = self._checkStringVariable("name", None)
583

    
584
    if not name:
585
      raise http.HttpBadRequest("Missing the required 'name'"
586
                                " parameter")
587

    
588
    changes = {}
589

    
590
    if "allocatable" in self.queryargs:
591
      changes[constants.SF_ALLOCATABLE] = \
592
        bool(self._checkIntVariable("allocatable", default=1))
593

    
594
    return ({}, {
595
      "node_name": self.items[0],
596
      "storage_type": storage_type,
597
      "name": name,
598
      "changes": changes,
599
      })
600

    
601

    
602
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
603
  """/2/nodes/[node_name]/storage/repair resource.
604

605
  """
606
  PUT_OPCODE = opcodes.OpRepairNodeStorage
607

    
608
  def GetPutOpInput(self):
609
    """Repairs a storage volume on a node.
610

611
    """
612
    storage_type = self._checkStringVariable("storage_type", None)
613
    name = self._checkStringVariable("name", None)
614
    if not name:
615
      raise http.HttpBadRequest("Missing the required 'name'"
616
                                " parameter")
617

    
618
    return ({}, {
619
      "node_name": self.items[0],
620
      "storage_type": storage_type,
621
      "name": name,
622
      })
623

    
624

    
625
class R_2_groups(baserlib.OpcodeResource):
626
  """/2/groups resource.
627

628
  """
629
  GET_OPCODE = opcodes.OpGroupQuery
630
  POST_OPCODE = opcodes.OpGroupAdd
631
  POST_RENAME = {
632
    "name": "group_name",
633
    }
634

    
635
  def GetPostOpInput(self):
636
    """Create a node group.
637

638
    """
639
    assert not self.items
640
    return (self.request_body, {
641
      "dry_run": self.dryRun(),
642
      })
643

    
644
  def GET(self):
645
    """Returns a list of all node groups.
646

647
    """
648
    client = self.GetClient()
649

    
650
    if self.useBulk():
651
      bulkdata = client.QueryGroups([], G_FIELDS, False)
652
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
653
    else:
654
      data = client.QueryGroups([], ["name"], False)
655
      groupnames = [row[0] for row in data]
656
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
657
                                   uri_fields=("name", "uri"))
658

    
659

    
660
class R_2_groups_name(baserlib.OpcodeResource):
661
  """/2/groups/[group_name] resource.
662

663
  """
664
  DELETE_OPCODE = opcodes.OpGroupRemove
665

    
666
  def GET(self):
667
    """Send information about a node group.
668

669
    """
670
    group_name = self.items[0]
671
    client = self.GetClient()
672

    
673
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
674
                                            names=[group_name], fields=G_FIELDS,
675
                                            use_locking=self.useLocking())
676

    
677
    return baserlib.MapFields(G_FIELDS, result[0])
678

    
679
  def GetDeleteOpInput(self):
680
    """Delete a node group.
681

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

    
689

    
690
class R_2_groups_name_modify(baserlib.OpcodeResource):
691
  """/2/groups/[group_name]/modify resource.
692

693
  """
694
  PUT_OPCODE = opcodes.OpGroupSetParams
695

    
696
  def GetPutOpInput(self):
697
    """Changes some parameters of node group.
698

699
    """
700
    assert self.items
701
    return (self.request_body, {
702
      "group_name": self.items[0],
703
      })
704

    
705

    
706
class R_2_groups_name_rename(baserlib.OpcodeResource):
707
  """/2/groups/[group_name]/rename resource.
708

709
  """
710
  PUT_OPCODE = opcodes.OpGroupRename
711

    
712
  def GetPutOpInput(self):
713
    """Changes the name of a node group.
714

715
    """
716
    assert len(self.items) == 1
717
    return (self.request_body, {
718
      "group_name": self.items[0],
719
      "dry_run": self.dryRun(),
720
      })
721

    
722

    
723
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
724
  """/2/groups/[group_name]/assign-nodes resource.
725

726
  """
727
  PUT_OPCODE = opcodes.OpGroupAssignNodes
728

    
729
  def GetPutOpInput(self):
730
    """Assigns nodes to a group.
731

732
    """
733
    assert len(self.items) == 1
734
    return (self.request_body, {
735
      "group_name": self.items[0],
736
      "dry_run": self.dryRun(),
737
      "force": self.useForce(),
738
      })
739

    
740

    
741
class R_2_instances(baserlib.OpcodeResource):
742
  """/2/instances resource.
743

744
  """
745
  GET_OPCODE = opcodes.OpInstanceQuery
746
  POST_OPCODE = opcodes.OpInstanceCreate
747
  POST_RENAME = {
748
    "os": "os_type",
749
    "name": "instance_name",
750
    }
751

    
752
  def GET(self):
753
    """Returns a list of all available instances.
754

755
    """
756
    client = self.GetClient()
757

    
758
    use_locking = self.useLocking()
759
    if self.useBulk():
760
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
761
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
762
    else:
763
      instancesdata = client.QueryInstances([], ["name"], use_locking)
764
      instanceslist = [row[0] for row in instancesdata]
765
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
766
                                   uri_fields=("id", "uri"))
767

    
768
  def GetPostOpInput(self):
769
    """Create an instance.
770

771
    @return: a job id
772

773
    """
774
    baserlib.CheckType(self.request_body, dict, "Body contents")
775

    
776
    # Default to request data version 0
777
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
778

    
779
    if data_version == 0:
780
      raise http.HttpBadRequest("Instance creation request version 0 is no"
781
                                " longer supported")
782
    elif data_version != 1:
783
      raise http.HttpBadRequest("Unsupported request data version %s" %
784
                                data_version)
785

    
786
    data = self.request_body.copy()
787
    # Remove "__version__"
788
    data.pop(_REQ_DATA_VERSION, None)
789

    
790
    return (data, {
791
      "dry_run": self.dryRun(),
792
      })
793

    
794

    
795
class R_2_instances_name(baserlib.OpcodeResource):
796
  """/2/instances/[instance_name] resource.
797

798
  """
799
  GET_OPCODE = opcodes.OpInstanceQuery
800
  DELETE_OPCODE = opcodes.OpInstanceRemove
801

    
802
  def GET(self):
803
    """Send information about an instance.
804

805
    """
806
    client = self.GetClient()
807
    instance_name = self.items[0]
808

    
809
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
810
                                            names=[instance_name],
811
                                            fields=I_FIELDS,
812
                                            use_locking=self.useLocking())
813

    
814
    return baserlib.MapFields(I_FIELDS, result[0])
815

    
816
  def GetDeleteOpInput(self):
817
    """Delete an instance.
818

819
    """
820
    assert len(self.items) == 1
821
    return ({}, {
822
      "instance_name": self.items[0],
823
      "ignore_failures": False,
824
      "dry_run": self.dryRun(),
825
      })
826

    
827

    
828
class R_2_instances_name_info(baserlib.OpcodeResource):
829
  """/2/instances/[instance_name]/info resource.
830

831
  """
832
  GET_OPCODE = opcodes.OpInstanceQueryData
833

    
834
  def GetGetOpInput(self):
835
    """Request detailed instance information.
836

837
    """
838
    assert len(self.items) == 1
839
    return ({}, {
840
      "instances": [self.items[0]],
841
      "static": bool(self._checkIntVariable("static", default=0)),
842
      })
843

    
844

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

848
  Implements an instance reboot.
849

850
  """
851
  POST_OPCODE = opcodes.OpInstanceReboot
852

    
853
  def GetPostOpInput(self):
854
    """Reboot an instance.
855

856
    The URI takes type=[hard|soft|full] and
857
    ignore_secondaries=[False|True] parameters.
858

859
    """
860
    return ({}, {
861
      "instance_name": self.items[0],
862
      "reboot_type":
863
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
864
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
865
      "dry_run": self.dryRun(),
866
      })
867

    
868

    
869
class R_2_instances_name_startup(baserlib.OpcodeResource):
870
  """/2/instances/[instance_name]/startup resource.
871

872
  Implements an instance startup.
873

874
  """
875
  PUT_OPCODE = opcodes.OpInstanceStartup
876

    
877
  def GetPutOpInput(self):
878
    """Startup an instance.
879

880
    The URI takes force=[False|True] parameter to start the instance
881
    if even if secondary disks are failing.
882

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

    
891

    
892
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
893
  """/2/instances/[instance_name]/shutdown resource.
894

895
  Implements an instance shutdown.
896

897
  """
898
  PUT_OPCODE = opcodes.OpInstanceShutdown
899

    
900
  def GetPutOpInput(self):
901
    """Shutdown an instance.
902

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

    
910

    
911
def _ParseInstanceReinstallRequest(name, data):
912
  """Parses a request for reinstalling an instance.
913

914
  """
915
  if not isinstance(data, dict):
916
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
917

    
918
  ostype = baserlib.CheckParameter(data, "os", default=None)
919
  start = baserlib.CheckParameter(data, "start", exptype=bool,
920
                                  default=True)
921
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
922

    
923
  ops = [
924
    opcodes.OpInstanceShutdown(instance_name=name),
925
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
926
                                osparams=osparams),
927
    ]
928

    
929
  if start:
930
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
931

    
932
  return ops
933

    
934

    
935
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
936
  """/2/instances/[instance_name]/reinstall resource.
937

938
  Implements an instance reinstall.
939

940
  """
941
  POST_OPCODE = opcodes.OpInstanceReinstall
942

    
943
  def POST(self):
944
    """Reinstall an instance.
945

946
    The URI takes os=name and nostartup=[0|1] optional
947
    parameters. By default, the instance will be started
948
    automatically.
949

950
    """
951
    if self.request_body:
952
      if self.queryargs:
953
        raise http.HttpBadRequest("Can't combine query and body parameters")
954

    
955
      body = self.request_body
956
    elif self.queryargs:
957
      # Legacy interface, do not modify/extend
958
      body = {
959
        "os": self._checkStringVariable("os"),
960
        "start": not self._checkIntVariable("nostartup"),
961
        }
962
    else:
963
      body = {}
964

    
965
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
966

    
967
    return self.SubmitJob(ops)
968

    
969

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

973
  """
974
  POST_OPCODE = opcodes.OpInstanceReplaceDisks
975

    
976
  def GetPostOpInput(self):
977
    """Replaces disks on an instance.
978

979
    """
980
    static = {
981
      "instance_name": self.items[0],
982
      }
983

    
984
    if self.request_body:
985
      data = self.request_body
986
    elif self.queryargs:
987
      # Legacy interface, do not modify/extend
988
      data = {
989
        "remote_node": self._checkStringVariable("remote_node", default=None),
990
        "mode": self._checkStringVariable("mode", default=None),
991
        "disks": self._checkStringVariable("disks", default=None),
992
        "iallocator": self._checkStringVariable("iallocator", default=None),
993
        }
994
    else:
995
      data = {}
996

    
997
    # Parse disks
998
    try:
999
      raw_disks = data.pop("disks")
1000
    except KeyError:
1001
      pass
1002
    else:
1003
      if raw_disks:
1004
        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1005
          data["disks"] = raw_disks
1006
        else:
1007
          # Backwards compatibility for strings of the format "1, 2, 3"
1008
          try:
1009
            data["disks"] = [int(part) for part in raw_disks.split(",")]
1010
          except (TypeError, ValueError), err:
1011
            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1012

    
1013
    return (data, static)
1014

    
1015

    
1016
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1017
  """/2/instances/[instance_name]/activate-disks resource.
1018

1019
  """
1020
  PUT_OPCODE = opcodes.OpInstanceActivateDisks
1021

    
1022
  def GetPutOpInput(self):
1023
    """Activate disks for an instance.
1024

1025
    The URI might contain ignore_size to ignore current recorded size.
1026

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

    
1033

    
1034
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1035
  """/2/instances/[instance_name]/deactivate-disks resource.
1036

1037
  """
1038
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1039

    
1040
  def GetPutOpInput(self):
1041
    """Deactivate disks for an instance.
1042

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

    
1048

    
1049
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1050
  """/2/instances/[instance_name]/recreate-disks resource.
1051

1052
  """
1053
  POST_OPCODE = opcodes.OpInstanceRecreateDisks
1054

    
1055
  def GetPostOpInput(self):
1056
    """Recreate disks for an instance.
1057

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

    
1063

    
1064
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1065
  """/2/instances/[instance_name]/prepare-export resource.
1066

1067
  """
1068
  PUT_OPCODE = opcodes.OpBackupPrepare
1069

    
1070
  def GetPutOpInput(self):
1071
    """Prepares an export for an instance.
1072

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

    
1079

    
1080
class R_2_instances_name_export(baserlib.OpcodeResource):
1081
  """/2/instances/[instance_name]/export resource.
1082

1083
  """
1084
  PUT_OPCODE = opcodes.OpBackupExport
1085
  PUT_RENAME = {
1086
    "destination": "target_node",
1087
    }
1088

    
1089
  def GetPutOpInput(self):
1090
    """Exports an instance.
1091

1092
    """
1093
    return (self.request_body, {
1094
      "instance_name": self.items[0],
1095
      })
1096

    
1097

    
1098
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1099
  """/2/instances/[instance_name]/migrate resource.
1100

1101
  """
1102
  PUT_OPCODE = opcodes.OpInstanceMigrate
1103

    
1104
  def GetPutOpInput(self):
1105
    """Migrates an instance.
1106

1107
    """
1108
    return (self.request_body, {
1109
      "instance_name": self.items[0],
1110
      })
1111

    
1112

    
1113
class R_2_instances_name_failover(baserlib.OpcodeResource):
1114
  """/2/instances/[instance_name]/failover resource.
1115

1116
  """
1117
  PUT_OPCODE = opcodes.OpInstanceFailover
1118

    
1119
  def GetPutOpInput(self):
1120
    """Does a failover of an instance.
1121

1122
    """
1123
    return (self.request_body, {
1124
      "instance_name": self.items[0],
1125
      })
1126

    
1127

    
1128
class R_2_instances_name_rename(baserlib.OpcodeResource):
1129
  """/2/instances/[instance_name]/rename resource.
1130

1131
  """
1132
  PUT_OPCODE = opcodes.OpInstanceRename
1133

    
1134
  def GetPutOpInput(self):
1135
    """Changes the name of an instance.
1136

1137
    """
1138
    return (self.request_body, {
1139
      "instance_name": self.items[0],
1140
      })
1141

    
1142

    
1143
class R_2_instances_name_modify(baserlib.OpcodeResource):
1144
  """/2/instances/[instance_name]/modify resource.
1145

1146
  """
1147
  PUT_OPCODE = opcodes.OpInstanceSetParams
1148

    
1149
  def GetPutOpInput(self):
1150
    """Changes parameters of an instance.
1151

1152
    """
1153
    return (self.request_body, {
1154
      "instance_name": self.items[0],
1155
      })
1156

    
1157

    
1158
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1159
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1160

1161
  """
1162
  POST_OPCODE = opcodes.OpInstanceGrowDisk
1163

    
1164
  def GetPostOpInput(self):
1165
    """Increases the size of an instance disk.
1166

1167
    """
1168
    return (self.request_body, {
1169
      "instance_name": self.items[0],
1170
      "disk": int(self.items[1]),
1171
      })
1172

    
1173

    
1174
class R_2_instances_name_console(baserlib.ResourceBase):
1175
  """/2/instances/[instance_name]/console resource.
1176

1177
  """
1178
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1179
  GET_OPCODE = opcodes.OpInstanceConsole
1180

    
1181
  def GET(self):
1182
    """Request information for connecting to instance's console.
1183

1184
    @return: Serialized instance console description, see
1185
             L{objects.InstanceConsole}
1186

1187
    """
1188
    client = self.GetClient()
1189

    
1190
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1191

    
1192
    if console is None:
1193
      raise http.HttpServiceUnavailable("Instance console unavailable")
1194

    
1195
    assert isinstance(console, dict)
1196
    return console
1197

    
1198

    
1199
def _GetQueryFields(args):
1200
  """
1201

1202
  """
1203
  try:
1204
    fields = args["fields"]
1205
  except KeyError:
1206
    raise http.HttpBadRequest("Missing 'fields' query argument")
1207

    
1208
  return _SplitQueryFields(fields[0])
1209

    
1210

    
1211
def _SplitQueryFields(fields):
1212
  """
1213

1214
  """
1215
  return [i.strip() for i in fields.split(",")]
1216

    
1217

    
1218
class R_2_query(baserlib.ResourceBase):
1219
  """/2/query/[resource] resource.
1220

1221
  """
1222
  # Results might contain sensitive information
1223
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1224
  GET_OPCODE = opcodes.OpQuery
1225
  PUT_OPCODE = opcodes.OpQuery
1226

    
1227
  def _Query(self, fields, qfilter):
1228
    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1229

    
1230
  def GET(self):
1231
    """Returns resource information.
1232

1233
    @return: Query result, see L{objects.QueryResponse}
1234

1235
    """
1236
    return self._Query(_GetQueryFields(self.queryargs), None)
1237

    
1238
  def PUT(self):
1239
    """Submits job querying for resources.
1240

1241
    @return: Query result, see L{objects.QueryResponse}
1242

1243
    """
1244
    body = self.request_body
1245

    
1246
    baserlib.CheckType(body, dict, "Body contents")
1247

    
1248
    try:
1249
      fields = body["fields"]
1250
    except KeyError:
1251
      fields = _GetQueryFields(self.queryargs)
1252

    
1253
    qfilter = body.get("qfilter", None)
1254
    # TODO: remove this after 2.7
1255
    if qfilter is None:
1256
      qfilter = body.get("filter", None)
1257

    
1258
    return self._Query(fields, qfilter)
1259

    
1260

    
1261
class R_2_query_fields(baserlib.ResourceBase):
1262
  """/2/query/[resource]/fields resource.
1263

1264
  """
1265
  GET_OPCODE = opcodes.OpQueryFields
1266

    
1267
  def GET(self):
1268
    """Retrieves list of available fields for a resource.
1269

1270
    @return: List of serialized L{objects.QueryFieldDefinition}
1271

1272
    """
1273
    try:
1274
      raw_fields = self.queryargs["fields"]
1275
    except KeyError:
1276
      fields = None
1277
    else:
1278
      fields = _SplitQueryFields(raw_fields[0])
1279

    
1280
    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1281

    
1282

    
1283
class _R_Tags(baserlib.OpcodeResource):
1284
  """Quasiclass for tagging resources.
1285

1286
  Manages tags. When inheriting this class you must define the
1287
  TAG_LEVEL for it.
1288

1289
  """
1290
  TAG_LEVEL = None
1291
  GET_OPCODE = opcodes.OpTagsGet
1292
  PUT_OPCODE = opcodes.OpTagsSet
1293
  DELETE_OPCODE = opcodes.OpTagsDel
1294

    
1295
  def __init__(self, items, queryargs, req, **kwargs):
1296
    """A tag resource constructor.
1297

1298
    We have to override the default to sort out cluster naming case.
1299

1300
    """
1301
    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1302

    
1303
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1304
      self.name = None
1305
    else:
1306
      self.name = items[0]
1307

    
1308
  def GET(self):
1309
    """Returns a list of tags.
1310

1311
    Example: ["tag1", "tag2", "tag3"]
1312

1313
    """
1314
    kind = self.TAG_LEVEL
1315

    
1316
    if kind in (constants.TAG_INSTANCE,
1317
                constants.TAG_NODEGROUP,
1318
                constants.TAG_NODE):
1319
      if not self.name:
1320
        raise http.HttpBadRequest("Missing name on tag request")
1321

    
1322
      cl = self.GetClient()
1323
      if kind == constants.TAG_INSTANCE:
1324
        fn = cl.QueryInstances
1325
      elif kind == constants.TAG_NODEGROUP:
1326
        fn = cl.QueryGroups
1327
      else:
1328
        fn = cl.QueryNodes
1329
      result = fn(names=[self.name], fields=["tags"], use_locking=False)
1330
      if not result or not result[0]:
1331
        raise http.HttpBadGateway("Invalid response from tag query")
1332
      tags = result[0][0]
1333

    
1334
    elif kind == constants.TAG_CLUSTER:
1335
      assert not self.name
1336
      # TODO: Use query API?
1337
      ssc = ssconf.SimpleStore()
1338
      tags = ssc.GetClusterTags()
1339

    
1340
    return list(tags)
1341

    
1342
  def GetPutOpInput(self):
1343
    """Add a set of tags.
1344

1345
    The request as a list of strings should be PUT to this URI. And
1346
    you'll have back a job id.
1347

1348
    """
1349
    return ({}, {
1350
      "kind": self.TAG_LEVEL,
1351
      "name": self.name,
1352
      "tags": self.queryargs.get("tag", []),
1353
      "dry_run": self.dryRun(),
1354
      })
1355

    
1356
  def GetDeleteOpInput(self):
1357
    """Delete a tag.
1358

1359
    In order to delete a set of tags, the DELETE
1360
    request should be addressed to URI like:
1361
    /tags?tag=[tag]&tag=[tag]
1362

1363
    """
1364
    # Re-use code
1365
    return self.GetPutOpInput()
1366

    
1367

    
1368
class R_2_instances_name_tags(_R_Tags):
1369
  """ /2/instances/[instance_name]/tags resource.
1370

1371
  Manages per-instance tags.
1372

1373
  """
1374
  TAG_LEVEL = constants.TAG_INSTANCE
1375

    
1376

    
1377
class R_2_nodes_name_tags(_R_Tags):
1378
  """ /2/nodes/[node_name]/tags resource.
1379

1380
  Manages per-node tags.
1381

1382
  """
1383
  TAG_LEVEL = constants.TAG_NODE
1384

    
1385

    
1386
class R_2_groups_name_tags(_R_Tags):
1387
  """ /2/groups/[group_name]/tags resource.
1388

1389
  Manages per-nodegroup tags.
1390

1391
  """
1392
  TAG_LEVEL = constants.TAG_NODEGROUP
1393

    
1394

    
1395
class R_2_tags(_R_Tags):
1396
  """ /2/tags resource.
1397

1398
  Manages cluster tags.
1399

1400
  """
1401
  TAG_LEVEL = constants.TAG_CLUSTER