rlib2.py 33.7 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
"""

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

# 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
165
166
167
168
169
class R_2(R_root):
  """/2 resource.

  """


170
class R_version(baserlib.ResourceBase):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
171
172
173
174
175
176
  """/version resource.

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

  """
177
178
  @staticmethod
  def GET():
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
179
180
181
182
183
184
    """Returns the remote API version.

    """
    return constants.RAPI_VERSION


185
class R_2_info(baserlib.OpcodeResource):
186
  """/2/info resource.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
187
188

  """
189
190
  GET_OPCODE = opcodes.OpClusterQuery

191
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
192
193
194
    """Returns cluster information.

    """
195
    client = self.GetClient()
196
    return client.QueryClusterInfo()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
197
198


199
class R_2_features(baserlib.ResourceBase):
200
201
202
203
204
205
206
207
  """/2/features resource.

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

    """
208
    return list(ALL_FEATURES)
209
210


211
class R_2_os(baserlib.OpcodeResource):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
212
213
214
  """/2/os resource.

  """
215
216
  GET_OPCODE = opcodes.OpOsDiagnose

217
  def GET(self):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
218
219
220
221
222
223
224
    """Return a list of all OSes.

    Can return error 500 in case of a problem.

    Example: ["debian-etch"]

    """
225
    cl = self.GetClient()
226
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
227
    job_id = self.SubmitJob([op], cl=cl)
Iustin Pop's avatar
Iustin Pop committed
228
229
230
    # 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
231
232

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

235
    os_names = []
236
237
    for (name, variants) in diagnose_data:
      os_names.extend(cli.CalculateOSNames(name, variants))
238
239

    return os_names
240

241

242
class R_2_redist_config(baserlib.OpcodeResource):
243
244
245
  """/2/redistribute-config resource.

  """
246
  PUT_OPCODE = opcodes.OpClusterRedistConf
247
248


249
class R_2_cluster_modify(baserlib.OpcodeResource):
250
251
252
  """/2/modify resource.

  """
253
  PUT_OPCODE = opcodes.OpClusterSetParams
254
255


256
class R_2_jobs(baserlib.ResourceBase):
257
258
259
  """/2/jobs resource.

  """
260
  def GET(self):
261
262
    """Returns a dictionary of jobs.

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

265
    """
266
    client = self.GetClient()
267
268

    if self.useBulk():
269
270
      bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
      return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
271
272
273
274
    else:
      jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
      return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
                                   uri_fields=("id", "uri"))
275
276


277
class R_2_jobs_id(baserlib.ResourceBase):
278
279
280
281
282
283
  """/2/jobs/[job_id] resource.

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

Iustin Pop's avatar
Iustin Pop committed
284
285
286
287
288
289
290
291
    @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
292

293
294
    """
    job_id = self.items[0]
295
    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
Iustin Pop's avatar
Iustin Pop committed
296
297
    if result is None:
      raise http.HttpNotFound()
298
    return baserlib.MapFields(J_FIELDS, result)
299

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
300
301
302
303
304
  def DELETE(self):
    """Cancel not-yet-started job.

    """
    job_id = self.items[0]
305
    result = self.GetClient().CancelJob(job_id)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
306
307
    return result

308

309
class R_2_jobs_id_wait(baserlib.ResourceBase):
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
  """/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")

339
    client = self.GetClient()
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
    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,
      }


358
class R_2_nodes(baserlib.OpcodeResource):
359
360
361
  """/2/nodes resource.

  """
362
363
  GET_OPCODE = opcodes.OpNodeQuery

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

367
    """
368
    client = self.GetClient()
Iustin Pop's avatar
Iustin Pop committed
369

370
    if self.useBulk():
371
      bulkdata = client.QueryNodes([], N_FIELDS, False)
372
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
373
374
375
376
377
    else:
      nodesdata = client.QueryNodes([], ["name"], False)
      nodeslist = [row[0] for row in nodesdata]
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
                                   uri_fields=("id", "uri"))
378
379


380
class R_2_nodes_name(baserlib.OpcodeResource):
381
  """/2/nodes/[node_name] resource.
