rlib2.py 32.4 KB
Newer Older
1
2
3
#
#

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


22
"""Remote API resource implementations.
23

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

27
28
29
According to RFC2616 the main difference between PUT and POST is that
POST can create new resources but PUT can only create the resource the
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
33
entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
new instance) with a name specified in the request.
34

35
Quoting from RFC2616, section 9.6::
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

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

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

52
53
"""

Iustin Pop's avatar
Iustin Pop committed
54
55
56
57
# pylint: disable-msg=C0103

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

Iustin Pop's avatar
Iustin Pop committed
58
from ganeti import opcodes
59
60
from ganeti import http
from ganeti import constants
Iustin Pop's avatar
Iustin Pop committed
61
from ganeti import cli
Michael Hanselmann's avatar
Michael Hanselmann committed
62
from ganeti import rapi
63
from ganeti import ht
64
from ganeti import compat
65
from ganeti import ssconf
Iustin Pop's avatar
Iustin Pop committed
66
from ganeti.rapi import baserlib
67

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
68

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

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

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

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

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

110
_NR_DRAINED = "drained"
111
_NR_MASTER_CANDIDATE = "master-candidate"
112
113
114
115
116
_NR_MASTER = "master"
_NR_OFFLINE = "offline"
_NR_REGULAR = "regular"

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

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

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

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

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

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

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

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

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

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
151

152
class R_root(baserlib.ResourceBase):
153
154
155
156
157
158
159
160
161
162
163
  """/ resource.

  """
  @staticmethod
  def GET():
    """Supported for legacy reasons.

    """
    return None


164
class R_version(baserlib.ResourceBase):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
165
166
167
168
169
170
  """/version resource.

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

  """
171
172
  @staticmethod
  def GET():
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
173
174
175
176
177
178
    """Returns the remote API version.

    """
    return constants.RAPI_VERSION


179
class R_2_info(baserlib.ResourceBase):
180
  """/2/info resource.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
181
182

  """
183
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
184
185
186
    """Returns cluster information.

    """
187
    client = self.GetClient()
188
    return client.QueryClusterInfo()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
189
190


191
class R_2_features(baserlib.ResourceBase):
192
193
194
195
196
197
198
199
  """/2/features resource.

  """
  @staticmethod
  def GET():
    """Returns list of optional RAPI features implemented.

    """
200
    return list(ALL_FEATURES)
201
202


203
class R_2_os(baserlib.ResourceBase):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
204
205
206
  """/2/os resource.

  """
207
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
208
209
210
211
212
213
214
    """Return a list of all OSes.

    Can return error 500 in case of a problem.

    Example: ["debian-etch"]

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

    if not isinstance(diagnose_data, list):
Iustin Pop's avatar
Iustin Pop committed
223
      raise http.HttpBadGateway(message="Can't get OS list")
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
224

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

    return os_names
230

231

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

  """
236
  PUT_OPCODE = opcodes.OpClusterRedistConf
237
238


239
class R_2_cluster_modify(baserlib.OpcodeResource):
240
241
242
  """/2/modify resource.

  """
243
  PUT_OPCODE = opcodes.OpClusterSetParams
244
245


246
class R_2_jobs(baserlib.ResourceBase):
247
248
249
  """/2/jobs resource.

  """
250
  def GET(self):
251
252
    """Returns a dictionary of jobs.

Iustin Pop's avatar
Iustin Pop committed
253
    @return: a dictionary with jobs id and uri.
Iustin Pop's avatar
Iustin Pop committed
254

255
    """
256
    client = self.GetClient()
257
258

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


267
class R_2_jobs_id(baserlib.ResourceBase):
268
269
270
271
272
273
  """/2/jobs/[job_id] resource.

  """
  def GET(self):
    """Returns a job status.

Iustin Pop's avatar
Iustin Pop committed
274
275
276
277
278
279
280
281
    @return: a dictionary with job parameters.
        The result includes:
            - id: job ID as a number
            - status: current job status as a string
            - ops: involved OpCodes as a list of dictionaries for each
              opcodes in the job
            - opstatus: OpCodes status as a list
            - opresult: OpCodes results as a list of lists
Iustin Pop's avatar
Iustin Pop committed
282

283
284
    """
    job_id = self.items[0]
285
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
Iustin Pop's avatar
Iustin Pop committed
286
287
    if result is None:
      raise http.HttpNotFound()
288
    return baserlib.MapFields(J_FIELDS, result)
