Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ e687ec01

History | View | Annotate | Download (37 kB)

1
#
2
#
3

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

    
21

    
22
"""Remote API resource implementations.
23

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

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

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

35
Quoting from RFC2616, section 9.6::
36

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

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

52
"""
53

    
54
# pylint: disable-msg=C0103
55

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

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

    
67

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
150

    
151
class R_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_storage(baserlib.R_Generic):
501
  """/2/nodes/[node_name]/storage resource.
502

503
  """
504
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
505
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
506

    
507
  def GET(self):
508
    node_name = self.items[0]
509

    
510
    storage_type = self._checkStringVariable("storage_type", None)
511
    if not storage_type:
512
      raise http.HttpBadRequest("Missing the required 'storage_type'"
513
                                " parameter")
514

    
515
    output_fields = self._checkStringVariable("output_fields", None)
516
    if not output_fields:
517
      raise http.HttpBadRequest("Missing the required 'output_fields'"
518
                                " parameter")
519

    
520
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
521
                                    storage_type=storage_type,
522
                                    output_fields=output_fields.split(","))
523
    return baserlib.SubmitJob([op])
524

    
525

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

529
  """
530
  def PUT(self):
531
    node_name = self.items[0]
532

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

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

    
543
    changes = {}
544

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

    
549
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
550
                                     storage_type=storage_type,
551
                                     name=name,
552
                                     changes=changes)
553
    return baserlib.SubmitJob([op])
554

    
555

    
556
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
557
  """/2/nodes/[node_name]/storage/repair resource.
558

559
  """
560
  def PUT(self):
561
    node_name = self.items[0]
562

    
563
    storage_type = self._checkStringVariable("storage_type", None)
564
    if not storage_type:
565
      raise http.HttpBadRequest("Missing the required 'storage_type'"
566
                                " parameter")
567

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

    
573
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
574
                                     storage_type=storage_type,
575
                                     name=name)
576
    return baserlib.SubmitJob([op])
577

    
578

    
579
def _ParseCreateGroupRequest(data, dry_run):
580
  """Parses a request for creating a node group.
581

582
  @rtype: L{opcodes.OpGroupAdd}
583
  @return: Group creation opcode
584

585
  """
586
  override = {
587
    "dry_run": dry_run,
588
    }
589

    
590
  rename = {
591
    "name": "group_name",
592
    }
593

    
594
  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
595
                             rename=rename)
596

    
597

    
598
class R_2_groups(baserlib.R_Generic):
599
  """/2/groups resource.
600

601
  """
602
  def GET(self):
603
    """Returns a list of all node groups.
604

605
    """
606
    client = baserlib.GetClient()
607

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

    
617
  def POST(self):
618
    """Create a node group.
619

620
    @return: a job id
621

622
    """
623
    baserlib.CheckType(self.request_body, dict, "Body contents")
624
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
625
    return baserlib.SubmitJob([op])
626

    
627

    
628
class R_2_groups_name(baserlib.R_Generic):
629
  """/2/groups/[group_name] resource.
630

631
  """
632
  def GET(self):
633
    """Send information about a node group.
634

635
    """
636
    group_name = self.items[0]
637
    client = baserlib.GetClient()
638

    
639
    result = baserlib.HandleItemQueryErrors(client.QueryGroups,
640
                                            names=[group_name], fields=G_FIELDS,
641
                                            use_locking=self.useLocking())
642

    
643
    return baserlib.MapFields(G_FIELDS, result[0])
644

    
645
  def DELETE(self):
646
    """Delete a node group.
647

648
    """
649
    op = opcodes.OpGroupRemove(group_name=self.items[0],
650
                               dry_run=bool(self.dryRun()))
651

    
652
    return baserlib.SubmitJob([op])
653

    
654

    
655
def _ParseModifyGroupRequest(name, data):
656
  """Parses a request for modifying a node group.
657

658
  @rtype: L{opcodes.OpGroupSetParams}
659
  @return: Group modify opcode
660

661
  """
662
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
663
    "group_name": name,
664
    })
665

    
666

    
667
class R_2_groups_name_modify(baserlib.R_Generic):
668
  """/2/groups/[group_name]/modify resource.
