Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 5d0566de

History | View | Annotate | Download (38 kB)

1
#
2
#
3

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

    
21

    
22
"""Remote API resource implementations.
23

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

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

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

35
Quoting from RFC2616, section 9.6::
36

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

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

52
"""
53

    
54
# pylint: disable=C0103
55

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

    
58
from ganeti import opcodes
59
from ganeti import http
60
from ganeti import constants
61
from ganeti import cli
62
from ganeti import rapi
63
from ganeti import ht
64
from ganeti import compat
65
from ganeti.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_version(baserlib.R_Generic):
152
  """/version resource.
153

154
  This resource should be used to determine the remote API version and
155
  to adapt clients accordingly.
156

157
  """
158
  @staticmethod
159
  def GET():
160
    """Returns the remote API version.
161

162
    """
163
    return constants.RAPI_VERSION
164

    
165

    
166
class R_2_info(baserlib.R_Generic):
167
  """/2/info resource.
168

169
  """
170
  @staticmethod
171
  def GET():
172
    """Returns cluster information.
173

174
    """
175
    client = baserlib.GetClient()
176
    return client.QueryClusterInfo()
177

    
178

    
179
class R_2_features(baserlib.R_Generic):
180
  """/2/features resource.
181

182
  """
183
  @staticmethod
184
  def GET():
185
    """Returns list of optional RAPI features implemented.
186

187
    """
188
    return list(ALL_FEATURES)
189

    
190

    
191
class R_2_os(baserlib.R_Generic):
192
  """/2/os resource.
193

194
  """
195
  @staticmethod
196
  def GET():
197
    """Return a list of all OSes.
198

199
    Can return error 500 in case of a problem.
200

201
    Example: ["debian-etch"]
202

203
    """
204
    cl = baserlib.GetClient()
205
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
206
    job_id = baserlib.SubmitJob([op], cl)
207
    # we use custom feedback function, instead of print we log the status
208
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
209
    diagnose_data = result[0]
210

    
211
    if not isinstance(diagnose_data, list):
212
      raise http.HttpBadGateway(message="Can't get OS list")
213

    
214
    os_names = []
215
    for (name, variants) in diagnose_data:
216
      os_names.extend(cli.CalculateOSNames(name, variants))
217

    
218
    return os_names
219

    
220

    
221
class R_2_redist_config(baserlib.R_Generic):
222
  """/2/redistribute-config resource.
223

224
  """
225
  @staticmethod
226
  def PUT():
227
    """Redistribute configuration to all nodes.
228

229
    """
230
    return baserlib.SubmitJob([opcodes.OpClusterRedistConf()])
231

    
232

    
233
class R_2_cluster_modify(baserlib.R_Generic):
234
  """/2/modify resource.
235

236
  """
237
  def PUT(self):
238
    """Modifies cluster parameters.
239

240
    @return: a job id
241

242
    """
243
    op = baserlib.FillOpcode(opcodes.OpClusterSetParams, self.request_body,
244
                             None)
245

    
246
    return baserlib.SubmitJob([op])
247

    
248

    
249
class R_2_jobs(baserlib.R_Generic):
250
  """/2/jobs resource.
251

252
  """
253
  def GET(self):
254
    """Returns a dictionary of jobs.
255

256
    @return: a dictionary with jobs id and uri.
257

258
    """
259
    client = baserlib.GetClient()
260

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

    
269

    
270
class R_2_jobs_id(baserlib.R_Generic):
271
  """/2/jobs/[job_id] resource.
272

273
  """
274
  def GET(self):
275
    """Returns a job status.
276

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

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

    
293
  def DELETE(self):
294
    """Cancel not-yet-started job.
295

296
    """
297
    job_id = self.items[0]
298
    result = baserlib.GetClient().CancelJob(job_id)
299
    return result
300

    
301

    
302
class R_2_jobs_id_wait(baserlib.R_Generic):
303
  """/2/jobs/[job_id]/wait resource.
304

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

    
310
  def GET(self):
311
    """Waits for job changes.
312

313
    """
314
    job_id = self.items[0]
315

    
316
    fields = self.getBodyParameter("fields")
317
    prev_job_info = self.getBodyParameter("previous_job_info", None)
