Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 0f945c65

History | View | Annotate | Download (37.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-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.rapi import baserlib
66

    
67

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
150

    
151
class R_root(baserlib.R_Generic):
152
  """/ resource.
153

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

159
    """
160
    return None
161

    
162

    
163
class R_version(baserlib.R_Generic):
164
  """/version resource.
165

166
  This resource should be used to determine the remote API version and
167
  to adapt clients accordingly.
168

169
  """
170
  @staticmethod
171
  def GET():
172
    """Returns the remote API version.
173

174
    """
175
    return constants.RAPI_VERSION
176

    
177

    
178
class R_2_info(baserlib.R_Generic):
179
  """/2/info resource.
180

181
  """
182
  @staticmethod
183
  def GET():
184
    """Returns cluster information.
185

186
    """
187
    client = baserlib.GetClient()
188
    return client.QueryClusterInfo()
189

    
190

    
191
class R_2_features(baserlib.R_Generic):
192
  """/2/features resource.
193

194
  """
195
  @staticmethod
196
  def GET():
197
    """Returns list of optional RAPI features implemented.
198

199
    """
200
    return list(ALL_FEATURES)
201

    
202

    
203
class R_2_os(baserlib.R_Generic):
204
  """/2/os resource.
205

206
  """
207
  @staticmethod
208
  def GET():
209
    """Return a list of all OSes.
210

211
    Can return error 500 in case of a problem.
212

213
    Example: ["debian-etch"]
214

215
    """
216
    cl = baserlib.GetClient()
217
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
218
    job_id = baserlib.SubmitJob([op], cl)
219
    # we use custom feedback function, instead of print we log the status
220
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
221
    diagnose_data = result[0]
222

    
223
    if not isinstance(diagnose_data, list):
224
      raise http.HttpBadGateway(message="Can't get OS list")
225

    
226
    os_names = []
227
    for (name, variants) in diagnose_data:
228
      os_names.extend(cli.CalculateOSNames(name, variants))
229

    
230
    return os_names
231

    
232

    
233
class R_2_redist_config(baserlib.R_Generic):
234
  """/2/redistribute-config resource.
235

236
  """
237
  @staticmethod
238
  def PUT():
239
    """Redistribute configuration to all nodes.
240

241
    """
242
    return baserlib.SubmitJob([opcodes.OpClusterRedistConf()])
243

    
244

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

248
  """
249
  def PUT(self):
250
    """Modifies cluster parameters.
251

252
    @return: a job id
253

254
    """
255
    op = baserlib.FillOpcode(opcodes.OpClusterSetParams, self.request_body,
256
                             None)
257

    
258
    return baserlib.SubmitJob([op])
259

    
260

    
261
class R_2_jobs(baserlib.R_Generic):
262
  """/2/jobs resource.
263

264
  """
265
  def GET(self):
266
    """Returns a dictionary of jobs.
267

268
    @return: a dictionary with jobs id and uri.
269

270
    """
271
    client = baserlib.GetClient()
272

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

    
281

    
282
class R_2_jobs_id(baserlib.R_Generic):
283
  """/2/jobs/[job_id] resource.
284

285
  """
286
  def GET(self):
287
    """Returns a job status.
288

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

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

    
305
  def DELETE(self):
306
    """Cancel not-yet-started job.
307

308
    """
309
    job_id = self.items[0]
310
    result = baserlib.GetClient().CancelJob(job_id)
311
    return result
312

    
313

    
314
class R_2_jobs_id_wait(baserlib.R_Generic):
315
  """/2/jobs/[job_id]/wait resource.
316

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

    
322
  def GET(self):
323
    """Waits for job changes.
324

325
    """
326
    job_id = self.items[0]
327

    
328
    fields = self.getBodyParameter("fields")
329
    prev_job_info = self.getBodyParameter("previous_job_info", None)
330
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
331

    
332
    if not isinstance(fields, list):
333
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
334

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

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

    
344
    client = baserlib.GetClient()
345
    result = client.WaitForJobChangeOnce(job_id, fields,
346
                                         prev_job_info, prev_log_serial,
347
                                         timeout=_WFJC_TIMEOUT)
348
    if not result:
349
      raise http.HttpNotFound()
350

    
351
    if result == constants.JOB_NOTCHANGED:
352
      # No changes
353
      return None
354

    
355
    (job_info, log_entries) = result
356

    
357
    return {
358
      "job_info": job_info,
359
      "log_entries": log_entries,
360
      }
361

    
362

    
363
class R_2_nodes(baserlib.R_Generic):
364
  """/2/nodes resource.
365

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

370
    """
371
    client = baserlib.GetClient()
372

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

    
382

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

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

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

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

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

    
400

    
401
class R_2_nodes_name_role(baserlib.R_Generic):
402
  """ /2/nodes/[node_name]/role resource.
403

404
  """
405
  def GET(self):
406
    """Returns the current node role.
407

408
    @return: Node role
409

410
    """
411
    node_name = self.items[0]
412
    client = baserlib.GetClient()
413
    result = client.QueryNodes(names=[node_name], fields=["role"],
414
                               use_locking=self.useLocking())
415

    
416
    return _NR_MAP[result[0][0]]
417

    
418
  def PUT(self):
419
    """Sets the node role.
420

421
    @return: a job id
422

423
    """
424
    if not isinstance(self.request_body, basestring):
425
      raise http.HttpBadRequest("Invalid body contents, not a string")
426

    
427
    node_name = self.items[0]
428
    role = self.request_body
429

    
430
    if role == _NR_REGULAR:
431
      candidate = False
432
      offline = False
433
      drained = False
434

    
435
    elif role == _NR_MASTER_CANDIATE:
436
      candidate = True
437
      offline = drained = None
438

    
439
    elif role == _NR_DRAINED:
440
      drained = True
441
      candidate = offline = None
442

    
443
    elif role == _NR_OFFLINE:
444
      offline = True
445
      candidate = drained = None
446

    
447
    else:
448
      raise http.HttpBadRequest("Can't set '%s' role" % role)
449

    
450
    op = opcodes.OpNodeSetParams(node_name=node_name,
451
                                 master_candidate=candidate,
452
                                 offline=offline,
453
                                 drained=drained,
454
                                 force=bool(self.useForce()))
455

    
456
    return baserlib.SubmitJob([op])
457

    
458

    
459
class R_2_nodes_name_evacuate(baserlib.R_Generic):
460
  """/2/nodes/[node_name]/evacuate resource.
461

462
  """
463
  def POST(self):
464
    """Evacuate all instances off a node.
465

466
    """
467
    op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
468
      "node_name": self.items[0],
469
      "dry_run": self.dryRun(),
470
      })
471

    
472
    return baserlib.SubmitJob([op])
473

    
474

    
475
class R_2_nodes_name_migrate(baserlib.R_Generic):
476
  """/2/nodes/[node_name]/migrate resource.
477

478
  """
479
  def POST(self):
480
    """Migrate all primary instances from a node.
481

482
    """
483
    node_name = self.items[0]
484

    
485
    if self.queryargs:
486
      # Support old-style requests
487
      if "live" in self.queryargs and "mode" in self.queryargs:
488
        raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
489
                                  " be passed")
490

    
491
      if "live" in self.queryargs:
492
        if self._checkIntVariable("live", default=1):
493
          mode = constants.HT_MIGRATION_LIVE
494
        else:
495
          mode = constants.HT_MIGRATION_NONLIVE
496
      else:
497
        mode = self._checkStringVariable("mode", default=None)
498

    
499
      data = {
500
        "mode": mode,
501
        }
502
    else:
503
      data = self.request_body
504

    
505
    op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
506
      "node_name": node_name,
507
      })
508

    
509
    return baserlib.SubmitJob([op])
510

    
511

    
512
class R_2_nodes_name_storage(baserlib.R_Generic):
513
  """/2/nodes/[node_name]/storage resource.
514

515
  """
516
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
517
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
518

    
519
  def GET(self):
520
    node_name = self.items[0]
521

    
522
    storage_type = self._checkStringVariable("storage_type", None)
523
    if not storage_type:
524
      raise http.HttpBadRequest("Missing the required 'storage_type'"
525
                                " parameter")
526

    
527
    output_fields = self._checkStringVariable("output_fields", None)
528
    if not output_fields:
529
      raise http.HttpBadRequest("Missing the required 'output_fields'"
530
                                " parameter")
531

    
532
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
533
                                    storage_type=storage_type,
534
                                    output_fields=output_fields.split(","))
535
    return baserlib.SubmitJob([op])
536

    
537

    
538
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
539
  """/2/nodes/[node_name]/storage/modify resource.
540

541
  """
542
  def PUT(self):
543
    node_name = self.items[0]
544

    
545
    storage_type = self._checkStringVariable("storage_type", None)
546
    if not storage_type:
547
      raise http.HttpBadRequest("Missing the required 'storage_type'"
548
                                " parameter")
549

    
550
    name = self._checkStringVariable("name", None)
551
    if not name:
552
      raise http.HttpBadRequest("Missing the required 'name'"
553
                                " parameter")
554

    
555
    changes = {}
556

    
557
    if "allocatable" in self.queryargs:
558
      changes[constants.SF_ALLOCATABLE] = \
559
        bool(self._checkIntVariable("allocatable", default=1))
560

    
561
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
562
                                     storage_type=storage_type,
563
                                     name=name,
564
                                     changes=changes)
565
    return baserlib.SubmitJob([op])
566

    
567

    
568
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
569
  """/2/nodes/[node_name]/storage/repair resource.
570

571
  """
572
  def PUT(self):
573
    node_name = self.items[0]
574

    
575
    storage_type = self._checkStringVariable("storage_type", None)
576
    if not storage_type:
577
      raise http.HttpBadRequest("Missing the required 'storage_type'"
578
                                " parameter")
579

    
580
    name = self._checkStringVariable("name", None)
581
    if not name:
582
      raise http.HttpBadRequest("Missing the required 'name'"
583
                                " parameter")
584

    
585
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
586
                                     storage_type=storage_type,
587
                                     name=name)
588
    return baserlib.SubmitJob([op])
589

    
590

    
591
def _ParseCreateGroupRequest(data, dry_run):
592
  """Parses a request for creating a node group.
593

594
  @rtype: L{opcodes.OpGroupAdd}
595
  @return: Group creation opcode
596

597
  """
598
  override = {
599
    "dry_run": dry_run,
600
    }
601

    
602
  rename = {
603
    "name": "group_name",
604
    }
605

    
606
  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
607
                             rename=rename)
608

    
609

    
610
class R_2_groups(baserlib.R_Generic):
611
  """/2/groups resource.
612

613
  """
614
  def GET(self):
615
    """Returns a list of all node groups.
616

617
    """
618
    client = baserlib.GetClient()
619

    
620
    if self.useBulk():
621
      bulkdata = client.QueryGroups([], G_FIELDS, False)
622
      return baserlib.MapBulkFields(bulkdata, G_FIELDS)
623
    else:
624
      data = client.QueryGroups([], ["name"], False)
625
      groupnames = [row[0] for row in data]
626
      return baserlib.BuildUriList(groupnames, "/2/groups/%s",
627
                                   uri_fields=("name", "uri"))
628

    
629
  def POST(self):
630
    """Create a node group.
631

632
    @return: a job id
633

634
    """
635
    baserlib.CheckType(self.request_body, dict, "Body contents")
636
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
637
    return baserlib.SubmitJob([op])
638

    
639

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

643
  """
644
  def GET(self):
645
    """Send information about a node group.
646

647
    """
648
    group_name = self.items[0]
649
    client = baserlib.GetClient()
650

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

    
655
    return baserlib.MapFields(G_FIELDS, result[0])
656

    
657
  def DELETE(self):
658
    """Delete a node group.
659

660
    """
661
    op = opcodes.OpGroupRemove(group_name=self.items[0],
662
                               dry_run=bool(self.dryRun()))
663

    
664
    return baserlib.SubmitJob([op])
665

    
666

    
667
def _ParseModifyGroupRequest(name, data):
668
  """Parses a request for modifying a node group.
669

670
  @rtype: L{opcodes.OpGroupSetParams}
671
  @return: Group modify opcode
672

673
  """
674
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
675
    "group_name": name,
676
    })