382
383

  """
384
385
  GET_OPCODE = opcodes.OpNodeQuery

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
386
387
388
389
390
  def GET(self):
    """Send information about a node.

    """
    node_name = self.items[0]
391
    client = self.GetClient()
392
393
394
395

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

    return baserlib.MapFields(N_FIELDS, result[0])
398
399


400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
  """/2/nodes/[node_name]/powercycle resource.

  """
  POST_OPCODE = opcodes.OpNodePowercycle

  def GetPostOpInput(self):
    """Tries to powercycle a node.

    """
    return (self.request_body, {
      "node_name": self.items[0],
      "force": self.useForce(),
      })


416
417
class R_2_nodes_name_role(baserlib.OpcodeResource):
  """/2/nodes/[node_name]/role resource.
418
419

  """
420
421
  PUT_OPCODE = opcodes.OpNodeSetParams

422
423
424
425
426
427
428
  def GET(self):
    """Returns the current node role.

    @return: Node role

    """
    node_name = self.items[0]
429
    client = self.GetClient()
430
431
432
433
434
    result = client.QueryNodes(names=[node_name], fields=["role"],
                               use_locking=self.useLocking())

    return _NR_MAP[result[0][0]]

435
  def GetPutOpInput(self):
436
437
438
    """Sets the node role.

    """
439
    baserlib.CheckType(self.request_body, basestring, "Body contents")
440

441
    role = self.request_body
442
443
444
445
446
447

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

448
    elif role == _NR_MASTER_CANDIDATE:
449
450
451
452
453
454
455
456
457
458
459
460
461
462
      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)

463
    assert len(self.items) == 1
464

465
466
467
468
469
470
    return ({}, {
      "node_name": self.items[0],
      "master_candidate": candidate,
      "offline": offline,
      "drained": drained,
      "force": self.useForce(),
Guido Trotter's avatar
Guido Trotter committed
471
      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
472
      })
473
474


475
class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
476
477
478
  """/2/nodes/[node_name]/evacuate resource.

  """
479
480
481
  POST_OPCODE = opcodes.OpNodeEvacuate

  def GetPostOpInput(self):
482
    """Evacuate all instances off a node.
483
484

    """
485
    return (self.request_body, {
486
487
488
      "node_name": self.items[0],
      "dry_run": self.dryRun(),
      })
489

490

491
class R_2_nodes_name_migrate(baserlib.OpcodeResource):
492
  """/2/nodes/[node_name]/migrate resource.
493
494

  """
495
496
497
  POST_OPCODE = opcodes.OpNodeMigrate

  def GetPostOpInput(self):
498
499
500
    """Migrate all primary instances from a node.

    """
501
502
503
504
505
506
507
508
509
510
511
    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
512
      else:
513
514
515
516
517
        mode = self._checkStringVariable("mode", default=None)

      data = {
        "mode": mode,
        }
518
    else:
519
      data = self.request_body
520

521
522
    return (data, {
      "node_name": self.items[0],
523
      })
524
525


Guido Trotter's avatar
Guido Trotter committed
526
class R_2_nodes_name_modify(baserlib.OpcodeResource):
527
528
529
  """/2/nodes/[node_name]/modify resource.

  """
Guido Trotter's avatar
Guido Trotter committed
530
  PUT_OPCODE = opcodes.OpNodeSetParams
531

Guido Trotter's avatar
Guido Trotter committed
532
533
  def GetPutOpInput(self):
    """Changes parameters of a node.
534
535

    """
Guido Trotter's avatar
Guido Trotter committed
536
    assert len(self.items) == 1
537

Guido Trotter's avatar
Guido Trotter committed
538
    return (self.request_body, {
539
      "node_name": self.items[0],
540
541
542
      })


543
class R_2_nodes_name_storage(baserlib.OpcodeResource):
544
  """/2/nodes/[node_name]/storage resource.
545
546

  """
547
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
548
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
549
  GET_OPCODE = opcodes.OpNodeQueryStorage
550

551
552
  def GetGetOpInput(self):
    """List storage available on a node.
553

