rlib2.py 37.5 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
111
112
113
114
115
116
_NR_DRAINED = "drained"
_NR_MASTER_CANDIATE = "master-candidate"
_NR_MASTER = "master"
_NR_OFFLINE = "offline"
_NR_REGULAR = "regular"

_NR_MAP = {
117
118
119
120
121
  constants.NR_MASTER: _NR_MASTER,
  constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
  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
class R_2_nodes_name_role(baserlib.ResourceBase):
387
388
389
390
391
392
393
394
395
396
  """ /2/nodes/[node_name]/role resource.

  """
  def GET(self):
    """Returns the current node role.

    @return: Node role

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

    return _NR_MAP[result[0][0]]

  def PUT(self):
    """Sets the node role.

    @return: a job id

    """
409
    if not isinstance(self.request_body, basestring):
410
411
412
      raise http.HttpBadRequest("Invalid body contents, not a string")

    node_name = self.items[0]
413
    role = self.request_body
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434

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

    elif role == _NR_MASTER_CANDIATE:
      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)

435
    op = opcodes.OpNodeSetParams(node_name=node_name,
436
437
438
439
440
                                 master_candidate=candidate,
                                 offline=offline,
                                 drained=drained,
                                 force=bool(self.useForce()))

441
    return self.SubmitJob([op])
442
443


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

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

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

457
    return self.SubmitJob([op])
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.ResourceBase):
496
  """/2/nodes/[node_name]/storage resource.
497
498

  """
499
  # LUNodeQueryStorage acquires locks, hence restricting access to GET
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]

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

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

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

515
    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
516
517
                                    storage_type=storage_type,
                                    output_fields=output_fields.split(","))
518
    return self.SubmitJob([op])
519
520


521
class R_2_nodes_name_storage_modify(baserlib.ResourceBase):
522
  """/2/nodes/[node_name]/storage/modify resource.
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543

  """
  def PUT(self):
    node_name = self.items[0]

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

    name = self._checkStringVariable("name", None)
    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
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
545
546
547
                                     storage_type=storage_type,
                                     name=name,
                                     changes=changes)
548
    return self.SubmitJob([op])
549
550


551
class R_2_nodes_name_storage_repair(baserlib.ResourceBase):
552
  """/2/nodes/[node_name]/storage/repair resource.
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570

  """
  def PUT(self):
    node_name = self.items[0]

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

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

    op = opcodes.OpRepairNodeStorage(node_name=node_name,
                                     storage_type=storage_type,
                                     name=name)
571
    return self.SubmitJob([op])
572
573


574
575
576
def _ParseCreateGroupRequest(data, dry_run):
  """Parses a request for creating a node group.

Iustin Pop's avatar
Iustin Pop committed
577
  @rtype: L{opcodes.OpGroupAdd}
578
579
580
  @return: Group creation opcode

  """
581
582
583
  override = {
    "dry_run": dry_run,
    }
584

585
586
587
588
589
590
  rename = {
    "name": "group_name",
    }

  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
                             rename=rename)
591
592


593
class R_2_groups(baserlib.ResourceBase):
594
595
596
597
598
599
600
  """/2/groups resource.

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

    """
601
    client = self.GetClient()
602
603
604
605
606
607
608
609
610
611

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

612
613
614
615
616
617
618
  def POST(self):
    """Create a node group.

    @return: a job id

    """
    baserlib.CheckType(self.request_body, dict, "Body contents")
619
    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
620
    return self.SubmitJob([op])
621

622

623
class R_2_groups_name(baserlib.ResourceBase):
624
  """/2/groups/[group_name] resource.
625
626
627
628
629
630
631

  """
  def GET(self):
    """Send information about a node group.

    """
    group_name = self.items[0]
632
    client = self.GetClient()
633
634
635
636
637
638
639

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

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

640
641
642
643
  def DELETE(self):
    """Delete a node group.

    """
644
    op = opcodes.OpGroupRemove(group_name=self.items[0],
645
646
                               dry_run=bool(self.dryRun()))

647
    return self.SubmitJob([op])
648
649


650
651
652
def _ParseModifyGroupRequest(name, data):
  """Parses a request for modifying a node group.

653
  @rtype: L{opcodes.OpGroupSetParams}
654
655
656
  @return: Group modify opcode

  """
657
658
659
660
  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
    "group_name": name,
    })

661

662
class R_2_groups_name_modify(baserlib.ResourceBase):
663
664
665
666
667
668
669
670
671
672
673
674
675
  """/2/groups/[group_name]/modify resource.

  """
  def PUT(self):
    """Changes some parameters of node group.

    @return: a job id

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

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

676
    return self.SubmitJob([op])
677
678


679
680
681
682
683
684
685
686
687
688
def _ParseRenameGroupRequest(name, data, dry_run):
  """Parses a request for renaming a node group.

  @type name: string
  @param name: name of the node group to rename
  @type data: dict
  @param data: the body received by the rename request
  @type dry_run: bool
  @param dry_run: whether to perform a dry run

689
  @rtype: L{opcodes.OpGroupRename}
690
691
692
  @return: Node group rename opcode

  """
693
694
695
696
  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
    "group_name": name,
    "dry_run": dry_run,
    })
