Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 370f2042

History | View | Annotate | Download (37.5 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["disks"]
1025
  except KeyError:
1026
    pass
1027
  else:
1028
    if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1029
      # Backwards compatibility for strings of the format "1, 2, 3"
1030
      try:
1031
        data["disks"] = [int(part) for part in raw_disks.split(",")]
1032
      except (TypeError, ValueError), err:
1033
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1034

    
1035
  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1036

    
1037

    
1038
class R_2_instances_name_replace_disks(baserlib.R_Generic):
1039
  """/2/instances/[instance_name]/replace-disks resource.
1040

1041
  """
1042
  def POST(self):
1043
    """Replaces disks on an instance.
1044

1045
    """
1046
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1047

    
1048
    return baserlib.SubmitJob([op])
1049

    
1050

    
1051
class R_2_instances_name_activate_disks(baserlib.R_Generic):
1052
  """/2/instances/[instance_name]/activate-disks resource.
1053

1054
  """
1055
  def PUT(self):
1056
    """Activate disks for an instance.
1057

1058
    The URI might contain ignore_size to ignore current recorded size.
1059

1060
    """
1061
    instance_name = self.items[0]
1062
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1063

    
1064
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1065
                                         ignore_size=ignore_size)
1066

    
1067
    return baserlib.SubmitJob([op])
1068

    
1069

    
1070
class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1071
  """/2/instances/[instance_name]/deactivate-disks resource.
1072

1073
  """
1074
  def PUT(self):
1075
    """Deactivate disks for an instance.
1076

1077
    """
1078
    instance_name = self.items[0]
1079

    
1080
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1081

    
1082
    return baserlib.SubmitJob([op])
1083

    
1084

    
1085
class R_2_instances_name_prepare_export(baserlib.R_Generic):
1086
  """/2/instances/[instance_name]/prepare-export resource.
1087

1088
  """
1089
  def PUT(self):
1090
    """Prepares an export for an instance.
1091

1092
    @return: a job id
1093

1094
    """
1095
    instance_name = self.items[0]
1096
    mode = self._checkStringVariable("mode")
1097

    
1098
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1099
                                 mode=mode)
1100

    
1101
    return baserlib.SubmitJob([op])
1102

    
1103

    
1104
def _ParseExportInstanceRequest(name, data):
1105
  """Parses a request for an instance export.
1106

1107
  @rtype: L{opcodes.OpBackupExport}
1108
  @return: Instance export opcode
1109

1110
  """
1111
  # Rename "destination" to "target_node"
1112
  try:
1113
    data["target_node"] = data.pop("destination")
1114
  except KeyError:
1115
    pass
1116

    
1117
  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1118
    "instance_name": name,
1119
    })
1120

    
1121

    
1122
class R_2_instances_name_export(baserlib.R_Generic):
1123
  """/2/instances/[instance_name]/export resource.
1124

1125
  """
1126
  def PUT(self):
1127
    """Exports an instance.
1128

1129
    @return: a job id
1130

1131
    """
1132
    if not isinstance(self.request_body, dict):
1133
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1134

    
1135
    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1136

    
1137
    return baserlib.SubmitJob([op])
1138

    
1139

    
1140
def _ParseMigrateInstanceRequest(name, data):
1141
  """Parses a request for an instance migration.
1142

1143
  @rtype: L{opcodes.OpInstanceMigrate}
1144
  @return: Instance migration opcode
1145

1146
  """
1147
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1148
    "instance_name": name,
1149
    })
1150

    
1151

    
1152
class R_2_instances_name_migrate(baserlib.R_Generic):
1153
  """/2/instances/[instance_name]/migrate resource.
1154

1155
  """
1156
  def PUT(self):
1157
    """Migrates an instance.
1158

1159
    @return: a job id
1160

1161
    """
1162
    baserlib.CheckType(self.request_body, dict, "Body contents")
1163

    
1164
    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1165

    
1166
    return baserlib.SubmitJob([op])
1167

    
1168

    
1169
class R_2_instances_name_failover(baserlib.R_Generic):
1170
  """/2/instances/[instance_name]/failover resource.
1171

1172
  """
1173
  def PUT(self):
1174
    """Does a failover of an instance.
1175

1176
    @return: a job id
1177

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

    
1181
    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1182
      "instance_name": self.items[0],
1183
      })
1184

    
1185
    return baserlib.SubmitJob([op])
1186

    
1187

    
1188
def _ParseRenameInstanceRequest(name, data):
1189
  """Parses a request for renaming an instance.
1190

1191
  @rtype: L{opcodes.OpInstanceRename}
1192
  @return: Instance rename opcode
1193

1194
  """
1195
  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1196
    "instance_name": name,
1197
    })
1198

    
1199

    
1200
class R_2_instances_name_rename(baserlib.R_Generic):
1201
  """/2/instances/[instance_name]/rename resource.
1202

1203
  """
1204
  def PUT(self):
1205
    """Changes the name of an instance.
1206

1207
    @return: a job id
1208

1209
    """
1210
    baserlib.CheckType(self.request_body, dict, "Body contents")
1211

    
1212
    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1213

    
1214
    return baserlib.SubmitJob([op])
1215

    
1216

    
1217
def _ParseModifyInstanceRequest(name, data):
1218
  """Parses a request for modifying an instance.
1219

1220
  @rtype: L{opcodes.OpInstanceSetParams}
1221
  @return: Instance modify opcode
1222