677

    
678

    
679
class R_2_groups_name_modify(baserlib.R_Generic):
680
  """/2/groups/[group_name]/modify resource.
681

682
  """
683
  def PUT(self):
684
    """Changes some parameters of node group.
685

686
    @return: a job id
687

688
    """
689
    baserlib.CheckType(self.request_body, dict, "Body contents")
690

    
691
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
692

    
693
    return baserlib.SubmitJob([op])
694

    
695

    
696
def _ParseRenameGroupRequest(name, data, dry_run):
697
  """Parses a request for renaming a node group.
698

699
  @type name: string
700
  @param name: name of the node group to rename
701
  @type data: dict
702
  @param data: the body received by the rename request
703
  @type dry_run: bool
704
  @param dry_run: whether to perform a dry run
705

706
  @rtype: L{opcodes.OpGroupRename}
707
  @return: Node group rename opcode
708

709
  """
710
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
711
    "group_name": name,
712
    "dry_run": dry_run,
713
    })
714

    
715

    
716
class R_2_groups_name_rename(baserlib.R_Generic):
717
  """/2/groups/[group_name]/rename resource.
718

719
  """
720
  def PUT(self):
721
    """Changes the name of a node group.
722

723
    @return: a job id
724

725
    """
726
    baserlib.CheckType(self.request_body, dict, "Body contents")