289

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
290
291
292
293
294
  def DELETE(self):
    """Cancel not-yet-started job.

    """
    job_id = self.items[0]
295
    result = self.GetClient().CancelJob(job_id)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
296
297
    return result

298

299
class R_2_jobs_id_wait(baserlib.ResourceBase):
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
  """/2/jobs/[job_id]/wait resource.

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

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

    """
    job_id = self.items[0]

    fields = self.getBodyParameter("fields")
    prev_job_info = self.getBodyParameter("previous_job_info", None)
    prev_log_serial = self.getBodyParameter("previous_log_serial", None)

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

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

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

329
    client = self.GetClient()
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
    result = client.WaitForJobChangeOnce(job_id, fields,
                                         prev_job_info, prev_log_serial,
                                         timeout=_WFJC_TIMEOUT)
    if not result:
      raise http.HttpNotFound()

    if result == constants.JOB_NOTCHANGED:
      # No changes
      return None

    (job_info, log_entries) = result

    return {
      "job_info": job_info,
      "log_entries": log_entries,
      }


348
class R_2_nodes(baserlib.ResourceBase):
349
350
351
352
353
  """/2/nodes resource.

  """
  def GET(self):
    """Returns a list of all nodes.
Iustin Pop's avatar
Iustin Pop committed
354

355
    """
356
    client = self.GetClient()
Iustin Pop's avatar
Iustin Pop committed
357

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


368
class R_2_nodes_name(baserlib.ResourceBase):
369
  """/2/nodes/[node_name] resource.
370
371

  """
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
372
373
374
375
376
  def GET(self):
    """Send information about a node.

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

    result = baserlib.HandleItemQueryErrors(client.QueryNodes,
                                            names=[node_name], fields=N_FIELDS,
                                            use_locking=self.useLocking())
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
382
383

    return baserlib.MapFields(N_FIELDS, result[0])
384
385


386
387
class R_2_nodes_name_role(baserlib.OpcodeResource):
  """/2/nodes/[node_name]/role resource.
388
389

  """
390
391
  PUT_OPCODE = opcodes.OpNodeSetParams

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

    @return: Node role

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

    return _NR_MAP[result[0][0]]

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

    """
409
    baserlib.CheckType(self.request_body, basestring, "Body contents")
410

411
    role = self.request_body
412
413
414
415
416
417

    if role == _NR_REGULAR:
      candidate = False
      offline = False
      drained = False

418
    elif role == _NR_MASTER_CANDIDATE:
419
420
421
422
423
424
425
426
427
428
429
430
431
432
      candidate = True
      offline = drained = None

    elif role == _NR_DRAINED:
      drained = True
      candidate = offline = None

    elif role == _NR_OFFLINE:
      offline = True
      candidate = drained = None

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

433
    assert len(self.items) == 1
434

435
436
437
438
439
440
441
    return ({}, {
      "node_name": self.items[0],
      "master_candidate": candidate,
      "offline": offline,
      "drained": drained,
      "force": self.useForce(),
      })
442
443


444
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
445
446
447
  """/2/nodes/[node_name]/evacuate resource.

  """
448
449
450
  POST_OPCODE = opcodes.OpNodeEvacuate

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

    """
454
    return (self.request_body, {
455
456
457
      "node_name": self.items[0],
      "dry_run": self.dryRun(),
      })
458

459

460
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
461
  """/2/nodes/[node_name]/migrate resource.
462
463

  """
464
465
466
  POST_OPCODE = opcodes.OpNodeMigrate

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

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

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

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

490
491
    return (data, {
      "node_name": self.items[0],
492
      })
493
494


495
class R_2_nodes_name_storage(baserlib.OpcodeResource):
496
  """/2/nodes/[node_name]/storage resource.
497
498

  """
499
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
500
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
501
  GET_OPCODE = opcodes.OpNodeQueryStorage
502

503
504
  def GetGetOpInput(self):
    """List storage available on a node.
505

506
    """
507
508
    storage_type = self._checkStringVariable("storage_type", None)
    output_fields = self._checkStringVariable("output_fields", None)
509

510
511
512
513
    if not output_fields:
      raise http.HttpBadRequest("Missing the required 'output_fields'"
                                " parameter")

514
515
516
517
518
    return ({}, {
      "nodes": [self.items[0]],
      "storage_type": storage_type,
      "output_fields": output_fields.split(","),
      })
519
520


521
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
522
  """/2/nodes/[node_name]/storage/modify resource.