318
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)
319

    
320
    if not isinstance(fields, list):
321
      raise http.HttpBadRequest("The 'fields' parameter should be a list")
322

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

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

    
332
    client = baserlib.GetClient()
333
    result = client.WaitForJobChangeOnce(job_id, fields,
334
                                         prev_job_info, prev_log_serial,
335
                                         timeout=_WFJC_TIMEOUT)
336
    if not result:
337
      raise http.HttpNotFound()
338

    
339
    if result == constants.JOB_NOTCHANGED:
340
      # No changes
341
      return None
342

    
343
    (job_info, log_entries) = result
344

    
345
    return {
346
      "job_info": job_info,
347
      "log_entries": log_entries,
348
      }
349

    
350

    
351
class R_2_nodes(baserlib.R_Generic):
352
  """/2/nodes resource.
353

354
  """
355
  def GET(self):
356
    """Returns a list of all nodes.
357

358
    """
359
    client = baserlib.GetClient()
360

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

    
370

    
371
class R_2_nodes_name(baserlib.R_Generic):
372
  """/2/nodes/[node_name] resource.
373

374
  """
375
  def GET(self):
376
    """Send information about a node.
377

378
    """
379
    node_name = self.items[0]
380
    client = baserlib.GetClient()
381

    
382
    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
383
                                            names=[node_name], fields=N_FIELDS,
384
                                            use_locking=self.useLocking())
385

    
386
    return baserlib.MapFields(N_FIELDS, result[0])
387

    
388

    
389
class R_2_nodes_name_role(baserlib.R_Generic):
390
  """ /2/nodes/[node_name]/role resource.
391

392
  """
393
  def GET(self):
394
    """Returns the current node role.
395

396
    @return: Node role
397

398
    """
399
    node_name = self.items[0]
400
    client = baserlib.GetClient()
401
    result = client.QueryNodes(names=[node_name], fields=["role"],
402
                               use_locking=self.useLocking())
403

    
404
    return _NR_MAP[result[0][0]]
405

    
406
  def PUT(self):
407
    """Sets the node role.
408

409
    @return: a job id
410

411
    """
412
    if not isinstance(self.request_body, basestring):
413
      raise http.HttpBadRequest("Invalid body contents, not a string")
414

    
415
    node_name = self.items[0]
416
    role = self.request_body
417

    
418
    auto_promote = bool(self._checkIntVariable("auto-promote"))
419

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

    
425
    elif role == _NR_MASTER_CANDIATE:
426
      candidate = True
427
      offline = drained = None
428

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

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

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

    
440
    op = opcodes.OpNodeSetParams(node_name=node_name,
441
                                 master_candidate=candidate,
442
                                 offline=offline,
443
                                 drained=drained,
444
                                 auto_promote=auto_promote,
445
                                 force=bool(self.useForce()))
446

    
447
    return baserlib.SubmitJob([op])
448

    
449

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

453
  """
454
  def POST(self):
455
    """Evacuate all instances off a node.
456

457
    """
458
    op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
459
      "node_name": self.items[0],
460
      "dry_run": self.dryRun(),
461
      })
462

    
463
    return baserlib.SubmitJob([op])
464

    
465

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

469
  """
470
  def POST(self):
471
    """Migrate all primary instances from a node.
472

473
    """
474
    node_name = self.items[0]
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
    op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
497
      "node_name": node_name,
498
      })
499

    
500
    return baserlib.SubmitJob([op])
501

    
502

    
503
class R_2_nodes_name_modify(baserlib.R_Generic):
504
  """/2/nodes/[node_name]/modify resource.
505

506
  """
507
  def POST(self):
508
    """Changes parameters of a node.
509

510
    @return: a job id
511

512
    """
513
    baserlib.CheckType(self.request_body, dict, "Body contents")
514

    
515
    op = baserlib.FillOpcode(opcodes.OpNodeSetParams, self.request_body, {
516
      "node_name": self.items[0],
517
      })
518

    
519
    return baserlib.SubmitJob([op])
520

    
521

    
522
class R_2_nodes_name_storage(baserlib.R_Generic):
523
  """/2/nodes/[node_name]/storage resource.
524

525
  """
