Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 94497dd1

History | View | Annotate | Download (37.3 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
    if role == _NR_REGULAR:
419
      candidate = False
420
      offline = False
421
      drained = False
422

    
423
    elif role == _NR_MASTER_CANDIATE:
424
      candidate = True
425
      offline = drained = None
426

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

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

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

    
438
    op = opcodes.OpNodeSetParams(node_name=node_name,
439
                                 master_candidate=candidate,
440
                                 offline=offline,
441
                                 drained=drained,
442
                                 force=bool(self.useForce()))
443

    
444
    return baserlib.SubmitJob([op])
445

    
446

    
447
class R_2_nodes_name_evacuate(baserlib.R_Generic):
448
  """/2/nodes/[node_name]/evacuate resource.
449

450
  """
451
  def POST(self):
452
    """Evacuate all instances off a node.
453

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

    
460
    return baserlib.SubmitJob([op])
461

    
462

    
463
class R_2_nodes_name_migrate(baserlib.R_Generic):
464
  """/2/nodes/[node_name]/migrate resource.
465

466
  """
467
  def POST(self):
468
    """Migrate all primary instances from a node.
469

470
    """
471
    node_name = self.items[0]
472

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

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

    
487
      data = {
488
        "mode": mode,
489
        }
490
    else:
491
      data = self.request_body
492

    
493
    op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
494
      "node_name": node_name,
495
      })
496

    
497
    return baserlib.SubmitJob([op])
498

    
499

    
500
class R_2_nodes_name_modify(baserlib.R_Generic):
501
  """/2/nodes/[node_name]/modify resource.
502

503
  """
504
  def POST(self):
505
    """Changes parameters of a node.
506

507
    @return: a job id
508

509
    """
510
    baserlib.CheckType(self.request_body, dict, "Body contents")
511

    
512
    op = baserlib.FillOpcode(opcodes.OpNodeSetParams, self.request_body, {
513
      "instance_name": self.items[0],
514
      })
515

    
516
    return baserlib.SubmitJob([op])
517

    
518

    
519
class R_2_nodes_name_storage(baserlib.R_Generic):
520
  """/2/nodes/[node_name]/storage resource.
521

522
  """
523
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
524
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
525

    
526
  def GET(self):
527
    node_name = self.items[0]
528

    
529
    storage_type = self._checkStringVariable("storage_type", None)
530
    if not storage_type:
531
      raise http.HttpBadRequest("Missing the required 'storage_type'"
532
                                " parameter")
533

    
534
    output_fields = self._checkStringVariable("output_fields", None)
535
    if not output_fields:
536
      raise http.HttpBadRequest("Missing the required 'output_fields'"
537
                                " parameter")
538

    
539
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
540
                                    storage_type=storage_type,
541
                                    output_fields=output_fields.split(","))
542
    return baserlib.SubmitJob([op])
543

    
544

    
545
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
546
  """/2/nodes/[node_name]/storage/modify resource.
547

548
  """
549
  def PUT(self):
550
    node_name = self.items[0]
551

    
552
    storage_type = self._checkStringVariable("storage_type", None)
553
    if not storage_type:
554
      raise http.HttpBadRequest("Missing the required 'storage_type'"
555
                                " parameter")
556

    
557
    name = self._checkStringVariable("name", None)
558
    if not name:
559
      raise http.HttpBadRequest("Missing the required 'name'"
560
                                " parameter")
561

    
562
    changes = {}
563

    
564
    if "allocatable" in self.queryargs:
565
      changes[constants.SF_ALLOCATABLE] = \
566
        bool(self._checkIntVariable("allocatable", default=1))
567

    
568
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
569
                                     storage_type=storage_type,
570
                                     name=name,
571
                                     changes=changes)
572
    return baserlib.SubmitJob([op])
573

    
574

    
575
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
576
  """/2/nodes/[node_name]/storage/repair resource.
577

578
  """
579
  def PUT(self):
580
    node_name = self.items[0]
581

    
582
    storage_type = self._checkStringVariable("storage_type", None)
583
    if not storage_type:
584
      raise http.HttpBadRequest("Missing the required 'storage_type'"
585
                                " parameter")
586

    
587
    name = self._checkStringVariable("name", None)
588
    if not name:
589
      raise http.HttpBadRequest("Missing the required 'name'"
590
                                " parameter")
591

    
592
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
593
                                     storage_type=storage_type,
594
                                     name=name)
595
    return baserlib.SubmitJob([op])
596

    
597

    
598
def _ParseCreateGroupRequest(data, dry_run):
599
  """Parses a request for creating a node group.
600

601
  @rtype: L{opcodes.OpGroupAdd}
602
  @return: Group creation opcode
603

604
  """
605
  override = {
606
    "dry_run": dry_run,
607
    }
608

    
609
  rename = {
610
    "name": "group_name",
611
    }
612

    
613
  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
614
                             rename=rename)
615

    
616

    
617
class R_2_groups(baserlib.R_Generic):
618
  """/2/groups resource.
619

620
  """
621
  def GET(self):
622
    """Returns a list of all node groups.
623

624
    """
625
    client = baserlib.GetClient()
626

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

    
636
  def POST(self):
637
    """Create a node group.
638

639
    @return: a job id
640

641
    """
642
    baserlib.CheckType(self.request_body, dict, "Body contents")
643
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
644
    return baserlib.SubmitJob([op])
645

    
646

    
647
class R_2_groups_name(baserlib.R_Generic):
648
  """/2/groups/[group_name] resource.
649

650
  """
651
  def GET(self):
652
    """Send information about a node group.
653

654
    """
655
    group_name = self.items[0]
656
    client = baserlib.GetClient()
657

    
658
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
659
                                            names=[group_name], fields=G_FIELDS,
660
                                            use_locking=self.useLocking())
661

    
662
    return baserlib.MapFields(G_FIELDS, result[0])
663

    
664
  def DELETE(self):
665
    """Delete a node group.
666

667
    """
668
    op = opcodes.OpGroupRemove(group_name=self.items[0],
669
                               dry_run=bool(self.dryRun()))
670

    
671
    return baserlib.SubmitJob([op])
672

    
673

    
674
def _ParseModifyGroupRequest(name, data):
675
  """Parses a request for modifying a node group.
676

677
  @rtype: L{opcodes.OpGroupSetParams}
678
  @return: Group modify opcode
679

680
  """
681
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
682
    "group_name": name,
683
    })
684

    
685

    
686
class R_2_groups_name_modify(baserlib.R_Generic):
687
  """/2/groups/[group_name]/modify resource.
688

689
  """
690
  def PUT(self):
691
    """Changes some parameters of node group.
692

693
    @return: a job id
694

695
    """
696
    baserlib.CheckType(self.request_body, dict, "Body contents")
697

    
698
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
699

    
700
    return baserlib.SubmitJob([op])
701

    
702

    
703
def _ParseRenameGroupRequest(name, data, dry_run):
704
  """Parses a request for renaming a node group.
705

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

713
  @rtype: L{opcodes.OpGroupRename}
714
  @return: Node group rename opcode
715

716
  """
717
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
718
    "group_name": name,
719
    "dry_run": dry_run,
720
    })
721

    
722

    
723
class R_2_groups_name_rename(baserlib.R_Generic):
724
  """/2/groups/[group_name]/rename resource.
725

726
  """
727
  def PUT(self):
728
    """Changes the name of a node group.
729

730
    @return: a job id
731

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

    
738

    
739
class R_2_groups_name_assign_nodes(baserlib.R_Generic):
740
  """/2/groups/[group_name]/assign-nodes resource.
741

742
  """
743
  def PUT(self):
744
    """Assigns nodes to a group.
745

746
    @return: a job id
747

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

    
755
    return baserlib.SubmitJob([op])
756

    
757

    
758
def _ParseInstanceCreateRequestVersion1(data, dry_run):
759
  """Parses an instance creation request version 1.
760

761
  @rtype: L{opcodes.OpInstanceCreate}
762
  @return: Instance creation opcode
763

764
  """
765
  override = {
766
    "dry_run": dry_run,
767
    }
768

    
769
  rename = {
770
    "os": "os_type",
771
    "name": "instance_name",
772
    }
773

    
774
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
775
                             rename=rename)
776

    
777

    
778
class R_2_instances(baserlib.R_Generic):
779
  """/2/instances resource.
780

781
  """
782
  def GET(self):
783
    """Returns a list of all available instances.
784

785
    """
786
    client = baserlib.GetClient()
787

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

    
798
  def POST(self):
799
    """Create an instance.
800

801
    @return: a job id
802

803
    """
804
    if not isinstance(self.request_body, dict):
805
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
806

    
807
    # Default to request data version 0
808
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
809

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

    
822
    return baserlib.SubmitJob([op])
823

    
824

    
825
class R_2_instances_name(baserlib.R_Generic):
826
  """/2/instances/[instance_name] resource.
827

828
  """
829
  def GET(self):
830
    """Send information about an instance.
831

832
    """
833
    client = baserlib.GetClient()
834
    instance_name = self.items[0]
835

    
836
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
837
                                            names=[instance_name],
838
                                            fields=I_FIELDS,
839
                                            use_locking=self.useLocking())
840

    
841
    return baserlib.MapFields(I_FIELDS, result[0])
842

    
843
  def DELETE(self):
844
    """Delete an instance.
845

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

    
852

    
853
class R_2_instances_name_info(baserlib.R_Generic):
854
  """/2/instances/[instance_name]/info resource.
855

856
  """
857
  def GET(self):
858
    """Request detailed instance information.
859

860
    """
861
    instance_name = self.items[0]
862
    static = bool(self._checkIntVariable("static", default=0))
863

    
864
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
865
                                     static=static)