727
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
728
                                  self.dryRun())
729
    return baserlib.SubmitJob([op])
730

    
731

    
732
class R_2_groups_name_assign_nodes(baserlib.R_Generic):
733
  """/2/groups/[group_name]/assign-nodes resource.
734

735
  """
736
  def PUT(self):
737
    """Assigns nodes to a group.
738

739
    @return: a job id
740

741
    """
742
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
743
      "group_name": self.items[0],
744
      "dry_run": self.dryRun(),
745
      "force": self.useForce(),
746
      })
747

    
748
    return baserlib.SubmitJob([op])
749

    
750

    
751
def _ParseInstanceCreateRequestVersion1(data, dry_run):
752
  """Parses an instance creation request version 1.
753

754
  @rtype: L{opcodes.OpInstanceCreate}
755
  @return: Instance creation opcode
756

757
  """
758
  override = {
759
    "dry_run": dry_run,
760
    }
761

    
762
  rename = {
763
    "os": "os_type",
764
    "name": "instance_name",
765
    }
766

    
767
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
768
                             rename=rename)
769

    
770

    
771
class R_2_instances(baserlib.R_Generic):
772
  """/2/instances resource.
773

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

778
    """
779
    client = baserlib.GetClient()
780

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

    
791
  def POST(self):
792
    """Create an instance.
793

794
    @return: a job id
795

796
    """