526
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
527
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
528

    
529
  def GET(self):
530
    node_name = self.items[0]
531

    
532
    storage_type = self._checkStringVariable("storage_type", None)
533
    if not storage_type:
534
      raise http.HttpBadRequest("Missing the required 'storage_type'"
535
                                " parameter")
536

    
537
    output_fields = self._checkStringVariable("output_fields", None)
538
    if not output_fields:
539
      raise http.HttpBadRequest("Missing the required 'output_fields'"
540
                                " parameter")
541

    
542
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
543
                                    storage_type=storage_type,
544
                                    output_fields=output_fields.split(","))
545
    return baserlib.SubmitJob([op])
546

    
547

    
548
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
549
  """/2/nodes/[node_name]/storage/modify resource.
550

551
  """
552
  def PUT(self):
553
    node_name = self.items[0]
554

    
555
    storage_type = self._checkStringVariable("storage_type", None)
556
    if not storage_type:
557
      raise http.HttpBadRequest("Missing the required 'storage_type'"
558
                                " parameter")
559

    
560
    name = self._checkStringVariable("name", None)
561
    if not name:
562
      raise http.HttpBadRequest("Missing the required 'name'"
563
                                " parameter")
564

    
565
    changes = {}
566

    
567
    if "allocatable" in self.queryargs:
568
      changes[constants.SF_ALLOCATABLE] = \
569
        bool(self._checkIntVariable("allocatable", default=1))
570

    
571
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
572
                                     storage_type=storage_type,
573
                                     name=name,
574
                                     changes=changes)
575
    return baserlib.SubmitJob([op])
576

    
577

    
578
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
579
  """/2/nodes/[node_name]/storage/repair resource.
580

581
  """
582
  def PUT(self):
583
    node_name = self.items[0]
584

    
585
    storage_type = self._checkStringVariable("storage_type", None)
586
    if not storage_type:
587
      raise http.HttpBadRequest("Missing the required 'storage_type'"
588
                                " parameter")
589

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

    
595
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
596
                                     storage_type=storage_type,
597
                                     name=name)
598
    return baserlib.SubmitJob([op])
599

    
600

    
601
def _ParseCreateGroupRequest(data, dry_run):
602
  """Parses a request for creating a node group.
603

604
  @rtype: L{opcodes.OpGroupAdd}
605
  @return: Group creation opcode
606

607
  """
608
  override = {
609
    "dry_run": dry_run,
610
    }
611

    
612
  rename = {
613
    "name": "group_name",
614
    }
615

    
616
  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
617
                             rename=rename)
618

    
619

    
620
class R_2_groups(baserlib.R_Generic):
621
  """/2/groups resource.
622

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

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

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

    
639
  def POST(self):
640
    """Create a node group.
641

642
    @return: a job id
643

644
    """
645
    baserlib.CheckType(self.request_body, dict, "Body contents")
646
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
647
    return baserlib.SubmitJob([op])
648

    
649

    
650
class R_2_groups_name(baserlib.R_Generic):
651
  """/2/groups/[group_name] resource.
652

653
  """
654
  def GET(self):
655
    """Send information about a node group.
656

657
    """
658
    group_name = self.items[0]
659
    client = baserlib.GetClient()
660

    
661
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
662
                                            names=[group_name], fields=G_FIELDS,
663
                                            use_locking=self.useLocking())
664

    
665
    return baserlib.MapFields(G_FIELDS, result[0])
666

    
667
  def DELETE(self):
668
    """Delete a node group.
669

670
    """
671
    op = opcodes.OpGroupRemove(group_name=self.items[0],
672
                               dry_run=bool(self.dryRun()))
673

    
674
    return baserlib.SubmitJob([op])
675

    
676

    
677
def _ParseModifyGroupRequest(name, data):
678
  """Parses a request for modifying a node group.
679

680
  @rtype: L{opcodes.OpGroupSetParams}
681
  @return: Group modify opcode
682

683
  """
684
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
685
    "group_name": name,
686
    })
687

    
688

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

692
  """
693
  def PUT(self):
694
    """Changes some parameters of node group.
695

696
    @return: a job id
697

698
    """