554
    """
555
556
    storage_type = self._checkStringVariable("storage_type", None)
    output_fields = self._checkStringVariable("output_fields", None)
557

558
559
560
561
    if not output_fields:
      raise http.HttpBadRequest("Missing the required 'output_fields'"
                                " parameter")

562
563
564
565
566
    return ({}, {
      "nodes": [self.items[0]],
      "storage_type": storage_type,
      "output_fields": output_fields.split(","),
      })
567
568


569
class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
570
  """/2/nodes/[node_name]/storage/modify resource.
571
572

  """
573
  PUT_OPCODE = opcodes.OpNodeModifyStorage
574

575
576
  def GetPutOpInput(self):
    """Modifies a storage volume on a node.
577

578
579
    """
    storage_type = self._checkStringVariable("storage_type", None)
580
    name = self._checkStringVariable("name", None)
581

582
583
584
585
586
587
588
589
590
591
    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))

592
593
594
595
596
597
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      "changes": changes,
      })
598
599


600
class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
601
  """/2/nodes/[node_name]/storage/repair resource.
602
603

  """
604
  PUT_OPCODE = opcodes.OpRepairNodeStorage
605

606
607
  def GetPutOpInput(self):
    """Repairs a storage volume on a node.
608

609
610
    """
    storage_type = self._checkStringVariable("storage_type", None)
611
612
613
614
615
    name = self._checkStringVariable("name", None)
    if not name:
      raise http.HttpBadRequest("Missing the required 'name'"
                                " parameter")

616
617
618
619
620
    return ({}, {
      "node_name": self.items[0],
      "storage_type": storage_type,
      "name": name,
      })
621
622


623
624
class R_2_groups(baserlib.OpcodeResource):
  """/2/groups resource.
625
626

  """
627
  GET_OPCODE = opcodes.OpGroupQuery
628
629
  POST_OPCODE = opcodes.OpGroupAdd
  POST_RENAME = {
630
631
632
    "name": "group_name",
    }

633
634
  def GetPostOpInput(self):
    """Create a node group.
635

636
637
638
639
640
    """
    assert not self.items
    return (self.request_body, {
      "dry_run": self.dryRun(),
      })
641
642
643
644
645

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

    """
646
    client = self.GetClient()
647
648
649
650
651
652
653
654
655
656
657

    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"))


658
class R_2_groups_name(baserlib.OpcodeResource):
659
  """/2/groups/[group_name] resource.
660
661

  """
662
663
  DELETE_OPCODE = opcodes.OpGroupRemove

664
665
666
667
668
  def GET(self):
    """Send information about a node group.

    """
    group_name = self.items[0]
669
    client = self.GetClient()
670
671
672
673
674
675
676

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

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

677
  def GetDeleteOpInput(self):
678
679
680
    """Delete a node group.

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


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

  """
692
  PUT_OPCODE = opcodes.OpGroupSetParams
693

694
695
  def GetPutOpInput(self):
    """Changes some parameters of node group.
696
697

    """
698
699
700
701
    assert self.items
    return (self.request_body, {
      "group_name": self.items[0],
      })
702
703


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

  """
708
  PUT_OPCODE = opcodes.OpGroupRename
709

710
711
  def GetPutOpInput(self):
    """Changes the name of a node group.
712
713

    """
714
715
716
717
718
    assert len(self.items) == 1
    return (self.request_body, {
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      })
719
720


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

  """
725
  PUT_OPCODE = opcodes.OpGroupAssignNodes
726

727
728
  def GetPutOpInput(self):
    """Assigns nodes to a group.
729
730

    """
731
732
    assert len(self.items) == 1
    return (self.request_body, {
733
734
735
736
737
738
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      "force": self.useForce(),
      })


739
740
class R_2_instances(baserlib.OpcodeResource):
  """/2/instances resource.
741
742

  """
743
  GET_OPCODE = opcodes.OpInstanceQuery
744
745
  POST_OPCODE = opcodes.OpInstanceCreate
  POST_RENAME = {
746
747
748
749
    "os": "os_type",
    "name": "instance_name",
    }

750
751
752
753
  def GET(self):
    """Returns a list of all available instances.

    """
754
    client = self.GetClient()
755

756
757
758
    use_locking = self.useLocking()
    if self.useBulk():
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
759
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
760
    else:
761
      instancesdata = client.QueryInstances([], ["name"], use_locking)
762
      instanceslist = [row[0] for row in instancesdata]