797
    if not isinstance(self.request_body, dict):
798
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
799

    
800
    # Default to request data version 0
801
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
802

    
803
    if data_version == 0:
804
      raise http.HttpBadRequest("Instance creation request version 0 is no"
805
                                " longer supported")
806
    elif data_version == 1:
807
      data = self.request_body.copy()
808
      # Remove "__version__"
809
      data.pop(_REQ_DATA_VERSION, None)
810
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
811
    else:
812
      raise http.HttpBadRequest("Unsupported request data version %s" %
813
                                data_version)
814

    
815
    return baserlib.SubmitJob([op])
816

    
817

    
818
class R_2_instances_name(baserlib.R_Generic):
819
  """/2/instances/[instance_name] resource.
820

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

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

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

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

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

839
    """
840
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
841
                                  ignore_failures=False,
842
                                  dry_run=bool(self.dryRun()))
843
    return baserlib.SubmitJob([op])
844

    
845

    
846
class R_2_instances_name_info(baserlib.R_Generic):
847
  """/2/instances/[instance_name]/info resource.
848

849
  """
850
  def GET(self):
851
    """Request detailed instance information.
852

853
    """
854
    instance_name = self.items[0]
855
    static = bool(self._checkIntVariable("static", default=0))
856

    
857
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
858
                                     static=static)