699
    baserlib.CheckType(self.request_body, dict, "Body contents")
700

    
701
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
702

    
703
    return baserlib.SubmitJob([op])
704

    
705

    
706
def _ParseRenameGroupRequest(name, data, dry_run):
707
  """Parses a request for renaming a node group.
708

709
  @type name: string
710
  @param name: name of the node group to rename
711
  @type data: dict
712
  @param data: the body received by the rename request
713
  @type dry_run: bool
714
  @param dry_run: whether to perform a dry run
715

716
  @rtype: L{opcodes.OpGroupRename}
717
  @return: Node group rename opcode
718

719
  """
720
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
721
    "group_name": name,
722
    "dry_run": dry_run,
723
    })
724

    
725

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

729
  """
730
  def PUT(self):
731
    """Changes the name of a node group.
732

733
    @return: a job id
734

735
    """
736
    baserlib.CheckType(self.request_body, dict, "Body contents")
737
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
738
                                  self.dryRun())
739
    return baserlib.SubmitJob([op])
740

    
741

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

745
  """
746
  def PUT(self):
747
    """Assigns nodes to a group.
748

749
    @return: a job id
750

751
    """
752
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
753
      "group_name": self.items[0],
754
      "dry_run": self.dryRun(),
755
      "force": self.useForce(),
756
      })
757

    
758
    return baserlib.SubmitJob([op])
759

    
760

    
761
def _ParseInstanceCreateRequestVersion1(data, dry_run):
762
  """Parses an instance creation request version 1.
763

764
  @rtype: L{opcodes.OpInstanceCreate}
765
  @return: Instance creation opcode
766

767
  """
768
  override = {
769
    "dry_run": dry_run,
770
    }
771

    
772
  rename = {
773
    "os": "os_type",
774
    "name": "instance_name",
775
    }
776

    
777
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
778
                             rename=rename)
779

    
780

    
781
class R_2_instances(baserlib.R_Generic):
782
  """/2/instances resource.
783

784
  """
785
  def GET(self):
786
    """Returns a list of all available instances.
787

788
    """
789
    client = baserlib.GetClient()
790

    
791
    use_locking = self.useLocking()
792
    if self.useBulk():
793
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
794
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
795
    else:
796
      instancesdata = client.QueryInstances([], ["name"], use_locking)
797
      instanceslist = [row[0] for row in instancesdata]
798
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
799
                                   uri_fields=("id", "uri"))
800

    
801
  def POST(self):
802
    """Create an instance.
803

804
    @return: a job id
805

806
    """
807
    if not isinstance(self.request_body, dict):
808
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
809

    
810
    # Default to request data version 0
811
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
812

    
813
    if data_version == 0:
814
      raise http.HttpBadRequest("Instance creation request version 0 is no"
815
                                " longer supported")
816
    elif data_version == 1:
817
      data = self.request_body.copy()
818
      # Remove "__version__"
819
      data.pop(_REQ_DATA_VERSION, None)
820
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
821
    else:
822
      raise http.HttpBadRequest("Unsupported request data version %s" %
823
                                data_version)
824

    
825
    return baserlib.SubmitJob([op])
826

    
827

    
828
class R_2_instances_name(baserlib.R_Generic):
829
  """/2/instances/[instance_name] resource.
830

831
  """
832
  def GET(self):
833
    """Send information about an instance.
834

835
    """
836
    client = baserlib.GetClient()
837
    instance_name = self.items[0]
838

    
839
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
840
                                            names=[instance_name],
841
                                            fields=I_FIELDS,
842
                                            use_locking=self.useLocking())
843

    
844
    return baserlib.MapFields(I_FIELDS, result[0])
845

    
846
  def DELETE(self):
847
    """Delete an instance.
848

849
    """
850
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
851
                                  ignore_failures=False,
852
                                  dry_run=bool(self.dryRun()))
853
    return baserlib.SubmitJob([op])
854

    
855

    
856
class R_2_instances_name_info(baserlib.R_Generic):
857
  """/2/instances/[instance_name]/info resource.
858

859
  """
860
  def GET(self):
861
    """Request detailed instance information.
862

863
    """
864
    instance_name = self.items[0]
865
    static = bool(self._checkIntVariable("static", default=0))