669

670
  """
671
  def PUT(self):
672
    """Changes some parameters of node group.
673

674
    @return: a job id
675

676
    """
677
    baserlib.CheckType(self.request_body, dict, "Body contents")
678

    
679
    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
680

    
681
    return baserlib.SubmitJob([op])
682

    
683

    
684
def _ParseRenameGroupRequest(name, data, dry_run):
685
  """Parses a request for renaming a node group.
686

687
  @type name: string
688
  @param name: name of the node group to rename
689
  @type data: dict
690
  @param data: the body received by the rename request
691
  @type dry_run: bool
692
  @param dry_run: whether to perform a dry run
693

694
  @rtype: L{opcodes.OpGroupRename}
695
  @return: Node group rename opcode
696

697
  """
698
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
699
    "group_name": name,
700
    "dry_run": dry_run,
701
    })
702

    
703

    
704
class R_2_groups_name_rename(baserlib.R_Generic):
705
  """/2/groups/[group_name]/rename resource.
706

707
  """
708
  def PUT(self):
709
    """Changes the name of a node group.
710

711
    @return: a job id
712

713
    """
714
    baserlib.CheckType(self.request_body, dict, "Body contents")
715
    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
716
                                  self.dryRun())
717
    return baserlib.SubmitJob([op])
718

    
719

    
720
class R_2_groups_name_assign_nodes(baserlib.R_Generic):
721
  """/2/groups/[group_name]/assign-nodes resource.
722

723
  """
724
  def PUT(self):
725
    """Assigns nodes to a group.
726

727
    @return: a job id
728

729
    """
730
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
731
      "group_name": self.items[0],
732
      "dry_run": self.dryRun(),
733
      "force": self.useForce(),
734
      })
735

    
736
    return baserlib.SubmitJob([op])
737

    
738

    
739
def _ParseInstanceCreateRequestVersion1(data, dry_run):
740
  """Parses an instance creation request version 1.
741

742
  @rtype: L{opcodes.OpInstanceCreate}
743
  @return: Instance creation opcode
744

745
  """
746
  override = {
747
    "dry_run": dry_run,
748
    }
749

    
750
  rename = {
751
    "os": "os_type",
752
    "name": "instance_name",
753
    }
754

    
755
  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
756
                             rename=rename)
757

    
758

    
759
class R_2_instances(baserlib.R_Generic):
760
  """/2/instances resource.
761

762
  """
763
  def GET(self):
764
    """Returns a list of all available instances.
765

766
    """
767
    client = baserlib.GetClient()
768

    
769
    use_locking = self.useLocking()
770
    if self.useBulk():
771
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
772
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
773
    else:
774
      instancesdata = client.QueryInstances([], ["name"], use_locking)
775
      instanceslist = [row[0] for row in instancesdata]
776
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
777
                                   uri_fields=("id", "uri"))
778

    
779
  def POST(self):
780
    """Create an instance.
781

782
    @return: a job id
783

784
    """
785
    if not isinstance(self.request_body, dict):
786
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
787

    
788
    # Default to request data version 0
789
    data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
790

    
791
    if data_version == 0:
792
      raise http.HttpBadRequest("Instance creation request version 0 is no"
793
                                " longer supported")
794
    elif data_version == 1:
795
      data = self.request_body.copy()
796
      # Remove "__version__"
797
      data.pop(_REQ_DATA_VERSION, None)
798
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
799
    else:
800
      raise http.HttpBadRequest("Unsupported request data version %s" %
801
                                data_version)
802

    
803
    return baserlib.SubmitJob([op])
804

    
805

    
806
class R_2_instances_name(baserlib.R_Generic):
807
  """/2/instances/[instance_name] resource.
808

809
  """
810
  def GET(self):
811
    """Send information about an instance.
812

813
    """
814
    client = baserlib.GetClient()
815
    instance_name = self.items[0]
816

    
817
    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
818
                                            names=[instance_name],
819
                                            fields=I_FIELDS,
820
                                            use_locking=self.useLocking())
821

    
822
    return baserlib.MapFields(I_FIELDS, result[0])
823

    
824
  def DELETE(self):
825
    """Delete an instance.