763
764
765
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
                                   uri_fields=("id", "uri"))

766
  def GetPostOpInput(self):
767
768
769
770
771
    """Create an instance.

    @return: a job id

    """
772
    baserlib.CheckType(self.request_body, dict, "Body contents")
773
774
775
776
777

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

    if data_version == 0:
778
779
      raise http.HttpBadRequest("Instance creation request version 0 is no"
                                " longer supported")
780
    elif data_version != 1:
781
      raise http.HttpBadRequest("Unsupported request data version %s" %
782
                                data_version)
783

784
785
786
787
788
789
790
    data = self.request_body.copy()
    # Remove "__version__"
    data.pop(_REQ_DATA_VERSION, None)

    return (data, {
      "dry_run": self.dryRun(),
      })
791

792

793
class R_2_instances_name(baserlib.OpcodeResource):
794
  """/2/instances/[instance_name] resource.
795
796

  """
797
  GET_OPCODE = opcodes.OpInstanceQuery
798
799
  DELETE_OPCODE = opcodes.OpInstanceRemove

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
800
801
802
803
  def GET(self):
    """Send information about an instance.

    """
804
    client = self.GetClient()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
805
    instance_name = self.items[0]
806
807
808
809
810

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

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

814
  def GetDeleteOpInput(self):
Iustin Pop's avatar
Iustin Pop committed
815
816
817
    """Delete an instance.

    """
818
819
820
821
822
823
    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
824

825

826
class R_2_instances_name_info(baserlib.OpcodeResource):
827
828
829
  """/2/instances/[instance_name]/info resource.

  """
830
831
832
  GET_OPCODE = opcodes.OpInstanceQueryData

  def GetGetOpInput(self):
833
834
835
    """Request detailed instance information.

    """
836
837
838
839
840
    assert len(self.items) == 1
    return ({}, {
      "instances": [self.items[0]],
      "static": bool(self._checkIntVariable("static", default=0)),
      })
841
842


843
class R_2_instances_name_reboot(baserlib.OpcodeResource):
844
845
846
847
848
  """/2/instances/[instance_name]/reboot resource.

  Implements an instance reboot.

  """
849
850
851
  POST_OPCODE = opcodes.OpInstanceReboot

  def GetPostOpInput(self):
852
853
    """Reboot an instance.

854
855
856
    The URI takes type=[hard|soft|full] and
    ignore_secondaries=[False|True] parameters.

857
    """
858
859
860
861
862
863
864
    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(),
      })
865
866


867
class R_2_instances_name_startup(baserlib.OpcodeResource):
868
869
870
871
872
  """/2/instances/[instance_name]/startup resource.

  Implements an instance startup.

  """
873
874
875
  PUT_OPCODE = opcodes.OpInstanceStartup

  def GetPutOpInput(self):
876
877
    """Startup an instance.

Iustin Pop's avatar
Iustin Pop committed
878
879
    The URI takes force=[False|True] parameter to start the instance
    if even if secondary disks are failing.
880
881

    """
882
883
884
885
886
887
    return ({}, {
      "instance_name": self.items[0],
      "force": self.useForce(),
      "dry_run": self.dryRun(),
      "no_remember": bool(self._checkIntVariable("no_remember")),
      })
888
889


890
class R_2_instances_name_shutdown(baserlib.OpcodeResource):
891
892
893
894
895
  """/2/instances/[instance_name]/shutdown resource.

  Implements an instance shutdown.

  """
896
  PUT_OPCODE = opcodes.OpInstanceShutdown
897

898
899
  def GetPutOpInput(self):
    """Shutdown an instance.
900

901
    """
902
903
904
905
906
    return (self.request_body, {
      "instance_name": self.items[0],
      "no_remember": bool(self._checkIntVariable("no_remember")),
      "dry_run": self.dryRun(),
      })
907
908


909
910
911
912
913
914
915
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")

916
  ostype = baserlib.CheckParameter(data, "os", default=None)
917
918
919
920
921
  start = baserlib.CheckParameter(data, "start", exptype=bool,
                                  default=True)
  osparams = baserlib.CheckParameter(data, "osparams", default=None)

  ops = [
922
    opcodes.OpInstanceShutdown(instance_name=name),
923
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
924
925
926
927
                                osparams=osparams),
    ]

  if start:
928
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
929
930
931
932

  return ops


933
class R_2_instances_name_reinstall(baserlib.OpcodeResource):
934
935
936
937
938
  """/2/instances/[instance_name]/reinstall resource.

  Implements an instance reinstall.

  """
939
940
  POST_OPCODE = opcodes.OpInstanceReinstall

941
942
943
944
945
946
947
948
  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.

    """
949
950
951
952
953
    if self.request_body:
      if self.queryargs:
        raise http.HttpBadRequest("Can't combine query and body parameters")

      body = self.request_body
954
    elif self.queryargs:
955
956
957
958
959
      # Legacy interface, do not modify/extend
      body = {
        "os": self._checkStringVariable("os"),
        "start": not self._checkIntVariable("nostartup"),
        }
960
961
    else:
      body = {}
962
963
964

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

965
    return self.SubmitJob(ops)
966
967


968
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
969
970
971
  """/2/instances/[instance_name]/replace-disks resource.

  """
972
973
974
  POST_OPCODE = opcodes.OpInstanceReplaceDisks

  def GetPostOpInput(self):
975
976
977
    """Replaces disks on an instance.

    """
978
979
980
981
    data = self.request_body.copy()
    static = {
      "instance_name": self.items[0],
      }
982

983
984
985
986
987
988
    # Parse disks
    try:
      raw_disks = data["disks"]
    except KeyError:
      pass
    else:
Andrea Spadaccini's avatar
Andrea Spadaccini committed
989
      if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
990
991
992
993
994
995
996
        # 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)
997
998


999
class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1000
1001
1002
  """/2/instances/[instance_name]/activate-disks resource.

  """
1003
1004
1005
  PUT_OPCODE = opcodes.OpInstanceActivateDisks

  def GetPutOpInput(self):
1006
1007
1008
1009
1010
    """Activate disks for an instance.

    The URI might contain ignore_size to ignore current recorded size.

    """
1011
1012
1013
1014
    return ({}, {
      "instance_name": self.items[0],
      "ignore_size": bool(self._checkIntVariable("ignore_size")),
      })
1015
1016


1017
class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1018
1019
1020
  """/2/instances/[instance_name]/deactivate-disks resource.

  """
1021
1022
1023
  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks

  def GetPutOpInput(self):
1024
1025
1026
    """Deactivate disks for an instance.

    """
1027
1028
1029
    return ({}, {
      "instance_name": self.items[0],
      })
1030
1031


1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
  """/2/instances/[instance_name]/recreate-disks resource.

  """
  POST_OPCODE = opcodes.OpInstanceRecreateDisks

  def GetPostOpInput(self):
    """Recreate disks for an instance.

    """
    return ({}, {
      "instance_name": self.items[0],
      })


1047
class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1048
1049
1050
  """/2/instances/[instance_name]/prepare-export resource.

  """
1051
  PUT_OPCODE = opcodes.OpBackupPrepare
1052

1053
1054
  def GetPutOpInput(self):
    """Prepares an export for an instance.
1055
1056

    """
1057
1058
1059
1060
    return ({}, {
      "instance_name": self.items[0],
      "mode": self._checkStringVariable("mode"),
      })
1061
1062


1063
class R_2_instances_name_export(baserlib.OpcodeResource):
1064
1065
1066
  """/2/instances/[instance_name]/export resource.

  """
1067
1068
1069
1070
  PUT_OPCODE = opcodes.OpBackupExport
  PUT_RENAME = {
    "destination": "target_node",
    }
1071

1072
1073
  def GetPutOpInput(self):
    """Exports an instance.
1074
1075

    """
1076
1077
1078
    return (self.request_body, {
      "instance_name": self.items[0],
      })
1079
1080


1081
class R_2_instances_name_migrate(baserlib.OpcodeResource):
1082
1083
1084
  """/2/instances/[instance_name]/migrate resource.

  """
1085
  PUT_OPCODE = opcodes.OpInstanceMigrate
1086

1087
1088
  def GetPutOpInput(self):
    """Migrates an instance.
1089
1090

    """
1091
1092
1093
    return (self.request_body, {
      "instance_name": self.items[0],
      })