866

    
867
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
868
                                     static=static)
869
    return baserlib.SubmitJob([op])
870

    
871

    
872
class R_2_instances_name_reboot(baserlib.R_Generic):
873
  """/2/instances/[instance_name]/reboot resource.
874

875
  Implements an instance reboot.
876

877
  """
878
  def POST(self):
879
    """Reboot an instance.
880

881
    The URI takes type=[hard|soft|full] and
882
    ignore_secondaries=[False|True] parameters.
883

884
    """
885
    instance_name = self.items[0]
886
    reboot_type = self.queryargs.get("type",
887
                                     [constants.INSTANCE_REBOOT_HARD])[0]
888
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
889
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
890
                                  reboot_type=reboot_type,
891
                                  ignore_secondaries=ignore_secondaries,
892
                                  dry_run=bool(self.dryRun()))
893

    
894
    return baserlib.SubmitJob([op])
895

    
896

    
897
class R_2_instances_name_startup(baserlib.R_Generic):
898
  """/2/instances/[instance_name]/startup resource.
899

900
  Implements an instance startup.
901

902
  """
903
  def PUT(self):
904
    """Startup an instance.
905

906
    The URI takes force=[False|True] parameter to start the instance
907
    if even if secondary disks are failing.
908

909
    """
910
    instance_name = self.items[0]
911
    force_startup = bool(self._checkIntVariable("force"))
912
    no_remember = bool(self._checkIntVariable("no_remember"))
913
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
914
                                   force=force_startup,
915
                                   dry_run=bool(self.dryRun()),
916
                                   no_remember=no_remember)
917

    
918
    return baserlib.SubmitJob([op])
919

    
920

    
921
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
922
  """Parses a request for an instance shutdown.
923

924
  @rtype: L{opcodes.OpInstanceShutdown}
925
  @return: Instance shutdown opcode
926

927
  """
928
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
929
    "instance_name": name,
930
    "dry_run": dry_run,
931
    "no_remember": no_remember,
932
    })
933

    
934

    
935
class R_2_instances_name_shutdown(baserlib.R_Generic):
936
  """/2/instances/[instance_name]/shutdown resource.
937

938
  Implements an instance shutdown.
939

940
  """
941
  def PUT(self):
942
    """Shutdown an instance.
943

944
    @return: a job id
945

946
    """
947
    no_remember = bool(self._checkIntVariable("no_remember"))
948
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
949
                                       bool(self.dryRun()), no_remember)
950

    
951
    return baserlib.SubmitJob([op])
952

    
953

    
954
def _ParseInstanceReinstallRequest(name, data):
955
  """Parses a request for reinstalling an instance.
956

957
  """
958
  if not isinstance(data, dict):
959
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
960

    
961
  ostype = baserlib.CheckParameter(data, "os", default=None)
962
  start = baserlib.CheckParameter(data, "start", exptype=bool,
963
                                  default=True)
964
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
965

    
966
  ops = [
967
    opcodes.OpInstanceShutdown(instance_name=name),
968
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
969
                                osparams=osparams),
970
    ]
971

    
972
  if start:
973
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
974

    
975
  return ops
976

    
977

    
978
class R_2_instances_name_reinstall(baserlib.R_Generic):
979
  """/2/instances/[instance_name]/reinstall resource.
980

981
  Implements an instance reinstall.
982

983
  """
984
  def POST(self):
985
    """Reinstall an instance.
986

987
    The URI takes os=name and nostartup=[0|1] optional
988
    parameters. By default, the instance will be started
989
    automatically.
990

991
    """
992
    if self.request_body:
993
      if self.queryargs:
994
        raise http.HttpBadRequest("Can't combine query and body parameters")
995

    
996
      body = self.request_body
997
    elif self.queryargs:
998
      # Legacy interface, do not modify/extend
999
      body = {
1000
        "os": self._checkStringVariable("os"),
1001
        "start": not self._checkIntVariable("nostartup"),
1002
        }
1003
    else:
1004
      body = {}
1005

    
1006
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1007

    
1008
    return baserlib.SubmitJob(ops)