826

827
    """
828
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
829
                                  ignore_failures=False,
830
                                  dry_run=bool(self.dryRun()))
831
    return baserlib.SubmitJob([op])
832

    
833

    
834
class R_2_instances_name_info(baserlib.R_Generic):
835
  """/2/instances/[instance_name]/info resource.
836

837
  """
838
  def GET(self):
839
    """Request detailed instance information.
840

841
    """
842
    instance_name = self.items[0]
843
    static = bool(self._checkIntVariable("static", default=0))
844

    
845
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
846
                                     static=static)
847
    return baserlib.SubmitJob([op])
848

    
849

    
850
class R_2_instances_name_reboot(baserlib.R_Generic):
851
  """/2/instances/[instance_name]/reboot resource.
852

853
  Implements an instance reboot.
854

855
  """
856
  def POST(self):
857
    """Reboot an instance.
858

859
    The URI takes type=[hard|soft|full] and
860
    ignore_secondaries=[False|True] parameters.
861

862
    """
863
    instance_name = self.items[0]
864
    reboot_type = self.queryargs.get("type",
865
                                     [constants.INSTANCE_REBOOT_HARD])[0]
866
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
867
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
868
                                  reboot_type=reboot_type,
869
                                  ignore_secondaries=ignore_secondaries,
870
                                  dry_run=bool(self.dryRun()))
871

    
872
    return baserlib.SubmitJob([op])
873

    
874

    
875
class R_2_instances_name_startup(baserlib.R_Generic):
876
  """/2/instances/[instance_name]/startup resource.
877

878
  Implements an instance startup.
879

880
  """
881
  def PUT(self):
882
    """Startup an instance.
883

884
    The URI takes force=[False|True] parameter to start the instance
885
    if even if secondary disks are failing.
886

887
    """
888
    instance_name = self.items[0]
889
    force_startup = bool(self._checkIntVariable("force"))
890
    no_remember = bool(self._checkIntVariable("no_remember"))
891
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
892
                                   force=force_startup,
893
                                   dry_run=bool(self.dryRun()),
894
                                   no_remember=no_remember)
895

    
896
    return baserlib.SubmitJob([op])
897

    
898

    
899
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
900
  """Parses a request for an instance shutdown.
901

902
  @rtype: L{opcodes.OpInstanceShutdown}
903
  @return: Instance shutdown opcode
904

905
  """
906
  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
907
    "instance_name": name,
908
    "dry_run": dry_run,
909
    "no_remember": no_remember,
910
    })
911

    
912

    
913
class R_2_instances_name_shutdown(baserlib.R_Generic):
914
  """/2/instances/[instance_name]/shutdown resource.
915

916
  Implements an instance shutdown.
917

918
  """
919
  def PUT(self):
920
    """Shutdown an instance.
921

922
    @return: a job id
923

924
    """
925
    baserlib.CheckType(self.request_body, dict, "Body contents")
926

    
927
    no_remember = bool(self._checkIntVariable("no_remember"))
928
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
929
                                       bool(self.dryRun()), no_remember)
930

    
931
    return baserlib.SubmitJob([op])
932

    
933

    
934
def _ParseInstanceReinstallRequest(name, data):
935
  """Parses a request for reinstalling an instance.
936

937
  """
938
  if not isinstance(data, dict):
939
    raise http.HttpBadRequest("Invalid body contents, not a dictionary")
940

    
941
  ostype = baserlib.CheckParameter(data, "os", default=None)
942
  start = baserlib.CheckParameter(data, "start", exptype=bool,
943
                                  default=True)
944
  osparams = baserlib.CheckParameter(data, "osparams", default=None)
945

    
946
  ops = [
947
    opcodes.OpInstanceShutdown(instance_name=name),
948
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
949
                                osparams=osparams),
950
    ]
951

    
952
  if start:
953
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
954

    
955
  return ops
956

    
957

    
958
class R_2_instances_name_reinstall(baserlib.R_Generic):
959
  """/2/instances/[instance_name]/reinstall resource.
960

961
  Implements an instance reinstall.