697
698


699
class R_2_groups_name_rename(baserlib.ResourceBase):
700
  """/2/groups/[group_name]/rename resource.
701
702
703
704
705
706
707
708
709
710
711

  """
  def PUT(self):
    """Changes the name of a node group.

    @return: a job id

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


715
class R_2_groups_name_assign_nodes(baserlib.ResourceBase):
716
  """/2/groups/[group_name]/assign-nodes resource.
717
718
719
720
721
722
723
724

  """
  def PUT(self):
    """Assigns nodes to a group.

    @return: a job id

    """
725
    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
726
727
728
729
730
      "group_name": self.items[0],
      "dry_run": self.dryRun(),
      "force": self.useForce(),
      })

731
    return self.SubmitJob([op])
732
733


734
735
736
def _ParseInstanceCreateRequestVersion1(data, dry_run):
  """Parses an instance creation request version 1.

737
  @rtype: L{opcodes.OpInstanceCreate}
738
739
740
  @return: Instance creation opcode

  """
741
742
743
  override = {
    "dry_run": dry_run,
    }
744

745
746
747
748
749
750
751
  rename = {
    "os": "os_type",
    "name": "instance_name",
    }

  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
                             rename=rename)
752
753


754
class R_2_instances(baserlib.ResourceBase):
755
756
757
758
759
760
761
  """/2/instances resource.

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

    """
762
    client = self.GetClient()
763

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

774
775
776
777
778
779
  def POST(self):
    """Create an instance.

    @return: a job id

    """
Luca Bigliardi's avatar
Luca Bigliardi committed
780
    if not isinstance(self.request_body, dict):
781
782
783
784
785
786
      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:
787
788
      raise http.HttpBadRequest("Instance creation request version 0 is no"
                                " longer supported")
789
    elif data_version == 1:
790
791
792
793
      data = self.request_body.copy()
      # Remove "__version__"
      data.pop(_REQ_DATA_VERSION, None)
      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
794
795
    else:
      raise http.HttpBadRequest("Unsupported request data version %s" %
796
                                data_version)
797

798
    return self.SubmitJob([op])
799

800

801
class R_2_instances_name(baserlib.ResourceBase):
802
  """/2/instances/[instance_name] resource.
803
804

  """
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
805
806
807
808
  def GET(self):
    """Send information about an instance.

    """
809
    client = self.GetClient()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
810
    instance_name = self.items[0]
811
812
813
814
815

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

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

Iustin Pop's avatar
Iustin Pop committed
819
820
821
822
  def DELETE(self):
    """Delete an instance.

    """
823
    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
Iustin Pop's avatar
Iustin Pop committed
824
825
                                  ignore_failures=False,
                                  dry_run=bool(self.dryRun()))
826
    return self.SubmitJob([op])
Iustin Pop's avatar
Iustin Pop committed
827

828

829
class R_2_instances_name_info(baserlib.ResourceBase):
830
831
832
833
834
835
836
837
838
839
  """/2/instances/[instance_name]/info resource.

  """
  def GET(self):
    """Request detailed instance information.

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

840
    op = opcodes.OpInstanceQueryData(instances=[instance_name],
841
                                     static=static)
842
    return self.SubmitJob([op])
843
844


845
class R_2_instances_name_reboot(baserlib.ResourceBase):
846
847
848
849
850
  """/2/instances/[instance_name]/reboot resource.

  Implements an instance reboot.

  """
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
851
  def POST(self):
852
853
    """Reboot an instance.

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

857
858
    """
    instance_name = self.items[0]
Iustin Pop's avatar
Iustin Pop committed
859
    reboot_type = self.queryargs.get("type",
860
                                     [constants.INSTANCE_REBOOT_HARD])[0]
Iustin Pop's avatar
Iustin Pop committed
861
    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
862
    op = opcodes.OpInstanceReboot(instance_name=instance_name,
Iustin Pop's avatar
Iustin Pop committed
863
                                  reboot_type=reboot_type,
Iustin Pop's avatar
Iustin Pop committed
864
865
                                  ignore_secondaries=ignore_secondaries,
                                  dry_run=bool(self.dryRun()))
866

867
    return self.SubmitJob([op])
868
869


870
class R_2_instances_name_startup(baserlib.ResourceBase):
871
872
873
874
875
  """/2/instances/[instance_name]/startup resource.

  Implements an instance startup.

  """
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
876
  def PUT(self):
877
878
    """Startup an instance.

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

    """
    instance_name = self.items[0]
Iustin Pop's avatar
Iustin Pop committed
884
885
    force_startup = bool(self._checkIntVariable("force"))
    no_remember = bool(self._checkIntVariable("no_remember"))