859
    return baserlib.SubmitJob([op])
860

    
861

    
862
class R_2_instances_name_reboot(baserlib.R_Generic):
863
  """/2/instances/[instance_name]/reboot resource.
864

865
  Implements an instance reboot.
866

867
  """
868
  def POST(self):
869
    """Reboot an instance.
870

871
    The URI takes type=[hard|soft|full] and
872
    ignore_secondaries=[False|True] parameters.
873

874
    """
875
    instance_name = self.items[0]
876
    reboot_type = self.queryargs.get("type",
877
                                     [constants.INSTANCE_REBOOT_HARD])[0]
878
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
879
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
880
                                  reboot_type=reboot_type,
881
                                  ignore_secondaries=ignore_secondaries,
882
                                  dry_run=bool(self.dryRun()))
883

    
884
    return baserlib.SubmitJob([op])
885

    
886

    
887
class R_2_instances_name_startup(baserlib.R_Generic):
888
  """/2/instances/[instance_name]/startup resource.
889

890
  Implements an instance startup.
891

892
  """
893
  def PUT(self):
894
    """Startup an instance.
895

896
    The URI takes force=[False|True] parameter to start the instance
897
    if even if secondary disks are failing.
898

899
    """
900
    instance_name = self.items[0]
901
    force_startup = bool(self._checkIntVariable("force"))
902
    no_remember = bool(self._checkIntVariable("no_remember"))
903
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
904
                                   force=force_startup,
905
                                   dry_run=bool(self.dryRun()),
906
                                   no_remember=no_remember)
907

    
908
    return baserlib.SubmitJob([op])
909

    
910

    
911
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
912
  """Parses a request for an instance shutdown.
913

914
  @rtype: L{opcodes.OpInstanceShutdown}
915
  @return: Instance shutdown opcode
916

917
  """
918
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
919
    "instance_name": name,
920
    "dry_run": dry_run,
921
    "no_remember": no_remember,
922
    })
923

    
924

    
925
class R_2_instances_name_shutdown(baserlib.R_Generic):
926
  """/2/instances/[instance_name]/shutdown resource.
927

928
  Implements an instance shutdown.
929

930
  """
931
  def PUT(self):
932
    """Shutdown an instance.
933

934
    @return: a job id
935

936
    """
937
    baserlib.CheckType(self.request_body, dict, "Body contents")
938

    
939
    no_remember = bool(self._checkIntVariable("no_remember"))
940
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
941
                                       bool(self.dryRun()), no_remember)
942

    
943
    return baserlib.SubmitJob([op])
944

    
945

    
946
def _ParseInstanceReinstallRequest(name, data):
947
  """Parses a request for reinstalling an instance.
948

949
  """
950
  if not isinstance(data, dict):
951
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
952

    
953
  ostype = baserlib.CheckParameter(data, "os", default=None)
954
  start = baserlib.CheckParameter(data, "start", exptype=bool,
955
                                  default=True)
956
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
957

    
958
  ops = [
959
    opcodes.OpInstanceShutdown(instance_name=name),
960
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
961
                                osparams=osparams),
962
    ]
963

    
964
  if start:
965
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
966

    
967
  return ops
968

    
969

    
970
class R_2_instances_name_reinstall(baserlib.R_Generic):
971
  """/2/instances/[instance_name]/reinstall resource.
972

973
  Implements an instance reinstall.
974

975
  """
976
  def POST(self):
977
    """Reinstall an instance.
978

979
    The URI takes os=name and nostartup=[0|1] optional
980
    parameters. By default, the instance will be started
981
    automatically.
982

983
    """
984
    if self.request_body:
985
      if self.queryargs:
986
        raise http.HttpBadRequest("Can't combine query and body parameters")
987

    
988
      body = self.request_body