866
    return baserlib.SubmitJob([op])
867

    
868

    
869
class R_2_instances_name_reboot(baserlib.R_Generic):
870
  """/2/instances/[instance_name]/reboot resource.
871

872
  Implements an instance reboot.
873

874
  """
875
  def POST(self):
876
    """Reboot an instance.
877

878
    The URI takes type=[hard|soft|full] and
879
    ignore_secondaries=[False|True] parameters.
880

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

    
891
    return baserlib.SubmitJob([op])
892

    
893

    
894
class R_2_instances_name_startup(baserlib.R_Generic):
895
  """/2/instances/[instance_name]/startup resource.
896

897
  Implements an instance startup.
898

899
  """
900
  def PUT(self):
901
    """Startup an instance.
902

903
    The URI takes force=[False|True] parameter to start the instance
904
    if even if secondary disks are failing.
905

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

    
915
    return baserlib.SubmitJob([op])
916

    
917

    
918
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
919
  """Parses a request for an instance shutdown.
920

921
  @rtype: L{opcodes.OpInstanceShutdown}
922
  @return: Instance shutdown opcode
923

924
  """
925
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
926
    "instance_name": name,
927
    "dry_run": dry_run,
928
    "no_remember": no_remember,
929
    })
930

    
931

    
932
class R_2_instances_name_shutdown(baserlib.R_Generic):
933
  """/2/instances/[instance_name]/shutdown resource.
934

935
  Implements an instance shutdown.
936

937
  """