523
524

  """
525
  PUT_OPCODE = opcodes.OpNodeModifyStorage
526

527
528
  def GetPutOpInput(self):
    """Modifies a storage volume on a node.
529

530
531
    """
    storage_type = self._checkStringVariable("storage_type", None)
532
    name = self._checkStringVariable("name", None)
533

534
535
536
537
538
539
540
541
542
543
    if not name:
      raise http.HttpBadRequest("Missing the required 'name'"
                                " parameter")

    changes = {}

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

544
545
546
547
548
549
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      "changes": changes,
      })
550
551


552
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
553
  """/2/nodes/[node_name]/storage/repair resource.
554
555

  """
556
  PUT_OPCODE = opcodes.OpRepairNodeStorage
557

558
559
  def GetPutOpInput(self):
    """Repairs a storage volume on a node.
560

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

568
569
570
571
572
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      })
573
574


575
576
class R_2_groups(baserlib.OpcodeResource):
  """/2/groups resource.
577
578

  """
579
580
  POST_OPCODE = opcodes.OpGroupAdd
  POST_RENAME = {
581
582
583
    "name": "group_name",
    }

584
585
  def GetPostOpInput(self):
    """Create a node group.
586

587
588
589
590
591
    """
    assert not self.items
    return (self.request_body, {
      "dry_run": self.dryRun(),
      })
592
593
594
595
596

  def GET(self):
    """Returns a list of all node groups.

    """
597
    client = self.GetClient()
598
599
600
601
602
603
604
605
606
607
608

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


609
class R_2_groups_name(baserlib.OpcodeResource):
610
  """/2/groups/[group_name] resource.
611
612

  """
613
614
  DELETE_OPCODE = opcodes.OpGroupRemove

615
616
617
618
619
  def GET(self):
    """Send information about a node group.

    """
    group_name = self.items[0]
620
    client = self.GetClient()
621
622
623
624
625
626
627

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

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

628
  def GetDeleteOpInput(self):
629
630
631
    """Delete a node group.

    """
632
633
634
635
636
    assert len(self.items) == 1
    return ({}, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
637
638


639
class R_2_groups_name_modify(baserlib.OpcodeResource):
640
641
642
  """/2/groups/[group_name]/modify resource.

  """
643
  PUT_OPCODE = opcodes.OpGroupSetParams
644

645
646
  def GetPutOpInput(self):
    """Changes some parameters of node group.
647
648

    """
649
650
651
652
    assert self.items
    return (self.request_body, {
      "group_name": self.items[0],
      })
653
654


655
class R_2_groups_name_rename(baserlib.OpcodeResource):
656
  """/2/groups/[group_name]/rename resource.
657
658

  """
659
  PUT_OPCODE = opcodes.OpGroupRename
660

661
662
  def GetPutOpInput(self):
    """Changes the name of a node group.
663
664

    """
665
666
667
668
669
    assert len(self.items) == 1
    return (self.request_body, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
670
671


672
class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
673
  """/2/groups/[group_name]/assign-nodes resource.
674
675

  """
676
  PUT_OPCODE = opcodes.OpGroupAssignNodes
677

678
679
  def GetPutOpInput(self):
    """Assigns nodes to a group.
680
681

    """
682
683
    assert len(self.items) == 1
    return (self.request_body, {
684
685
686
687
688
689
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      "force": self.useForce(),
      })


690
691
692
def _ParseInstanceCreateRequestVersion1(data, dry_run):
  """Parses an instance creation request version 1.

693
  @rtype: L{opcodes.OpInstanceCreate}
694
695
696
  @return: Instance creation opcode

  """
697
698
699
  override = {
    "dry_run": dry_run,
    }
700

701
702
703
704
705
706
707
  rename = {
    "os": "os_type",
    "name": "instance_name",
    }

  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
                             rename=rename)
708
709


710
class R_2_instances(baserlib.ResourceBase):
711
712
713
714
715
716
717
  """/2/instances resource.

  """
  def GET(self):
    """Returns a list of all available instances.

    """
718
    client = self.GetClient()
719

720
721
722
    use_locking = self.useLocking()
    if self.useBulk():
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
723
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
724
    else:
725
      instancesdata = client.QueryInstances([], ["name"], use_locking)
726
      instanceslist = [row[0] for row in instancesdata]
727
728
729
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
                                   uri_fields=("id", "uri"))

730
731
732
733
734
735
  def POST(self):
    """Create an instance.

    @return: a job id

    """
Luca Bigliardi's avatar
Luca Bigliardi committed
736
    if not isinstance(self.request_body, dict):