1009

    
1010

    
1011
def _ParseInstanceReplaceDisksRequest(name, data):
1012
  """Parses a request for an instance export.
1013

1014
  @rtype: L{opcodes.OpInstanceReplaceDisks}
1015
  @return: Instance export opcode
1016

1017
  """
1018
  override = {
1019
    "instance_name": name,
1020
    }
1021

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

    
1038
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1039

    
1040

    
1041
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1042
  """/2/instances/[instance_name]/replace-disks resource.
1043

1044
  """
1045
  def POST(self):
1046
    """Replaces disks on an instance.
1047

1048
    """
1049
    if self.request_body:
1050
      body = self.request_body
1051
    elif self.queryargs:
1052
      # Legacy interface, do not modify/extend
1053
      body = {
1054
        "remote_node": self._checkStringVariable("remote_node", default=None),
1055
        "mode": self._checkStringVariable("mode", default=None),
1056
        "disks": self._checkStringVariable("disks", default=None),
1057
        "iallocator": self._checkStringVariable("iallocator", default=None),
1058
        }
1059
    else:
1060
      body = {}
1061

    
1062
    op = _ParseInstanceReplaceDisksRequest(self.items[0], body)
1063

    
1064
    return baserlib.SubmitJob([op])
1065

    
1066

    
1067
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1068
  """/2/instances/[instance_name]/activate-disks resource.
1069

1070
  """
1071
  def PUT(self):
1072
    """Activate disks for an instance.
1073

1074
    The URI might contain ignore_size to ignore current recorded size.
1075

1076
    """
1077
    instance_name = self.items[0]
1078
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1079

    
1080
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1081
                                         ignore_size=ignore_size)
1082

    
1083
    return baserlib.SubmitJob([op])
1084

    
1085

    
1086
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1087
  """/2/instances/[instance_name]/deactivate-disks resource.
1088

1089
  """
1090
  def PUT(self):
1091
    """Deactivate disks for an instance.
1092

1093
    """
1094
    instance_name = self.items[0]
1095

    
1096
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1097

    
1098
    return baserlib.SubmitJob([op])
1099

    
1100

    
1101
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1102
  """/2/instances/[instance_name]/prepare-export resource.
1103

1104
  """
1105
  def PUT(self):
1106
    """Prepares an export for an instance.
1107

1108
    @return: a job id
1109

1110
    """
1111
    instance_name = self.items[0]
1112
    mode = self._checkStringVariable("mode")
1113

    
1114
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1115
                                 mode=mode)
1116

    
1117
    return baserlib.SubmitJob([op])
1118

    
1119

    
1120
def _ParseExportInstanceRequest(name, data):
1121
  """Parses a request for an instance export.
1122

1123
  @rtype: L{opcodes.OpBackupExport}
1124
  @return: Instance export opcode
1125

1126
  """
1127
  # Rename "destination" to "target_node"
1128
  try:
1129
    data["target_node"] = data.pop("destination")
1130
  except KeyError:
1131
    pass
1132

    
1133
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1134
    "instance_name": name,
1135
    })
1136

    
1137

    
1138
class R_2_instances_name_export(baserlib.R_Generic):
1139
  """/2/instances/[instance_name]/export resource.
1140

1141
  """
1142
  def PUT(self):
1143
    """Exports an instance.
1144

1145
    @return: a job id
1146

1147
    """
1148
    if not isinstance(self.request_body, dict):
1149
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1150

    
1151
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1152

    
1153
    return baserlib.SubmitJob([op])
1154

    
1155

    
1156
def _ParseMigrateInstanceRequest(name, data):
1157
  """Parses a request for an instance migration.
1158

1159
  @rtype: L{opcodes.OpInstanceMigrate}
1160
  @return: Instance migration opcode
1161

1162
  """
1163
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1164
    "instance_name": name,
1165
    })
1166

    
1167

    
1168
class R_2_instances_name_migrate(baserlib.R_Generic):
1169
  """/2/instances/[instance_name]/migrate resource.
1170

1171
  """
1172
  def PUT(self):
1173
    """Migrates an instance.
1174

1175
    @return: a job id
1176

1177
    """
1178
    baserlib.CheckType(self.request_body, dict, "Body contents")
1179

    
1180
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1181

    
1182
    return baserlib.SubmitJob([op])