989
    elif self.queryargs:
990
      # Legacy interface, do not modify/extend
991
      body = {
992
        "os": self._checkStringVariable("os"),
993
        "start": not self._checkIntVariable("nostartup"),
994
        }
995
    else:
996
      body = {}
997

    
998
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
999

    
1000
    return baserlib.SubmitJob(ops)
1001

    
1002

    
1003
def _ParseInstanceReplaceDisksRequest(name, data):
1004
  """Parses a request for an instance export.
1005

1006
  @rtype: L{opcodes.OpInstanceReplaceDisks}
1007
  @return: Instance export opcode
1008

1009
  """
1010
  override = {
1011
    "instance_name": name,
1012
    }
1013

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

    
1027
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1028

    
1029

    
1030
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1031
  """/2/instances/[instance_name]/replace-disks resource.
1032

1033
  """
1034
  def POST(self):
1035
    """Replaces disks on an instance.
1036

1037
    """
1038
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1039

    
1040
    return baserlib.SubmitJob([op])
1041

    
1042

    
1043
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1044
  """/2/instances/[instance_name]/activate-disks resource.
1045

1046
  """
1047
  def PUT(self):
1048
    """Activate disks for an instance.
1049

1050
    The URI might contain ignore_size to ignore current recorded size.
1051

1052
    """
1053
    instance_name = self.items[0]
1054
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1055

    
1056
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1057
                                         ignore_size=ignore_size)
1058

    
1059
    return baserlib.SubmitJob([op])
1060

    
1061

    
1062
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1063
  """/2/instances/[instance_name]/deactivate-disks resource.
1064

1065
  """
1066
  def PUT(self):
1067
    """Deactivate disks for an instance.
1068

1069
    """
1070
    instance_name = self.items[0]
1071

    
1072
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1073

    
1074
    return baserlib.SubmitJob([op])
1075

    
1076

    
1077
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1078
  """/2/instances/[instance_name]/prepare-export resource.
1079

1080
  """
1081
  def PUT(self):
1082
    """Prepares an export for an instance.
1083

1084
    @return: a job id
1085

1086
    """
1087
    instance_name = self.items[0]
1088
    mode = self._checkStringVariable("mode")
1089

    
1090
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1091
                                 mode=mode)
1092

    
1093
    return baserlib.SubmitJob([op])
1094

    
1095

    
1096
def _ParseExportInstanceRequest(name, data):
1097
  """Parses a request for an instance export.
1098

1099
  @rtype: L{opcodes.OpBackupExport}
1100
  @return: Instance export opcode
1101

1102
  """
1103
  # Rename "destination" to "target_node"
1104
  try:
1105
    data["target_node"] = data.pop("destination")
1106
  except KeyError:
1107
    pass
1108

    
1109
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1110
    "instance_name": name,
1111
    })
1112

    
1113

    
1114
class R_2_instances_name_export(baserlib.R_Generic):
1115
  """/2/instances/[instance_name]/export resource.
1116

1117
  """
1118
  def PUT(self):
1119
    """Exports an instance.
1120

1121
    @return: a job id
1122

1123
    """
1124
    if not isinstance(self.request_body, dict):
1125
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1126

    
1127
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1128

    
1129
    return baserlib.SubmitJob([op])
1130

    
1131

    
1132
def _ParseMigrateInstanceRequest(name, data):
1133
  """Parses a request for an instance migration.
1134

1135
  @rtype: L{opcodes.OpInstanceMigrate}
1136
  @return: Instance migration opcode
1137

1138
  """
1139
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1140
    "instance_name": name,
1141
    })
1142

    
1143

    
1144
class R_2_instances_name_migrate(baserlib.R_Generic):
1145
  """/2/instances/[instance_name]/migrate resource.
1146

1147
  """
1148
  def PUT(self):
1149
    """Migrates an instance.
1150

1151
    @return: a job id
1152

1153
    """
1154
    baserlib.CheckType(self.request_body, dict, "Body contents")
1155

    
1156
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1157

    
1158
    return baserlib.SubmitJob([op])