938
  def PUT(self):
939
    """Shutdown an instance.
940

941
    @return: a job id
942

943
    """
944
    no_remember = bool(self._checkIntVariable("no_remember"))
945
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
946
                                       bool(self.dryRun()), no_remember)
947

    
948
    return baserlib.SubmitJob([op])
949

    
950

    
951
def _ParseInstanceReinstallRequest(name, data):
952
  """Parses a request for reinstalling an instance.
953

954
  """
955
  if not isinstance(data, dict):
956
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
957

    
958
  ostype = baserlib.CheckParameter(data, "os", default=None)
959
  start = baserlib.CheckParameter(data, "start", exptype=bool,
960
                                  default=True)
961
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
962

    
963
  ops = [
964
    opcodes.OpInstanceShutdown(instance_name=name),
965
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
966
                                osparams=osparams),
967
    ]
968

    
969
  if start:
970
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
971

    
972
  return ops
973

    
974

    
975
class R_2_instances_name_reinstall(baserlib.R_Generic):
976
  """/2/instances/[instance_name]/reinstall resource.
977

978
  Implements an instance reinstall.
979

980
  """
981
  def POST(self):
982
    """Reinstall an instance.
983

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

988
    """
989
    if self.request_body:
990
      if self.queryargs:
991
        raise http.HttpBadRequest("Can't combine query and body parameters")