1183

    
1184

    
1185
class R_2_instances_name_failover(baserlib.R_Generic):
1186
  """/2/instances/[instance_name]/failover resource.
1187

1188
  """
1189
  def PUT(self):
1190
    """Does a failover of an instance.
1191

1192
    @return: a job id
1193

1194
    """
1195
    baserlib.CheckType(self.request_body, dict, "Body contents")
1196

    
1197
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1198
      "instance_name": self.items[0],
1199
      })
1200

    
1201
    return baserlib.SubmitJob([op])
1202

    
1203

    
1204
def _ParseRenameInstanceRequest(name, data):
1205
  """Parses a request for renaming an instance.
1206

1207
  @rtype: L{opcodes.OpInstanceRename}
1208
  @return: Instance rename opcode
1209

1210
  """
1211
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1212
    "instance_name": name,
1213
    })
1214

    
1215

    
1216
class R_2_instances_name_rename(baserlib.R_Generic):
1217
  """/2/instances/[instance_name]/rename resource.
1218

1219
  """
1220
  def PUT(self):
1221
    """Changes the name of an instance.
1222

1223
    @return: a job id
1224

1225
    """
1226
    baserlib.CheckType(self.request_body, dict, "Body contents")
1227

    
1228
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1229

    
1230
    return baserlib.SubmitJob([op])
1231

    
1232

    
1233
def _ParseModifyInstanceRequest(name, data):
1234
  """Parses a request for modifying an instance.
1235

1236
  @rtype: L{opcodes.OpInstanceSetParams}
1237
  @return: Instance modify opcode
1238

1239
  """
1240
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1241
    "instance_name": name,
1242
    })
1243

    
1244

    
1245
class R_2_instances_name_modify(baserlib.R_Generic):
1246
  """/2/instances/[instance_name]/modify resource.
1247

1248
  """
1249
  def PUT(self):
1250
    """Changes some parameters of an instance.
1251

1252
    @return: a job id
1253

1254
    """
1255
    baserlib.CheckType(self.request_body, dict, "Body contents")
1256

    
1257
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1258

    
1259
    return baserlib.SubmitJob([op])
1260

    
1261

    
1262
class R_2_instances_name_disk_grow(baserlib.R_Generic):
1263
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1264

1265
  """
1266
  def POST(self):
1267
    """Increases the size of an instance disk.
1268

1269
    @return: a job id
1270

1271
    """
1272
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1273
      "instance_name": self.items[0],
1274
      "disk": int(self.items[1]),
1275
      })
1276

    
1277
    return baserlib.SubmitJob([op])
1278

    
1279

    
1280
class R_2_instances_name_console(baserlib.R_Generic):
1281
  """/2/instances/[instance_name]/console resource.
1282

1283
  """
1284
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1285

    
1286
  def GET(self):
1287
    """Request information for connecting to instance's console.
1288

1289
    @return: Serialized instance console description, see
1290
             L{objects.InstanceConsole}
1291

1292
    """
1293
    client = baserlib.GetClient()
1294

    
1295
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1296

    
1297
    if console is None:
1298
      raise http.HttpServiceUnavailable("Instance console unavailable")
1299

    
1300
    assert isinstance(console, dict)
1301
    return console
1302

    
1303

    
1304
def _GetQueryFields(args):
1305
  """
1306

1307
  """
1308
  try:
1309
    fields = args["fields"]
1310
  except KeyError:
1311
    raise http.HttpBadRequest("Missing 'fields' query argument")
1312

    
1313
  return _SplitQueryFields(fields[0])
1314

    
1315

    
1316
def _SplitQueryFields(fields):
1317
  """
1318

1319
  """
1320
  return [i.strip() for i in fields.split(",")]
1321

    
1322

    
1323
class R_2_query(baserlib.R_Generic):
1324
  """/2/query/[resource] resource.
1325

1326
  """
1327
  # Results might contain sensitive information
1328
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1329

    
1330
  def _Query(self, fields, filter_):
1331
    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1332

    
1333
  def GET(self):
1334
    """Returns resource information.
1335

1336
    @return: Query result, see L{objects.QueryResponse}
1337