886
    op = opcodes.OpInstanceStartup(instance_name=instance_name,
Iustin Pop's avatar
Iustin Pop committed
887
                                   force=force_startup,
888
889
                                   dry_run=bool(self.dryRun()),
                                   no_remember=no_remember)
890

891
    return self.SubmitJob([op])
892
893


894
def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
895
896
897
898
899
900
901
902
903
  """Parses a request for an instance shutdown.

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

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


908
class R_2_instances_name_shutdown(baserlib.ResourceBase):
909
910
911
912
913
  """/2/instances/[instance_name]/shutdown resource.

  Implements an instance shutdown.

  """
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
914
  def PUT(self):
915
916
    """Shutdown an instance.

917
918
    @return: a job id

919
    """
920
921
    baserlib.CheckType(self.request_body, dict, "Body contents")

Iustin Pop's avatar
Iustin Pop committed
922
    no_remember = bool(self._checkIntVariable("no_remember"))
923
    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
924
                                       bool(self.dryRun()), no_remember)
925

926
    return self.SubmitJob([op])
927
928


929
930
931
932
933
934
935
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")

936
  ostype = baserlib.CheckParameter(data, "os", default=None)
937
938
939
940
941
  start = baserlib.CheckParameter(data, "start", exptype=bool,
                                  default=True)
  osparams = baserlib.CheckParameter(data, "osparams", default=None)

  ops = [
942
    opcodes.OpInstanceShutdown(instance_name=name),
943
    opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
944
945
946
947
                                osparams=osparams),
    ]

  if start:
948
    ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
949
950
951
952

  return ops


953
class R_2_instances_name_reinstall(baserlib.ResourceBase):
954
955
956
957
958
959
960
961
962
963
964
965
966
  """/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.

    """
967
968
969
970
971
    if self.request_body:
      if self.queryargs:
        raise http.HttpBadRequest("Can't combine query and body parameters")

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

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

983
    return self.SubmitJob(ops)
984
985


986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
def _ParseInstanceReplaceDisksRequest(name, data):
  """Parses a request for an instance export.

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

  """
  override = {
    "instance_name": name,
    }

  # 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" % str(err))

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


1013
class R_2_instances_name_replace_disks(baserlib.ResourceBase):
1014
1015
1016
1017
1018
1019
1020
  """/2/instances/[instance_name]/replace-disks resource.

  """
  def POST(self):
    """Replaces disks on an instance.

    """
1021
    op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1022

1023
    return self.SubmitJob([op])
1024
1025


1026
class R_2_instances_name_activate_disks(baserlib.ResourceBase):
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
  """/2/instances/[instance_name]/activate-disks resource.

  """
  def PUT(self):
    """Activate disks for an instance.

    The URI might contain ignore_size to ignore current recorded size.

    """
    instance_name = self.items[0]
Iustin Pop's avatar
Iustin Pop committed
1037
    ignore_size = bool(self._checkIntVariable("ignore_size"))
1038

1039
    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1040
1041
                                         ignore_size=ignore_size)

1042
    return self.SubmitJob([op])
1043
1044


1045
class R_2_instances_name_deactivate_disks(baserlib.ResourceBase):
1046
1047
1048
1049
1050
1051
1052
1053
1054
  """/2/instances/[instance_name]/deactivate-disks resource.

  """
  def PUT(self):
    """Deactivate disks for an instance.

    """
    instance_name = self.items[0]

1055
    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1056

1057
    return self.SubmitJob([op])
1058
1059


1060
class R_2_instances_name_prepare_export(baserlib.ResourceBase):
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
  """/2/instances/[instance_name]/prepare-export resource.

  """
  def PUT(self):
    """Prepares an export for an instance.

    @return: a job id

    """
    instance_name = self.items[0]
    mode = self._checkStringVariable("mode")

1073
    op = opcodes.OpBackupPrepare(instance_name=instance_name,
1074
1075
                                 mode=mode)

1076
    return self.SubmitJob([op])
1077
1078
1079
1080
1081


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

1082
  @rtype: L{opcodes.OpBackupExport}
1083
1084
1085
  @return: Instance export opcode

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

  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
    "instance_name": name,
    })
1095
1096


1097
class R_2_instances_name_export(baserlib.ResourceBase):
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
  """/2/instances/[instance_name]/export resource.

  """
  def PUT(self):
    """Exports an instance.

    @return: a job id

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

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

1112
    return self.SubmitJob([op])
1113
1114


1115
1116
1117
def _ParseMigrateInstanceRequest(name, data):
  """Parses a request for an instance migration.

1118
  @rtype: L{opcodes.OpInstanceMigrate}
1119
1120
1121
  @return: Instance migration opcode

  """
1122
1123
1124
  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
    "instance_name": name,
    })
1125
1126


1127
class R_2_instances_name_migrate(baserlib.ResourceBase):
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
  """/2/instances/[instance_name]/migrate resource.

  """
  def PUT(self):
    """Migrates an instance.

    @return: a job id

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

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