992

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

    
1003
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
1004

    
1005
    return baserlib.SubmitJob(ops)
1006

    
1007

    
1008
def _ParseInstanceReplaceDisksRequest(name, data):
1009
  """Parses a request for an instance export.
1010

1011
  @rtype: L{opcodes.OpInstanceReplaceDisks}
1012
  @return: Instance export opcode
1013

1014
  """
1015
  override = {
1016
    "instance_name": name,
1017
    }
1018

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

    
1032
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1033

    
1034

    
1035
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1036
  """/2/instances/[instance_name]/replace-disks resource.
1037

1038
  """
1039
  def POST(self):
1040
    """Replaces disks on an instance.
1041

1042
    """
1043
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1044

    
1045
    return baserlib.SubmitJob([op])
1046

    
1047

    
1048
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1049
  """/2/instances/[instance_name]/activate-disks resource.
1050

1051
  """
1052
  def PUT(self):
1053
    """Activate disks for an instance.
1054

1055
    The URI might contain ignore_size to ignore current recorded size.
1056

1057
    """
1058
    instance_name = self.items[0]
1059
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1060

    
1061
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1062
                                         ignore_size=ignore_size)
1063

    
1064
    return baserlib.SubmitJob([op])
1065

    
1066

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

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

1074
    """
1075
    instance_name = self.items[0]
1076

    
1077
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1078

    
1079
    return baserlib.SubmitJob([op])
1080

    
1081

    
1082
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1083
  """/2/instances/[instance_name]/prepare-export resource.
1084

1085
  """
1086
  def PUT(self):
1087
    """Prepares an export for an instance.
1088

1089
    @return: a job id
1090

1091
    """
1092
    instance_name = self.items[0]
1093
    mode = self._checkStringVariable("mode")
1094

    
1095
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1096
                                 mode=mode)
1097

    
1098
    return baserlib.SubmitJob([op])
1099

    
1100

    
1101
def _ParseExportInstanceRequest(name, data):
1102
  """Parses a request for an instance export.
1103

1104
  @rtype: L{opcodes.OpBackupExport}
1105
  @return: Instance export opcode
1106

1107
  """
1108
  # Rename "destination" to "target_node"
1109
  try:
1110
    data["target_node"] = data.pop("destination")
1111
  except KeyError:
1112
    pass
1113

    
1114
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1115
    "instance_name": name,
1116
    })
1117

    
1118

    
1119
class R_2_instances_name_export(baserlib.R_Generic):
1120
  """/2/instances/[instance_name]/export resource.
1121

1122
  """
1123
  def PUT(self):
1124
    """Exports an instance.
1125

1126
    @return: a job id
1127

1128
    """
1129
    if not isinstance(self.request_body, dict):
1130
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1131

    
1132
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1133

    
1134
    return baserlib.SubmitJob([op])
1135

    
1136

    
1137
def _ParseMigrateInstanceRequest(name, data):
1138
  """Parses a request for an instance migration.
1139

1140
  @rtype: L{opcodes.OpInstanceMigrate}
1141
  @return: Instance migration opcode
1142

1143
  """
1144
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1145
    "instance_name": name,
1146
    })
1147

    
1148

    
1149
class R_2_instances_name_migrate(baserlib.R_Generic):
1150
  """/2/instances/[instance_name]/migrate resource.
1151

1152
  """
1153
  def PUT(self):
1154
    """Migrates an instance.
1155

1156
    @return: a job id
1157