1159

    
1160

    
1161
class R_2_instances_name_failover(baserlib.R_Generic):
1162
  """/2/instances/[instance_name]/failover resource.
1163

1164
  """
1165
  def PUT(self):
1166
    """Does a failover of an instance.
1167

1168
    @return: a job id
1169

1170
    """
1171
    baserlib.CheckType(self.request_body, dict, "Body contents")
1172

    
1173
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1174
      "instance_name": self.items[0],
1175
      })
1176

    
1177
    return baserlib.SubmitJob([op])
1178

    
1179

    
1180
def _ParseRenameInstanceRequest(name, data):
1181
  """Parses a request for renaming an instance.
1182

1183
  @rtype: L{opcodes.OpInstanceRename}
1184
  @return: Instance rename opcode
1185

1186
  """
1187
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1188
    "instance_name": name,
1189
    })
1190

    
1191

    
1192
class R_2_instances_name_rename(baserlib.R_Generic):
1193
  """/2/instances/[instance_name]/rename resource.
1194

1195
  """
1196
  def PUT(self):
1197
    """Changes the name of an instance.
1198

1199
    @return: a job id
1200

1201
    """
1202
    baserlib.CheckType(self.request_body, dict, "Body contents")
1203

    
1204
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1205

    
1206
    return baserlib.SubmitJob([op])
1207

    
1208

    
1209
def _ParseModifyInstanceRequest(name, data):
1210
  """Parses a request for modifying an instance.
1211

1212
  @rtype: L{opcodes.OpInstanceSetParams}
1213
  @return: Instance modify opcode
1214

1215
  """
1216
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1217
    "instance_name": name,
1218
    })
1219

    
1220

    
1221
class R_2_instances_name_modify(baserlib.R_Generic):
1222
  """/2/instances/[instance_name]/modify resource.
1223

1224
  """
1225
  def PUT(self):
1226
    """Changes some parameters of an instance.
1227

1228
    @return: a job id
1229

1230
    """
1231
    baserlib.CheckType(self.request_body, dict, "Body contents")
1232

    
1233
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1234

    
1235
    return baserlib.SubmitJob([op])
1236

    
1237

    
1238
class R_2_instances_name_disk_grow(baserlib.R_Generic):
1239
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1240

1241
  """
1242
  def POST(self):
1243
    """Increases the size of an instance disk.
1244

1245
    @return: a job id
1246

1247
    """
1248
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1249
      "instance_name": self.items[0],
1250
      "disk": int(self.items[1]),
1251
      })
1252

    
1253
    return baserlib.SubmitJob([op])
1254

    
1255

    
1256
class R_2_instances_name_console(baserlib.R_Generic):
1257
  """/2/instances/[instance_name]/console resource.
1258

1259
  """
1260
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1261

    
1262
  def GET(self):
1263
    """Request information for connecting to instance's console.
1264

1265
    @return: Serialized instance console description, see
1266
             L{objects.InstanceConsole}
1267

1268
    """
1269
    client = baserlib.GetClient()
1270

    
1271
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1272

    
1273
    if console is None:
1274
      raise http.HttpServiceUnavailable("Instance console unavailable")
1275

    
1276
    assert isinstance(console, dict)
1277
    return console
1278

    
1279

    
1280
def _GetQueryFields(args):
1281
  """
1282

1283
  """
1284
  try:
1285
    fields = args["fields"]
1286
  except KeyError:
1287
    raise http.HttpBadRequest("Missing 'fields' query argument")
1288

    
1289
  return _SplitQueryFields(fields[0])
1290

    
1291

    
1292
def _SplitQueryFields(fields):
1293
  """
1294

1295
  """
1296
  return [i.strip() for i in fields.split(",")]
1297

    
1298

    
1299
class R_2_query(baserlib.R_Generic):
1300
  """/2/query/[resource] resource.
1301

1302
  """
1303
  # Results might contain sensitive information
1304
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1305

    
1306
  def _Query(self, fields, filter_):
1307
    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1308

    
1309
  def GET(self):
1310
    """Returns resource information.