737
738
739
740
741
742
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")

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

    if data_version == 0:
743
744
      raise http.HttpBadRequest("Instance creation request version 0 is no"
                                " longer supported")
745
    elif data_version == 1:
746
747
748
749
      data = self.request_body.copy()
      # Remove "__version__"
      data.pop(_REQ_DATA_VERSION, None)
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
750
751
    else:
      raise http.HttpBadRequest("Unsupported request data version %s" %
752
                                data_version)
753

754
    return self.SubmitJob([op])
755

756

757
class R_2_instances_name(baserlib.OpcodeResource):
758
  """/2/instances/[instance_name] resource.
759
760

  """
761
762
  DELETE_OPCODE = opcodes.OpInstanceRemove

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
763
764
765
766
  def GET(self):
    """Send information about an instance.

    """
767
    client = self.GetClient()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
768
    instance_name = self.items[0]
769
770
771
772
773

    result = baserlib.HandleItemQueryErrors(client.QueryInstances,
                                            names=[instance_name],
                                            fields=I_FIELDS,
                                            use_locking=self.useLocking())
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
774
775

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

777
  def GetDeleteOpInput(self):
Iustin Pop's avatar
Iustin Pop committed
778
779
780
    """Delete an instance.

    """
781
782
783
784
785
786
    assert len(self.items) == 1
    return ({}, {
      "instance_name": self.items[0],
      "ignore_failures": False,
      "dry_run": self.dryRun(),
      })
Iustin Pop's avatar
Iustin Pop committed
787

788

789
class R_2_instances_name_info(baserlib.OpcodeResource):
790
791
792
  """/2/instances/[instance_name]/info resource.

  """
793
794
795
  GET_OPCODE = opcodes.OpInstanceQueryData

  def GetGetOpInput(self):
796
797
798
    """Request detailed instance information.

    """
799
800
801
802
803
    assert len(self.items) == 1
    return ({}, {
      "instances": [self.items[0]],
      "static": bool(self._checkIntVariable("static", default=0)),
      })
804
805


806
class R_2_instances_name_reboot(baserlib.OpcodeResource):
807
808
809
810
811
  """/2/instances/[instance_name]/reboot resource.

  Implements an instance reboot.

  """
812
813
814
  POST_OPCODE = opcodes.OpInstanceReboot

  def GetPostOpInput(self):
815
816
    """Reboot an instance.

817
818
819
    The URI takes type=[hard|soft|full] and
    ignore_secondaries=[False|True] parameters.

820
    """
821
822
823
824
825
826
827
    return ({}, {
      "instance_name": self.items[0],
      "reboot_type":
        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
      "dry_run": self.dryRun(),
      })
828
829


830
class R_2_instances_name_startup(baserlib.OpcodeResource):
831
832
833
834
835
  """/2/instances/[instance_name]/startup resource.

  Implements an instance startup.

  """
836
837
838
  PUT_OPCODE = opcodes.OpInstanceStartup

  def GetPutOpInput(self):
839
840
    """Startup an instance.

Iustin Pop's avatar
Iustin Pop committed
841
842
    The URI takes force=[False|True] parameter to start the instance
    if even if secondary disks are failing.
843
844

    """
845
846
847
848
849
850
    return ({}, {
      "instance_name": self.items[0],
      "force": self.useForce(),
      "dry_run": self.dryRun(),
      "no_remember": bool(self._checkIntVariable("no_remember")),
      })
851
852


853
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
854
855
856
857
858
  """/2/instances/[instance_name]/shutdown resource.

  Implements an instance shutdown.

  """
859
  PUT_OPCODE = opcodes.OpInstanceShutdown
860

861
862
  def GetPutOpInput(self):
    """Shutdown an instance.
863

864
    """
865
866
867
868
869
    return (self.request_body, {
      "instance_name": self.items[0],
      "no_remember": bool(self._checkIntVariable("no_remember")),
      "dry_run": self.dryRun(),
      })
870
871