1158
    """
1159
    baserlib.CheckType(self.request_body, dict, "Body contents")
1160

    
1161
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1162

    
1163
    return baserlib.SubmitJob([op])
1164

    
1165

    
1166
class R_2_instances_name_failover(baserlib.R_Generic):
1167
  """/2/instances/[instance_name]/failover resource.
1168

1169
  """
1170
  def PUT(self):
1171
    """Does a failover of an instance.
1172

1173
    @return: a job id
1174

1175
    """
1176
    baserlib.CheckType(self.request_body, dict, "Body contents")
1177

    
1178
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1179
      "instance_name": self.items[0],
1180
      })
1181

    
1182
    return baserlib.SubmitJob([op])
1183

    
1184

    
1185
def _ParseRenameInstanceRequest(name, data):
1186
  """Parses a request for renaming an instance.
1187

1188
  @rtype: L{opcodes.OpInstanceRename}
1189
  @return: Instance rename opcode
1190

1191
  """
1192
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1193
    "instance_name": name,
1194
    })
1195

    
1196

    
1197
class R_2_instances_name_rename(baserlib.R_Generic):
1198
  """/2/instances/[instance_name]/rename resource.
1199

1200
  """
1201
  def PUT(self):
1202
    """Changes the name of an instance.
1203

1204
    @return: a job id
1205

1206
    """
1207
    baserlib.CheckType(self.request_body, dict, "Body contents")
1208

    
1209
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1210

    
1211
    return baserlib.SubmitJob([op])
1212

    
1213

    
1214
def _ParseModifyInstanceRequest(name, data):
1215
  """Parses a request for modifying an instance.
1216

1217
  @rtype: L{opcodes.OpInstanceSetParams}
1218
  @return: Instance modify opcode
1219

1220
  """
1221
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1222
    "instance_name": name,
1223
    })
1224

    
1225

    
1226
class R_2_instances_name_modify(baserlib.R_Generic):
1227
  """/2/instances/[instance_name]/modify resource.
1228

1229
  """
1230
  def PUT(self):
1231
    """Changes some parameters of an instance.
1232

1233
    @return: a job id
1234

1235
    """
1236
    baserlib.CheckType(self.request_body, dict, "Body contents")
1237

    
1238
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1239

    
1240
    return baserlib.SubmitJob([op])
1241

    
1242

    
1243
class R_2_instances_name_disk_grow(baserlib.R_Generic):
1244
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1245

1246
  """
1247
  def POST(self):
1248
    """Increases the size of an instance disk.
1249

1250
    @return: a job id
1251

1252
    """
1253
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1254
      "instance_name": self.items[0],
1255
      "disk": int(self.items[1]),
1256
      })
1257

    
1258
    return baserlib.SubmitJob([op])
1259

    
1260

    
1261
class R_2_instances_name_console(baserlib.R_Generic):
1262
  """/2/instances/[instance_name]/console resource.
1263

1264
  """
1265
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1266

    
1267
  def GET(self):
1268
    """Request information for connecting to instance's console.
1269

1270
    @return: Serialized instance console description, see
1271
             L{objects.InstanceConsole}
1272

1273
    """
1274
    client = baserlib.GetClient()
1275

    
1276
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1277

    
1278
    if console is None:
1279
      raise http.HttpServiceUnavailable("Instance console unavailable")
1280

    
1281
    assert isinstance(console, dict)
1282
    return console
1283

    
1284

    
1285
def _GetQueryFields(args):
1286
  """
1287

1288
  """
1289
  try:
1290
    fields = args["fields"]
1291
  except KeyError:
1292
    raise http.HttpBadRequest("Missing 'fields' query argument")
1293

    
1294
  return _SplitQueryFields(fields[0])
1295

    
1296

    
1297
def _SplitQueryFields(fields):
1298
  """
1299

1300
  """
1301
  return [i.strip() for i in fields.split(",")]
1302

    
1303

    
1304
class R_2_query(baserlib.R_Generic):
1305
  """/2/query/[resource] resource.
1306

1307
  """
1308
  # Results might contain sensitive information