1338
    """
1339
    return self._Query(_GetQueryFields(self.queryargs), None)
1340

    
1341
  def PUT(self):
1342
    """Submits job querying for resources.
1343

1344
    @return: Query result, see L{objects.QueryResponse}
1345

1346
    """
1347
    body = self.request_body
1348

    
1349
    baserlib.CheckType(body, dict, "Body contents")
1350

    
1351
    try:
1352
      fields = body["fields"]
1353
    except KeyError:
1354
      fields = _GetQueryFields(self.queryargs)
1355

    
1356
    return self._Query(fields, self.request_body.get("filter", None))
1357

    
1358

    
1359
class R_2_query_fields(baserlib.R_Generic):
1360
  """/2/query/[resource]/fields resource.
1361

1362
  """
1363
  def GET(self):
1364
    """Retrieves list of available fields for a resource.
1365

1366
    @return: List of serialized L{objects.QueryFieldDefinition}
1367

1368
    """
1369
    try:
1370
      raw_fields = self.queryargs["fields"]
1371
    except KeyError:
1372
      fields = None
1373
    else:
1374
      fields = _SplitQueryFields(raw_fields[0])
1375

    
1376
    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1377

    
1378

    
1379
class _R_Tags(baserlib.R_Generic):
1380
  """ Quasiclass for tagging resources
1381

1382
  Manages tags. When inheriting this class you must define the
1383
  TAG_LEVEL for it.
1384

1385
  """
1386
  TAG_LEVEL = None
1387

    
1388
  def __init__(self, items, queryargs, req):
1389
    """A tag resource constructor.
1390

1391
    We have to override the default to sort out cluster naming case.
1392

1393
    """
1394
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1395

    
1396
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1397
      self.name = None
1398
    else:
1399
      self.name = items[0]
1400

    
1401
  def GET(self):
1402
    """Returns a list of tags.
1403

1404
    Example: ["tag1", "tag2", "tag3"]
1405

1406
    """
1407
    # pylint: disable=W0212
1408
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1409

    
1410
  def PUT(self):
1411
    """Add a set of tags.
1412

1413
    The request as a list of strings should be PUT to this URI. And
1414
    you'll have back a job id.
1415

1416
    """
1417
    # pylint: disable=W0212
1418
    if "tag" not in self.queryargs:
1419
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1420
                                " the 'tag' parameter")
1421
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1422
                              self.queryargs["tag"], name=self.name,
1423
                              dry_run=bool(self.dryRun()))
1424

    
1425
  def DELETE(self):
1426
    """Delete a tag.
1427

1428
    In order to delete a set of tags, the DELETE
1429
    request should be addressed to URI like:
1430
    /tags?tag=[tag]&tag=[tag]
1431

1432
    """
1433
    # pylint: disable=W0212
1434
    if "tag" not in self.queryargs:
1435
      # no we not gonna delete all tags
1436
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1437
                                " tag(s) using the 'tag' parameter")
1438
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1439
                                 self.queryargs["tag"],
1440
                                 name=self.name,
1441
                                 dry_run=bool(self.dryRun()))
1442

    
1443

    
1444
class R_2_instances_name_tags(_R_Tags):
1445
  """ /2/instances/[instance_name]/tags resource.
1446

1447
  Manages per-instance tags.
1448

1449
  """
1450
  TAG_LEVEL = constants.TAG_INSTANCE
1451

    
1452

    
1453
class R_2_nodes_name_tags(_R_Tags):
1454
  """ /2/nodes/[node_name]/tags resource.
1455

1456
  Manages per-node tags.
1457

1458
  """
1459
  TAG_LEVEL = constants.TAG_NODE
1460

    
1461

    
1462
class R_2_groups_name_tags(_R_Tags):
1463
  """ /2/groups/[group_name]/tags resource.
1464

1465
  Manages per-nodegroup tags.
1466

1467
  """
1468
  TAG_LEVEL = constants.TAG_NODEGROUP
1469

    
1470

    
1471
class R_2_tags(_R_Tags):
1472
  """ /2/tags resource.
1473

1474
  Manages cluster tags.
1475

1476
  """
1477
  TAG_LEVEL = constants.TAG_CLUSTER