962

963
  """
964
  def POST(self):
965
    """Reinstall an instance.
966

967
    The URI takes os=name and nostartup=[0|1] optional
968
    parameters. By default, the instance will be started
969
    automatically.
970

971
    """
972
    if self.request_body:
973
      if self.queryargs:
974
        raise http.HttpBadRequest("Can't combine query and body parameters")
975

    
976
      body = self.request_body
977
    elif self.queryargs:
978
      # Legacy interface, do not modify/extend
979
      body = {
980
        "os": self._checkStringVariable("os"),
981
        "start": not self._checkIntVariable("nostartup"),
982
        }
983
    else:
984
      body = {}
985

    
986
    ops = _ParseInstanceReinstallRequest(self.items[0], body)
987

    
988
    return baserlib.SubmitJob(ops)
989

    
990

    
991
def _ParseInstanceReplaceDisksRequest(name, data):
992
  """Parses a request for an instance export.
993

994
  @rtype: L{opcodes.OpInstanceReplaceDisks}
995
  @return: Instance export opcode
996

997
  """
998
  override = {
999
    "instance_name": name,
1000
    }
1001

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

    
1015
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1016

    
1017

    
1018
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1019
  """/2/instances/[instance_name]/replace-disks resource.
1020

1021
  """
1022
  def POST(self):
1023
    """Replaces disks on an instance.
1024

1025
    """
1026
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1027

    
1028
    return baserlib.SubmitJob([op])
1029

    
1030

    
1031
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1032
  """/2/instances/[instance_name]/activate-disks resource.
1033

1034
  """
1035
  def PUT(self):
1036
    """Activate disks for an instance.
1037

1038
    The URI might contain ignore_size to ignore current recorded size.
1039

1040
    """
1041
    instance_name = self.items[0]
1042
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1043

    
1044
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1045
                                         ignore_size=ignore_size)
1046

    
1047
    return baserlib.SubmitJob([op])
1048

    
1049

    
1050
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1051
  """/2/instances/[instance_name]/deactivate-disks resource.
1052

1053
  """
1054
  def PUT(self):
1055
    """Deactivate disks for an instance.
1056

1057
    """
1058
    instance_name = self.items[0]
1059

    
1060
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1061

    
1062
    return baserlib.SubmitJob([op])
1063

    
1064

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

1068
  """
1069
  def PUT(self):
1070
    """Prepares an export for an instance.
1071

1072
    @return: a job id
1073

1074
    """
1075
    instance_name = self.items[0]
1076
    mode = self._checkStringVariable("mode")
1077

    
1078
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1079
                                 mode=mode)
1080

    
1081
    return baserlib.SubmitJob([op])
1082

    
1083

    
1084
def _ParseExportInstanceRequest(name, data):
1085
  """Parses a request for an instance export.
1086

1087
  @rtype: L{opcodes.OpBackupExport}
1088
  @return: Instance export opcode
1089

1090
  """
1091
  # Rename "destination" to "target_node"
1092
  try:
1093
    data["target_node"] = data.pop("destination")
1094
  except KeyError:
1095
    pass
1096

    
1097
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1098
    "instance_name": name,
1099
    })
1100

    
1101

    
1102
class R_2_instances_name_export(baserlib.R_Generic):
1103
  """/2/instances/[instance_name]/export resource.
1104

1105
  """
1106
  def PUT(self):
1107
    """Exports an instance.
1108

1109
    @return: a job id
1110

1111
    """
1112
    if not isinstance(self.request_body, dict):
1113
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1114

    
1115
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1116

    
1117
    return baserlib.SubmitJob([op])
1118

    
1119

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

1123
  @rtype: L{opcodes.OpInstanceMigrate}
1124
  @return: Instance migration opcode
1125

1126
  """
1127
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1128
    "instance_name": name,
1129
    })
1130

    
1131

    
1132
class R_2_instances_name_migrate(baserlib.R_Generic):
1133
  """/2/instances/[instance_name]/migrate resource.
1134

1135
  """
1136
  def PUT(self):
1137
    """Migrates an instance.
1138

1139
    @return: a job id
1140