1309
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1310

    
1311
  def _Query(self, fields, filter_):
1312
    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1313

    
1314
  def GET(self):
1315
    """Returns resource information.
1316

1317
    @return: Query result, see L{objects.QueryResponse}
1318

1319
    """
1320
    return self._Query(_GetQueryFields(self.queryargs), None)
1321

    
1322
  def PUT(self):
1323
    """Submits job querying for resources.
1324

1325
    @return: Query result, see L{objects.QueryResponse}
1326

1327
    """
1328
    body = self.request_body
1329

    
1330
    baserlib.CheckType(body, dict, "Body contents")
1331

    
1332
    try:
1333
      fields = body["fields"]
1334
    except KeyError:
1335
      fields = _GetQueryFields(self.queryargs)
1336

    
1337
    return self._Query(fields, self.request_body.get("filter", None))
1338

    
1339

    
1340
class R_2_query_fields(baserlib.R_Generic):
1341
  """/2/query/[resource]/fields resource.
1342

1343
  """
1344
  def GET(self):
1345
    """Retrieves list of available fields for a resource.
1346

1347
    @return: List of serialized L{objects.QueryFieldDefinition}
1348

1349
    """
1350
    try:
1351
      raw_fields = self.queryargs["fields"]
1352
    except KeyError:
1353
      fields = None
1354
    else:
1355
      fields = _SplitQueryFields(raw_fields[0])
1356

    
1357
    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1358

    
1359

    
1360
class _R_Tags(baserlib.R_Generic):
1361
  """ Quasiclass for tagging resources
1362

1363
  Manages tags. When inheriting this class you must define the
1364
  TAG_LEVEL for it.
1365

1366
  """
1367
  TAG_LEVEL = None
1368

    
1369
  def __init__(self, items, queryargs, req):
1370
    """A tag resource constructor.
1371

1372
    We have to override the default to sort out cluster naming case.
1373

1374
    """
1375
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1376

    
1377
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1378
      self.name = None
1379
    else:
1380
      self.name = items[0]
1381

    
1382
  def GET(self):
1383
    """Returns a list of tags.
1384

1385
    Example: ["tag1", "tag2", "tag3"]
1386

1387
    """
1388
    # pylint: disable=W0212
1389
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1390

    
1391
  def PUT(self):
1392
    """Add a set of tags.
1393

1394
    The request as a list of strings should be PUT to this URI. And
1395
    you'll have back a job id.
1396

1397
    """
1398
    # pylint: disable=W0212
1399
    if "tag" not in self.queryargs:
1400
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1401
                                " the 'tag' parameter")
1402
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1403
                              self.queryargs["tag"], name=self.name,
1404
                              dry_run=bool(self.dryRun()))
1405

    
1406
  def DELETE(self):
1407
    """Delete a tag.
1408

1409
    In order to delete a set of tags, the DELETE
1410
    request should be addressed to URI like:
1411
    /tags?tag=[tag]&tag=[tag]
1412

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

    
1424

    
1425
class R_2_instances_name_tags(_R_Tags):
1426
  """ /2/instances/[instance_name]/tags resource.
1427

1428
  Manages per-instance tags.
1429

1430
  """
1431
  TAG_LEVEL = constants.TAG_INSTANCE
1432

    
1433

    
1434
class R_2_nodes_name_tags(_R_Tags):
1435
  """ /2/nodes/[node_name]/tags resource.
1436

1437
  Manages per-node tags.
1438

1439
  """
1440
  TAG_LEVEL = constants.TAG_NODE
1441

    
1442

    
1443
class R_2_groups_name_tags(_R_Tags):
1444
  """ /2/groups/[group_name]/tags resource.
1445

1446
  Manages per-nodegroup tags.
1447

1448
  """
1449
  TAG_LEVEL = constants.TAG_NODEGROUP
1450

    
1451

    
1452
class R_2_tags(_R_Tags):
1453
  """ /2/tags resource.
1454

1455
  Manages cluster tags.
1456

1457
  """
1458
  TAG_LEVEL = constants.TAG_CLUSTER