1311

1312
    @return: Query result, see L{objects.QueryResponse}
1313

1314
    """
1315
    return self._Query(_GetQueryFields(self.queryargs), None)
1316

    
1317
  def PUT(self):
1318
    """Submits job querying for resources.
1319

1320
    @return: Query result, see L{objects.QueryResponse}
1321

1322
    """
1323
    body = self.request_body
1324

    
1325
    baserlib.CheckType(body, dict, "Body contents")
1326

    
1327
    try:
1328
      fields = body["fields"]
1329
    except KeyError:
1330
      fields = _GetQueryFields(self.queryargs)
1331

    
1332
    return self._Query(fields, self.request_body.get("filter", None))
1333

    
1334

    
1335
class R_2_query_fields(baserlib.R_Generic):
1336
  """/2/query/[resource]/fields resource.
1337

1338
  """
1339
  def GET(self):
1340
    """Retrieves list of available fields for a resource.
1341

1342
    @return: List of serialized L{objects.QueryFieldDefinition}
1343

1344
    """
1345
    try:
1346
      raw_fields = self.queryargs["fields"]
1347
    except KeyError:
1348
      fields = None
1349
    else:
1350
      fields = _SplitQueryFields(raw_fields[0])
1351

    
1352
    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1353

    
1354

    
1355
class _R_Tags(baserlib.R_Generic):
1356
  """ Quasiclass for tagging resources
1357

1358
  Manages tags. When inheriting this class you must define the
1359
  TAG_LEVEL for it.
1360

1361
  """
1362
  TAG_LEVEL = None
1363

    
1364
  def __init__(self, items, queryargs, req):
1365
    """A tag resource constructor.
1366

1367
    We have to override the default to sort out cluster naming case.
1368

1369
    """
1370
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1371

    
1372
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1373
      self.name = None
1374
    else:
1375
      self.name = items[0]
1376

    
1377
  def GET(self):
1378
    """Returns a list of tags.
1379

1380
    Example: ["tag1", "tag2", "tag3"]
1381

1382
    """
1383
    # pylint: disable-msg=W0212
1384
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1385

    
1386
  def PUT(self):
1387
    """Add a set of tags.
1388

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

1392
    """
1393
    # pylint: disable-msg=W0212
1394
    if "tag" not in self.queryargs:
1395
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1396
                                " the 'tag' parameter")
1397
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1398
                              self.queryargs["tag"], name=self.name,
1399
                              dry_run=bool(self.dryRun()))
1400

    
1401
  def DELETE(self):
1402
    """Delete a tag.
1403

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

1408
    """
1409
    # pylint: disable-msg=W0212
1410
    if "tag" not in self.queryargs:
1411
      # no we not gonna delete all tags
1412
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1413
                                " tag(s) using the 'tag' parameter")
1414
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1415
                                 self.queryargs["tag"],
1416
                                 name=self.name,
1417
                                 dry_run=bool(self.dryRun()))
1418

    
1419

    
1420
class R_2_instances_name_tags(_R_Tags):
1421
  """ /2/instances/[instance_name]/tags resource.
1422

1423
  Manages per-instance tags.
1424

1425
  """
1426
  TAG_LEVEL = constants.TAG_INSTANCE
1427

    
1428

    
1429
class R_2_nodes_name_tags(_R_Tags):
1430
  """ /2/nodes/[node_name]/tags resource.
1431

1432
  Manages per-node tags.
1433

1434
  """
1435
  TAG_LEVEL = constants.TAG_NODE
1436

    
1437

    
1438
class R_2_groups_name_tags(_R_Tags):
1439
  """ /2/groups/[group_name]/tags resource.
1440

1441
  Manages per-nodegroup tags.
1442

1443
  """
1444
  TAG_LEVEL = constants.TAG_NODEGROUP
1445

    
1446

    
1447
class R_2_tags(_R_Tags):
1448
  """ /2/tags resource.
1449

1450
  Manages cluster tags.
1451

1452
  """
1453
  TAG_LEVEL = constants.TAG_CLUSTER