1141
    """
1142
    baserlib.CheckType(self.request_body, dict, "Body contents")
1143

    
1144
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1145

    
1146
    return baserlib.SubmitJob([op])
1147

    
1148

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

1152
  """
1153
  def PUT(self):
1154
    """Does a failover of an instance.
1155

1156
    @return: a job id
1157

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

    
1161
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1162
      "instance_name": self.items[0],
1163
      })
1164

    
1165
    return baserlib.SubmitJob([op])
1166

    
1167

    
1168
def _ParseRenameInstanceRequest(name, data):
1169
  """Parses a request for renaming an instance.
1170

1171
  @rtype: L{opcodes.OpInstanceRename}
1172
  @return: Instance rename opcode
1173

1174
  """
1175
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1176
    "instance_name": name,
1177
    })
1178

    
1179

    
1180
class R_2_instances_name_rename(baserlib.R_Generic):
1181
  """/2/instances/[instance_name]/rename resource.
1182

1183
  """
1184
  def PUT(self):
1185
    """Changes the name of an instance.
1186

1187
    @return: a job id
1188

1189
    """
1190
    baserlib.CheckType(self.request_body, dict, "Body contents")
1191

    
1192
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1193

    
1194
    return baserlib.SubmitJob([op])
1195

    
1196

    
1197
def _ParseModifyInstanceRequest(name, data):
1198
  """Parses a request for modifying an instance.
1199

1200
  @rtype: L{opcodes.OpInstanceSetParams}
1201
  @return: Instance modify opcode
1202

1203
  """
1204
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1205
    "instance_name": name,
1206
    })
1207

    
1208

    
1209
class R_2_instances_name_modify(baserlib.R_Generic):
1210
  """/2/instances/[instance_name]/modify resource.
1211

1212
  """
1213
  def PUT(self):
1214
    """Changes some parameters of an instance.
1215

1216
    @return: a job id
1217

1218
    """
1219
    baserlib.CheckType(self.request_body, dict, "Body contents")
1220

    
1221
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1222

    
1223
    return baserlib.SubmitJob([op])
1224

    
1225

    
1226
class R_2_instances_name_disk_grow(baserlib.R_Generic):
1227
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1228

1229
  """
1230
  def POST(self):
1231
    """Increases the size of an instance disk.
1232

1233
    @return: a job id
1234

1235
    """
1236
    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1237
      "instance_name": self.items[0],
1238
      "disk": int(self.items[1]),
1239
      })
1240

    
1241
    return baserlib.SubmitJob([op])
1242

    
1243

    
1244
class R_2_instances_name_console(baserlib.R_Generic):
1245
  """/2/instances/[instance_name]/console resource.
1246

1247
  """
1248
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1249

    
1250
  def GET(self):
1251
    """Request information for connecting to instance's console.
1252

1253
    @return: Serialized instance console description, see
1254
             L{objects.InstanceConsole}
1255

1256
    """
1257
    client = baserlib.GetClient()
1258

    
1259
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1260

    
1261
    if console is None:
1262
      raise http.HttpServiceUnavailable("Instance console unavailable")
1263

    
1264
    assert isinstance(console, dict)
1265
    return console
1266

    
1267

    
1268
def _GetQueryFields(args):
1269
  """
1270

1271
  """
1272
  try:
1273
    fields = args["fields"]
1274
  except KeyError:
1275
    raise http.HttpBadRequest("Missing 'fields' query argument")
1276

    
1277
  return _SplitQueryFields(fields[0])
1278

    
1279

    
1280
def _SplitQueryFields(fields):
1281
  """
1282

1283
  """
1284
  return [i.strip() for i in fields.split(",")]
1285

    
1286

    
1287
class R_2_query(baserlib.R_Generic):
1288
  """/2/query/[resource] resource.
1289

1290
  """
1291
  # Results might contain sensitive information
1292
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1293

    
1294
  def _Query(self, fields, filter_):
1295
    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1296

    
1297
  def GET(self):
1298
    """Returns resource information.
1299

1300
    @return: Query result, see L{objects.QueryResponse}
1301

1302
    """