872
873
874
875
876
877
878
def _ParseInstanceReinstallRequest(name, data):
  """Parses a request for reinstalling an instance.

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

879
  ostype = baserlib.CheckParameter(data, "os", default=None)
880
881
882
883
884
  start = baserlib.CheckParameter(data, "start", exptype=bool,
                                  default=True)
  osparams = baserlib.CheckParameter(data, "osparams", default=None)

  ops = [
885
    opcodes.OpInstanceShutdown(instance_name=name),
886
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
887
888
889
890
                                osparams=osparams),
    ]

  if start:
891
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
892
893
894
895

  return ops


896
class R_2_instances_name_reinstall(baserlib.ResourceBase):
897
898
899
900
901
902
903
904
905
906
907
908
909
  """/2/instances/[instance_name]/reinstall resource.

  Implements an instance reinstall.

  """
  def POST(self):
    """Reinstall an instance.

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

    """
910
911
912
913
914
    if self.request_body:
      if self.queryargs:
        raise http.HttpBadRequest("Can't combine query and body parameters")

      body = self.request_body
915
    elif self.queryargs:
916
917
918
919
920
      # Legacy interface, do not modify/extend
      body = {
        "os": self._checkStringVariable("os"),
        "start": not self._checkIntVariable("nostartup"),
        }
921
922
    else:
      body = {}
923
924
925

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

926
    return self.SubmitJob(ops)
927
928


929
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
930
931
932
  """/2/instances/[instance_name]/replace-disks resource.

  """
933
934
935
  POST_OPCODE = opcodes.OpInstanceReplaceDisks

  def GetPostOpInput(self):
936
937
938
    """Replaces disks on an instance.

    """
939
940
941
942
    data = self.request_body.copy()
    static = {
      "instance_name": self.items[0],
      }
943

944
945
946
947
948
949
950
951
952
953
954
955
956
957
    # Parse disks
    try:
      raw_disks = data["disks"]
    except KeyError:
      pass
    else:
      if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
        # Backwards compatibility for strings of the format "1, 2, 3"
        try:
          data["disks"] = [int(part) for part in raw_disks.split(",")]
        except (TypeError, ValueError), err:
          raise http.HttpBadRequest("Invalid disk index passed: %s" % err)

    return (data, static)
958
959


960
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
961
962
963
  """/2/instances/[instance_name]/activate-disks resource.

  """
964
965
966
  PUT_OPCODE = opcodes.OpInstanceActivateDisks

  def GetPutOpInput(self):
967
968
969
970
971
    """Activate disks for an instance.

    The URI might contain ignore_size to ignore current recorded size.

    """
972
973
974
975
    return ({}, {
      "instance_name": self.items[0],
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
      })
976
977


978
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
979
980
981
  """/2/instances/[instance_name]/deactivate-disks resource.

  """
982
983
984
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks

  def GetPutOpInput(self):
985
986
987
    """Deactivate disks for an instance.

    """
988
989
990
    return ({}, {
      "instance_name": self.items[0],
      })
991
992


993
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
994
995
996
  """/2/instances/[instance_name]/prepare-export resource.

  """
997
  PUT_OPCODE = opcodes.OpBackupPrepare
998

999
1000
  def GetPutOpInput(self):
    """Prepares an export for an instance.
1001
1002

    """
1003
1004
1005
1006
    return ({}, {
      "instance_name": self.items[0],
      "mode": self._checkStringVariable("mode"),
      })
1007
1008


1009
class R_2_instances_name_export(baserlib.OpcodeResource):
1010
1011
1012
  """/2/instances/[instance_name]/export resource.

  """
1013
1014
1015
1016
  PUT_OPCODE = opcodes.OpBackupExport
  PUT_RENAME = {
    "destination": "target_node",
    }
1017

1018
1019
  def GetPutOpInput(self):
    """Exports an instance.
1020
1021

    """
1022
1023
1024
    return (self.request_body, {
      "instance_name": self.items[0],
      })
1025
1026


1027
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1028
1029
1030
  """/2/instances/[instance_name]/migrate resource.

  """
1031
  PUT_OPCODE = opcodes.OpInstanceMigrate
1032

1033
1034
  def GetPutOpInput(self):
    """Migrates an instance.
1035
1036

    """
1037
1038
1039
    return (self.request_body, {
      "instance_name": self.items[0],
      })
1040
1041


1042
class R_2_instances_name_failover(baserlib.OpcodeResource):
1043
1044
1045
  """/2/instances/[instance_name]/failover resource.

  """
1046
  PUT_OPCODE = opcodes.OpInstanceFailover
1047

1048
1049
  def GetPutOpInput(self):
    """Does a failover of an instance.
1050
1051

    """
1052
    return (self.request_body, {
1053
1054
1055
1056
      "instance_name": self.items[0],
      })


1057
class R_2_instances_name_rename(baserlib.OpcodeResource):
1058
1059
1060
  """/2/instances/[instance_name]/rename resource.

  """
1061
  PUT_OPCODE