1223
  """
1224
  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1225
    "instance_name": name,
1226
    })
1227

    
1228

    
1229
class R_2_instances_name_modify(baserlib.R_Generic):
1230
  """/2/instances/[instance_name]/modify resource.
1231

1232
  """
1233
  def PUT(self):
1234
    """Changes some parameters of an instance.
1235

1236
    @return: a job id
1237

1238
    """
1239
    baserlib.CheckType(self.request_body, dict, "Body contents")
1240

    
1241
    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1242

    
1243
    return baserlib.SubmitJob([op])
1244

    
1245

    
1246
class R_2_instances_name_disk_grow(baserlib.R_Generic):
1247
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1248

1249
  """
1250
  def POST(self):
1251
    """Increases the size of an instance disk.
1252

1253
    @return: a job id
1254

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

    
1261
    return baserlib.SubmitJob([op])
1262

    
1263

    
1264
class R_2_instances_name_console(baserlib.R_Generic):
1265
  """/2/instances/[instance_name]/console resource.
1266

1267
  """
1268
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1269

    
1270
  def GET(self):
1271
    """Request information for connecting to instance's console.
1272

1273
    @return: Serialized instance console description, see
1274
             L{objects.InstanceConsole}
1275

1276
    """
1277
    client = baserlib.GetClient()
1278

    
1279
    ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1280

    
1281
    if console is None:
1282
      raise http.HttpServiceUnavailable("Instance console unavailable")
1283

    
1284
    assert isinstance(console, dict)
1285
    return console
1286

    
1287

    
1288
def _GetQueryFields(args):
1289
  """
1290

1291
  """
1292
  try:
1293
    fields = args["fields"]
1294
  except KeyError:
1295
    raise http.HttpBadRequest("Missing 'fields' query argument")
1296

    
1297
  return _SplitQueryFields(fields[0])
1298

    
1299

    
1300
def _SplitQueryFields(fields):
1301
  """
1302

1303
  """
1304
  return [i.strip() for i in fields.split(",")]
1305

    
1306

    
1307
class R_2_query(baserlib.R_Generic):
1308
  """/2/query/[resource] resource.
1309

1310
  """
1311
  # Results might contain sensitive information
1312
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1313

    
1314
  def _Query(self, fields, filter_):
1315
    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1316

    
1317
  def GET(self):
1318
    """Returns resource information.
1319

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

1322
    """
1323
    return self._Query(_GetQueryFields(self.queryargs), None)
1324

    
1325
  def PUT(self):
1326
    """Submits job querying for resources.
1327

1328
    @return: Query result, see L{objects.QueryResponse}
1329

1330
    """
1331
    body = self.request_body
1332

    
1333
    baserlib.CheckType(body, dict, "Body contents")
1334

    
1335
    try:
1336
      fields = body["fields"]
1337
    except KeyError:
1338
      fields = _GetQueryFields(self.queryargs)
1339

    
1340
    return self._Query(fields, self.request_body.get("filter", None))
1341

    
1342

    
1343
class R_2_query_fields(baserlib.R_Generic):
1344
  """/2/query/[resource]/fields resource.
1345

1346
  """
1347
  def GET(self):
1348
    """Retrieves list of available fields for a resource.
1349

1350
    @return: List of serialized L{objects.QueryFieldDefinition}
1351

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

    
1360
    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1361

    
1362

    
1363
class _R_Tags(baserlib.R_Generic):
1364
  """ Quasiclass for tagging resources
1365

1366
  Manages tags. When inheriting this class you must define the
1367
  TAG_LEVEL for it.
1368

1369
  """
1370
  TAG_LEVEL = None
1371

    
1372
  def __init__(self, items, queryargs, req):
1373
    """A tag resource constructor.
1374

1375
    We have to override the default to sort out cluster naming case.
1376

1377
    """
1378
    baserlib.R_Generic.__init__(self, items, queryargs, req)
1379

    
1380
    if self.TAG_LEVEL == constants.TAG_CLUSTER:
1381
      self.name = None
1382
    else:
1383
      self.name = items[0]
1384

    
1385
  def GET(self):
1386
    """Returns a list of tags.
1387

1388
    Example: ["tag1", "tag2", "tag3"]
1389

1390
    """
1391
    # pylint: disable=W0212
1392
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1393

    
1394
  def PUT(self):
1395
    """Add a set of tags.
1396

1397
    The request as a list of strings should be PUT to this URI. And
1398
    you'll have back a job id.
1399

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

    
1409
  def DELETE(self):
1410
    """Delete a tag.
1411

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

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

    
1427

    
1428
class R_2_instances_name_tags(_R_Tags):
1429
  """ /2/instances/[instance_name]/tags resource.
1430

1431
  Manages per-instance tags.
1432

1433
  """
1434
  TAG_LEVEL = constants.TAG_INSTANCE
1435

    
1436

    
1437
class R_2_nodes_name_tags(_R_Tags):
1438
  """ /2/nodes/[node_name]/tags resource.
1439

1440
  Manages per-node tags.
1441

1442
  """
1443
  TAG_LEVEL = constants.TAG_NODE
1444

    
1445

    
1446
class R_2_groups_name_tags(_R_Tags):
1447
  """ /2/groups/[group_name]/tags resource.
1448

1449
  Manages per-nodegroup tags.
1450

1451
  """
1452
  TAG_LEVEL = constants.TAG_NODEGROUP
1453

    
1454

    
1455
class R_2_tags(_R_Tags):
1456
  """ /2/tags resource.
1457

1458
  Manages cluster tags.
1459

1460
  """
1461
  TAG_LEVEL = constants.TAG_CLUSTER