1303
    return self._Query(_GetQueryFields(self.queryargs), None)
1304

    
1305
  def PUT(self):
1306
    """Submits job querying for resources.
1307

1308
    @return: Query result, see L{objects.QueryResponse}
1309

1310
    """
1311
    body = self.request_body
1312

    
1313
    baserlib.CheckType(body, dict, "Body contents")
1314

    
1315
    try:
1316
      fields = body["fields"]
1317
    except KeyError:
1318
      fields = _GetQueryFields(self.queryargs)
1319

    
1320
    return self._Query(fields, self.request_body.get("filter", None))
1321

    
1322

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

1326
  """
1327
  def GET(self):
1328
    """Retrieves list of available fields for a resource.
1329

1330
    @return: List of serialized L{objects.QueryFieldDefinition}
1331

1332
    """
1333
    try:
1334
      raw_fields = self.queryargs["fields"]
1335
    except KeyError:
1336
      fields = None
1337
    else:
1338
      fields = _SplitQueryFields(raw_fields[0])
1339

    
1340
    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1341

    
1342

    
1343
class _R_Tags(baserlib.R_Generic):
1344
  """ Quasiclass for tagging resources
1345

1346
  Manages tags. When inheriting this class you must define the
1347
  TAG_LEVEL for it.
1348

1349
  """
1350
  TAG_LEVEL = None
1351

    
1352
  def __init__(self, items, queryargs, req):
1353
    """A tag resource constructor.
1354

1355
    We have to override the default to sort out cluster naming case.
1356

1357
    """
1358
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1359

    
1360
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1361
      self.name = None
1362
    else:
1363
      self.name = items[0]
1364

    
1365
  def GET(self):
1366
    """Returns a list of tags.
1367

1368
    Example: ["tag1", "tag2", "tag3"]
1369

1370
    """
1371
    # pylint: disable-msg=W0212
1372
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1373

    
1374
  def PUT(self):
1375
    """Add a set of tags.
1376

1377
    The request as a list of strings should be PUT to this URI. And
1378
    you'll have back a job id.
1379

1380
    """
1381
    # pylint: disable-msg=W0212
1382
    if "tag" not in self.queryargs:
1383
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
1384
                                " the 'tag' parameter")
1385
    return baserlib._Tags_PUT(self.TAG_LEVEL,
1386
                              self.queryargs["tag"], name=self.name,
1387
                              dry_run=bool(self.dryRun()))
1388

    
1389
  def DELETE(self):
1390
    """Delete a tag.
1391

1392
    In order to delete a set of tags, the DELETE
1393
    request should be addressed to URI like:
1394
    /tags?tag=[tag]&tag=[tag]
1395

1396
    """
1397
    # pylint: disable-msg=W0212
1398
    if "tag" not in self.queryargs:
1399
      # no we not gonna delete all tags
1400
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
1401
                                " tag(s) using the 'tag' parameter")
1402
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
1403
                                 self.queryargs["tag"],
1404
                                 name=self.name,
1405
                                 dry_run=bool(self.dryRun()))
1406

    
1407

    
1408
class R_2_instances_name_tags(_R_Tags):
1409
  """ /2/instances/[instance_name]/tags resource.
1410

1411
  Manages per-instance tags.
1412

1413
  """
1414
  TAG_LEVEL = constants.TAG_INSTANCE
1415

    
1416

    
1417
class R_2_nodes_name_tags(_R_Tags):
1418
  """ /2/nodes/[node_name]/tags resource.
1419

1420
  Manages per-node tags.
1421

1422
  """
1423
  TAG_LEVEL = constants.TAG_NODE
1424

    
1425

    
1426
class R_2_groups_name_tags(_R_Tags):
1427
  """ /2/groups/[group_name]/tags resource.
1428

1429
  Manages per-nodegroup tags.
1430

1431
  """
1432
  TAG_LEVEL = constants.TAG_NODEGROUP
1433

    
1434

    
1435
class R_2_tags(_R_Tags):
1436
  """ /2/tags resource.
1437

1438
  Manages cluster tags.
1439

1440
  """
1441
  TAG_LEVEL = constants.TAG_CLUSTER