Replace single- with double-quotes
[ganeti-local] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API resource implementations.
23
24 PUT or POST?
25 ============
26
27 According to RFC2616 the main difference between PUT and POST is that
28 POST can create new resources but PUT can only create the resource the
29 URI was pointing to on the PUT request.
30
31 In the context of this module POST on ``/2/instances`` to change an existing
32 entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
33 new instance) with a name specified in the request.
34
35 Quoting from RFC2616, section 9.6::
36
37   The fundamental difference between the POST and PUT requests is reflected in
38   the different meaning of the Request-URI. The URI in a POST request
39   identifies the resource that will handle the enclosed entity. That resource
40   might be a data-accepting process, a gateway to some other protocol, or a
41   separate entity that accepts annotations. In contrast, the URI in a PUT
42   request identifies the entity enclosed with the request -- the user agent
43   knows what URI is intended and the server MUST NOT attempt to apply the
44   request to some other resource. If the server desires that the request be
45   applied to a different URI, it MUST send a 301 (Moved Permanently) response;
46   the user agent MAY then make its own decision regarding whether or not to
47   redirect the request.
48
49 So when adding new methods, if they are operating on the URI entity itself,
50 PUT should be prefered over POST.
51
52 """
53
54 # pylint: disable=C0103
55
56 # C0103: Invalid name, since the R_* names are not conforming
57
58 from ganeti import opcodes
59 from ganeti import http
60 from ganeti import constants
61 from ganeti import cli
62 from ganeti import rapi
63 from ganeti import ht
64 from ganeti import compat
65 from ganeti import ssconf
66 from ganeti.rapi import baserlib
67
68
69 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
70 I_FIELDS = ["name", "admin_state", "os",
71             "pnode", "snodes",
72             "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",
79             ] + _COMMON_FIELDS
80
81 N_FIELDS = ["name", "offline", "master_candidate", "drained",
82             "dtotal", "dfree",
83             "mtotal", "mnode", "mfree",
84             "pinst_cnt", "sinst_cnt",
85             "ctotal", "cnodes", "csockets",
86             "pip", "sip", "role",
87             "pinst_list", "sinst_list",
88             "master_capable", "vm_capable",
89             "ndparams",
90             "group.uuid",
91             ] + _COMMON_FIELDS
92
93 G_FIELDS = [
94   "alloc_policy",
95   "name",
96   "node_cnt",
97   "node_list",
98   "ipolicy",
99   ] + _COMMON_FIELDS
100
101 J_FIELDS_BULK = [
102   "id", "ops", "status", "summary",
103   "opstatus",
104   "received_ts", "start_ts", "end_ts",
105   ]
106
107 J_FIELDS = J_FIELDS_BULK + [
108   "oplog",
109   "opresult",
110   ]
111
112 _NR_DRAINED = "drained"
113 _NR_MASTER_CANDIDATE = "master-candidate"
114 _NR_MASTER = "master"
115 _NR_OFFLINE = "offline"
116 _NR_REGULAR = "regular"
117
118 _NR_MAP = {
119   constants.NR_MASTER: _NR_MASTER,
120   constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
121   constants.NR_DRAINED: _NR_DRAINED,
122   constants.NR_OFFLINE: _NR_OFFLINE,
123   constants.NR_REGULAR: _NR_REGULAR,
124   }
125
126 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
127
128 # Request data version field
129 _REQ_DATA_VERSION = "__version__"
130
131 # Feature string for instance creation request data version 1
132 _INST_CREATE_REQV1 = "instance-create-reqv1"
133
134 # Feature string for instance reinstall request version 1
135 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
136
137 # Feature string for node migration version 1
138 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
139
140 # Feature string for node evacuation with LU-generated jobs
141 _NODE_EVAC_RES1 = "node-evac-res1"
142
143 ALL_FEATURES = frozenset([
144   _INST_CREATE_REQV1,
145   _INST_REINSTALL_REQV1,
146   _NODE_MIGRATE_REQV1,
147   _NODE_EVAC_RES1,
148   ])
149
150 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
151 _WFJC_TIMEOUT = 10
152
153
154 class R_root(baserlib.ResourceBase):
155   """/ resource.
156
157   """
158   @staticmethod
159   def GET():
160     """Supported for legacy reasons.
161
162     """
163     return None
164
165
166 class R_2(R_root):
167   """/2 resource.
168
169   """
170
171
172 class R_version(baserlib.ResourceBase):
173   """/version resource.
174
175   This resource should be used to determine the remote API version and
176   to adapt clients accordingly.
177
178   """
179   @staticmethod
180   def GET():
181     """Returns the remote API version.
182
183     """
184     return constants.RAPI_VERSION
185
186
187 class R_2_info(baserlib.OpcodeResource):
188   """/2/info resource.
189
190   """
191   GET_OPCODE = opcodes.OpClusterQuery
192
193   def GET(self):
194     """Returns cluster information.
195
196     """
197     client = self.GetClient()
198     return client.QueryClusterInfo()
199
200
201 class R_2_features(baserlib.ResourceBase):
202   """/2/features resource.
203
204   """
205   @staticmethod
206   def GET():
207     """Returns list of optional RAPI features implemented.
208
209     """
210     return list(ALL_FEATURES)
211
212
213 class R_2_os(baserlib.OpcodeResource):
214   """/2/os resource.
215
216   """
217   GET_OPCODE = opcodes.OpOsDiagnose
218
219   def GET(self):
220     """Return a list of all OSes.
221
222     Can return error 500 in case of a problem.
223
224     Example: ["debian-etch"]
225
226     """
227     cl = self.GetClient()
228     op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
229     job_id = self.SubmitJob([op], cl=cl)
230     # we use custom feedback function, instead of print we log the status
231     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
232     diagnose_data = result[0]
233
234     if not isinstance(diagnose_data, list):
235       raise http.HttpBadGateway(message="Can't get OS list")
236
237     os_names = []
238     for (name, variants) in diagnose_data:
239       os_names.extend(cli.CalculateOSNames(name, variants))
240
241     return os_names
242
243
244 class R_2_redist_config(baserlib.OpcodeResource):
245   """/2/redistribute-config resource.
246
247   """
248   PUT_OPCODE = opcodes.OpClusterRedistConf
249
250
251 class R_2_cluster_modify(baserlib.OpcodeResource):
252   """/2/modify resource.
253
254   """
255   PUT_OPCODE = opcodes.OpClusterSetParams
256
257
258 class R_2_jobs(baserlib.ResourceBase):
259   """/2/jobs resource.
260
261   """
262   def GET(self):
263     """Returns a dictionary of jobs.
264
265     @return: a dictionary with jobs id and uri.
266
267     """
268     client = self.GetClient()
269
270     if self.useBulk():
271       bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
272       return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
273     else:
274       jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
275       return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
276                                    uri_fields=("id", "uri"))
277
278
279 class R_2_jobs_id(baserlib.ResourceBase):
280   """/2/jobs/[job_id] resource.
281
282   """
283   def GET(self):
284     """Returns a job status.
285
286     @return: a dictionary with job parameters.
287         The result includes:
288             - id: job ID as a number
289             - status: current job status as a string
290             - ops: involved OpCodes as a list of dictionaries for each
291               opcodes in the job
292             - opstatus: OpCodes status as a list
293             - opresult: OpCodes results as a list of lists
294
295     """
296     job_id = self.items[0]
297     result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
298     if result is None:
299       raise http.HttpNotFound()
300     return baserlib.MapFields(J_FIELDS, result)
301
302   def DELETE(self):
303     """Cancel not-yet-started job.
304
305     """
306     job_id = self.items[0]
307     result = self.GetClient().CancelJob(job_id)
308     return result
309
310
311 class R_2_jobs_id_wait(baserlib.ResourceBase):
312   """/2/jobs/[job_id]/wait resource.
313
314   """
315   # WaitForJobChange provides access to sensitive information and blocks
316   # machine resources (it's a blocking RAPI call), hence restricting access.
317   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
318
319   def GET(self):
320     """Waits for job changes.
321
322     """
323     job_id = self.items[0]
324
325     fields = self.getBodyParameter("fields")
326     prev_job_info = self.getBodyParameter("previous_job_info", None)
327     prev_log_serial = self.getBodyParameter("previous_log_serial", None)
328
329     if not isinstance(fields, list):
330       raise http.HttpBadRequest("The 'fields' parameter should be a list")
331
332     if not (prev_job_info is None or isinstance(prev_job_info, list)):
333       raise http.HttpBadRequest("The 'previous_job_info' parameter should"
334                                 " be a list")
335
336     if not (prev_log_serial is None or
337             isinstance(prev_log_serial, (int, long))):
338       raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
339                                 " be a number")
340
341     client = self.GetClient()
342     result = client.WaitForJobChangeOnce(job_id, fields,
343                                          prev_job_info, prev_log_serial,
344                                          timeout=_WFJC_TIMEOUT)
345     if not result:
346       raise http.HttpNotFound()
347
348     if result == constants.JOB_NOTCHANGED:
349       # No changes
350       return None
351
352     (job_info, log_entries) = result
353
354     return {
355       "job_info": job_info,
356       "log_entries": log_entries,
357       }
358
359
360 class R_2_nodes(baserlib.OpcodeResource):
361   """/2/nodes resource.
362
363   """
364   GET_OPCODE = opcodes.OpNodeQuery
365
366   def GET(self):
367     """Returns a list of all nodes.
368
369     """
370     client = self.GetClient()
371
372     if self.useBulk():
373       bulkdata = client.QueryNodes([], N_FIELDS, False)
374       return baserlib.MapBulkFields(bulkdata, N_FIELDS)
375     else:
376       nodesdata = client.QueryNodes([], ["name"], False)
377       nodeslist = [row[0] for row in nodesdata]
378       return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
379                                    uri_fields=("id", "uri"))
380
381
382 class R_2_nodes_name(baserlib.OpcodeResource):
383   """/2/nodes/[node_name] resource.
384
385   """
386   GET_OPCODE = opcodes.OpNodeQuery
387
388   def GET(self):
389     """Send information about a node.
390
391     """
392     node_name = self.items[0]
393     client = self.GetClient()
394
395     result = baserlib.HandleItemQueryErrors(client.QueryNodes,
396                                             names=[node_name], fields=N_FIELDS,
397                                             use_locking=self.useLocking())
398
399     return baserlib.MapFields(N_FIELDS, result[0])
400
401
402 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
403   """/2/nodes/[node_name]/powercycle resource.
404
405   """
406   POST_OPCODE = opcodes.OpNodePowercycle
407
408   def GetPostOpInput(self):
409     """Tries to powercycle a node.
410
411     """
412     return (self.request_body, {
413       "node_name": self.items[0],
414       "force": self.useForce(),
415       })
416
417
418 class R_2_nodes_name_role(baserlib.OpcodeResource):
419   """/2/nodes/[node_name]/role resource.
420
421   """
422   PUT_OPCODE = opcodes.OpNodeSetParams
423
424   def GET(self):
425     """Returns the current node role.
426
427     @return: Node role
428
429     """
430     node_name = self.items[0]
431     client = self.GetClient()
432     result = client.QueryNodes(names=[node_name], fields=["role"],
433                                use_locking=self.useLocking())
434
435     return _NR_MAP[result[0][0]]
436
437   def GetPutOpInput(self):
438     """Sets the node role.
439
440     """
441     baserlib.CheckType(self.request_body, basestring, "Body contents")
442
443     role = self.request_body
444
445     if role == _NR_REGULAR:
446       candidate = False
447       offline = False
448       drained = False
449
450     elif role == _NR_MASTER_CANDIDATE:
451       candidate = True
452       offline = drained = None
453
454     elif role == _NR_DRAINED:
455       drained = True
456       candidate = offline = None
457
458     elif role == _NR_OFFLINE:
459       offline = True
460       candidate = drained = None
461
462     else:
463       raise http.HttpBadRequest("Can't set '%s' role" % role)
464
465     assert len(self.items) == 1
466
467     return ({}, {
468       "node_name": self.items[0],
469       "master_candidate": candidate,
470       "offline": offline,
471       "drained": drained,
472       "force": self.useForce(),
473       "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
474       })
475
476
477 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
478   """/2/nodes/[node_name]/evacuate resource.
479
480   """
481   POST_OPCODE = opcodes.OpNodeEvacuate
482
483   def GetPostOpInput(self):
484     """Evacuate all instances off a node.
485
486     """
487     return (self.request_body, {
488       "node_name": self.items[0],
489       "dry_run": self.dryRun(),
490       })
491
492
493 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
494   """/2/nodes/[node_name]/migrate resource.
495
496   """
497   POST_OPCODE = opcodes.OpNodeMigrate
498
499   def GetPostOpInput(self):
500     """Migrate all primary instances from a node.
501
502     """
503     if self.queryargs:
504       # Support old-style requests
505       if "live" in self.queryargs and "mode" in self.queryargs:
506         raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
507                                   " be passed")
508
509       if "live" in self.queryargs:
510         if self._checkIntVariable("live", default=1):
511           mode = constants.HT_MIGRATION_LIVE
512         else:
513           mode = constants.HT_MIGRATION_NONLIVE
514       else:
515         mode = self._checkStringVariable("mode", default=None)
516
517       data = {
518         "mode": mode,
519         }
520     else:
521       data = self.request_body
522
523     return (data, {
524       "node_name": self.items[0],
525       })
526
527
528 class R_2_nodes_name_modify(baserlib.OpcodeResource):
529   """/2/nodes/[node_name]/modify resource.
530
531   """
532   POST_OPCODE = opcodes.OpNodeSetParams
533
534   def GetPostOpInput(self):
535     """Changes parameters of a node.
536
537     """
538     assert len(self.items) == 1
539
540     return (self.request_body, {
541       "node_name": self.items[0],
542       })
543
544
545 class R_2_nodes_name_storage(baserlib.OpcodeResource):
546   """/2/nodes/[node_name]/storage resource.
547
548   """
549   # LUNodeQueryStorage acquires locks, hence restricting access to GET
550   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
551   GET_OPCODE = opcodes.OpNodeQueryStorage
552
553   def GetGetOpInput(self):
554     """List storage available on a node.
555
556     """
557     storage_type = self._checkStringVariable("storage_type", None)
558     output_fields = self._checkStringVariable("output_fields", None)
559
560     if not output_fields:
561       raise http.HttpBadRequest("Missing the required 'output_fields'"
562                                 " parameter")
563
564     return ({}, {
565       "nodes": [self.items[0]],
566       "storage_type": storage_type,
567       "output_fields": output_fields.split(","),
568       })
569
570
571 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
572   """/2/nodes/[node_name]/storage/modify resource.
573
574   """
575   PUT_OPCODE = opcodes.OpNodeModifyStorage
576
577   def GetPutOpInput(self):
578     """Modifies a storage volume on a node.
579
580     """
581     storage_type = self._checkStringVariable("storage_type", None)
582     name = self._checkStringVariable("name", None)
583
584     if not name:
585       raise http.HttpBadRequest("Missing the required 'name'"
586                                 " parameter")
587
588     changes = {}
589
590     if "allocatable" in self.queryargs:
591       changes[constants.SF_ALLOCATABLE] = \
592         bool(self._checkIntVariable("allocatable", default=1))
593
594     return ({}, {
595       "node_name": self.items[0],
596       "storage_type": storage_type,
597       "name": name,
598       "changes": changes,
599       })
600
601
602 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
603   """/2/nodes/[node_name]/storage/repair resource.
604
605   """
606   PUT_OPCODE = opcodes.OpRepairNodeStorage
607
608   def GetPutOpInput(self):
609     """Repairs a storage volume on a node.
610
611     """
612     storage_type = self._checkStringVariable("storage_type", None)
613     name = self._checkStringVariable("name", None)
614     if not name:
615       raise http.HttpBadRequest("Missing the required 'name'"
616                                 " parameter")
617
618     return ({}, {
619       "node_name": self.items[0],
620       "storage_type": storage_type,
621       "name": name,
622       })
623
624
625 class R_2_groups(baserlib.OpcodeResource):
626   """/2/groups resource.
627
628   """
629   GET_OPCODE = opcodes.OpGroupQuery
630   POST_OPCODE = opcodes.OpGroupAdd
631   POST_RENAME = {
632     "name": "group_name",
633     }
634
635   def GetPostOpInput(self):
636     """Create a node group.
637
638     """
639     assert not self.items
640     return (self.request_body, {
641       "dry_run": self.dryRun(),
642       })
643
644   def GET(self):
645     """Returns a list of all node groups.
646
647     """
648     client = self.GetClient()
649
650     if self.useBulk():
651       bulkdata = client.QueryGroups([], G_FIELDS, False)
652       return baserlib.MapBulkFields(bulkdata, G_FIELDS)
653     else:
654       data = client.QueryGroups([], ["name"], False)
655       groupnames = [row[0] for row in data]
656       return baserlib.BuildUriList(groupnames, "/2/groups/%s",
657                                    uri_fields=("name", "uri"))
658
659
660 class R_2_groups_name(baserlib.OpcodeResource):
661   """/2/groups/[group_name] resource.
662
663   """
664   DELETE_OPCODE = opcodes.OpGroupRemove
665
666   def GET(self):
667     """Send information about a node group.
668
669     """
670     group_name = self.items[0]
671     client = self.GetClient()
672
673     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
674                                             names=[group_name], fields=G_FIELDS,
675                                             use_locking=self.useLocking())
676
677     return baserlib.MapFields(G_FIELDS, result[0])
678
679   def GetDeleteOpInput(self):
680     """Delete a node group.
681
682     """
683     assert len(self.items) == 1
684     return ({}, {
685       "group_name": self.items[0],
686       "dry_run": self.dryRun(),
687       })
688
689
690 class R_2_groups_name_modify(baserlib.OpcodeResource):
691   """/2/groups/[group_name]/modify resource.
692
693   """
694   PUT_OPCODE = opcodes.OpGroupSetParams
695
696   def GetPutOpInput(self):
697     """Changes some parameters of node group.
698
699     """
700     assert self.items
701     return (self.request_body, {
702       "group_name": self.items[0],
703       })
704
705
706 class R_2_groups_name_rename(baserlib.OpcodeResource):
707   """/2/groups/[group_name]/rename resource.
708
709   """
710   PUT_OPCODE = opcodes.OpGroupRename
711
712   def GetPutOpInput(self):
713     """Changes the name of a node group.
714
715     """
716     assert len(self.items) == 1
717     return (self.request_body, {
718       "group_name": self.items[0],
719       "dry_run": self.dryRun(),
720       })
721
722
723 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
724   """/2/groups/[group_name]/assign-nodes resource.
725
726   """
727   PUT_OPCODE = opcodes.OpGroupAssignNodes
728
729   def GetPutOpInput(self):
730     """Assigns nodes to a group.
731
732     """
733     assert len(self.items) == 1
734     return (self.request_body, {
735       "group_name": self.items[0],
736       "dry_run": self.dryRun(),
737       "force": self.useForce(),
738       })
739
740
741 class R_2_instances(baserlib.OpcodeResource):
742   """/2/instances resource.
743
744   """
745   GET_OPCODE = opcodes.OpInstanceQuery
746   POST_OPCODE = opcodes.OpInstanceCreate
747   POST_RENAME = {
748     "os": "os_type",
749     "name": "instance_name",
750     }
751
752   def GET(self):
753     """Returns a list of all available instances.
754
755     """
756     client = self.GetClient()
757
758     use_locking = self.useLocking()
759     if self.useBulk():
760       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
761       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
762     else:
763       instancesdata = client.QueryInstances([], ["name"], use_locking)
764       instanceslist = [row[0] for row in instancesdata]
765       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
766                                    uri_fields=("id", "uri"))
767
768   def GetPostOpInput(self):
769     """Create an instance.
770
771     @return: a job id
772
773     """
774     baserlib.CheckType(self.request_body, dict, "Body contents")
775
776     # Default to request data version 0
777     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
778
779     if data_version == 0:
780       raise http.HttpBadRequest("Instance creation request version 0 is no"
781                                 " longer supported")
782     elif data_version != 1:
783       raise http.HttpBadRequest("Unsupported request data version %s" %
784                                 data_version)
785
786     data = self.request_body.copy()
787     # Remove "__version__"
788     data.pop(_REQ_DATA_VERSION, None)
789
790     return (data, {
791       "dry_run": self.dryRun(),
792       })
793
794
795 class R_2_instances_name(baserlib.OpcodeResource):
796   """/2/instances/[instance_name] resource.
797
798   """
799   GET_OPCODE = opcodes.OpInstanceQuery
800   DELETE_OPCODE = opcodes.OpInstanceRemove
801
802   def GET(self):
803     """Send information about an instance.
804
805     """
806     client = self.GetClient()
807     instance_name = self.items[0]
808
809     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
810                                             names=[instance_name],
811                                             fields=I_FIELDS,
812                                             use_locking=self.useLocking())
813
814     return baserlib.MapFields(I_FIELDS, result[0])
815
816   def GetDeleteOpInput(self):
817     """Delete an instance.
818
819     """
820     assert len(self.items) == 1
821     return ({}, {
822       "instance_name": self.items[0],
823       "ignore_failures": False,
824       "dry_run": self.dryRun(),
825       })
826
827
828 class R_2_instances_name_info(baserlib.OpcodeResource):
829   """/2/instances/[instance_name]/info resource.
830
831   """
832   GET_OPCODE = opcodes.OpInstanceQueryData
833
834   def GetGetOpInput(self):
835     """Request detailed instance information.
836
837     """
838     assert len(self.items) == 1
839     return ({}, {
840       "instances": [self.items[0]],
841       "static": bool(self._checkIntVariable("static", default=0)),
842       })
843
844
845 class R_2_instances_name_reboot(baserlib.OpcodeResource):
846   """/2/instances/[instance_name]/reboot resource.
847
848   Implements an instance reboot.
849
850   """
851   POST_OPCODE = opcodes.OpInstanceReboot
852
853   def GetPostOpInput(self):
854     """Reboot an instance.
855
856     The URI takes type=[hard|soft|full] and
857     ignore_secondaries=[False|True] parameters.
858
859     """
860     return ({}, {
861       "instance_name": self.items[0],
862       "reboot_type":
863         self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
864       "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
865       "dry_run": self.dryRun(),
866       })
867
868
869 class R_2_instances_name_startup(baserlib.OpcodeResource):
870   """/2/instances/[instance_name]/startup resource.
871
872   Implements an instance startup.
873
874   """
875   PUT_OPCODE = opcodes.OpInstanceStartup
876
877   def GetPutOpInput(self):
878     """Startup an instance.
879
880     The URI takes force=[False|True] parameter to start the instance
881     if even if secondary disks are failing.
882
883     """
884     return ({}, {
885       "instance_name": self.items[0],
886       "force": self.useForce(),
887       "dry_run": self.dryRun(),
888       "no_remember": bool(self._checkIntVariable("no_remember")),
889       })
890
891
892 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
893   """/2/instances/[instance_name]/shutdown resource.
894
895   Implements an instance shutdown.
896
897   """
898   PUT_OPCODE = opcodes.OpInstanceShutdown
899
900   def GetPutOpInput(self):
901     """Shutdown an instance.
902
903     """
904     return (self.request_body, {
905       "instance_name": self.items[0],
906       "no_remember": bool(self._checkIntVariable("no_remember")),
907       "dry_run": self.dryRun(),
908       })
909
910
911 def _ParseInstanceReinstallRequest(name, data):
912   """Parses a request for reinstalling an instance.
913
914   """
915   if not isinstance(data, dict):
916     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
917
918   ostype = baserlib.CheckParameter(data, "os", default=None)
919   start = baserlib.CheckParameter(data, "start", exptype=bool,
920                                   default=True)
921   osparams = baserlib.CheckParameter(data, "osparams", default=None)
922
923   ops = [
924     opcodes.OpInstanceShutdown(instance_name=name),
925     opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
926                                 osparams=osparams),
927     ]
928
929   if start:
930     ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
931
932   return ops
933
934
935 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
936   """/2/instances/[instance_name]/reinstall resource.
937
938   Implements an instance reinstall.
939
940   """
941   POST_OPCODE = opcodes.OpInstanceReinstall
942
943   def POST(self):
944     """Reinstall an instance.
945
946     The URI takes os=name and nostartup=[0|1] optional
947     parameters. By default, the instance will be started
948     automatically.
949
950     """
951     if self.request_body:
952       if self.queryargs:
953         raise http.HttpBadRequest("Can't combine query and body parameters")
954
955       body = self.request_body
956     elif self.queryargs:
957       # Legacy interface, do not modify/extend
958       body = {
959         "os": self._checkStringVariable("os"),
960         "start": not self._checkIntVariable("nostartup"),
961         }
962     else:
963       body = {}
964
965     ops = _ParseInstanceReinstallRequest(self.items[0], body)
966
967     return self.SubmitJob(ops)
968
969
970 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
971   """/2/instances/[instance_name]/replace-disks resource.
972
973   """
974   POST_OPCODE = opcodes.OpInstanceReplaceDisks
975
976   def GetPostOpInput(self):
977     """Replaces disks on an instance.
978
979     """
980     static = {
981       "instance_name": self.items[0],
982       }
983
984     if self.request_body:
985       data = self.request_body
986     elif self.queryargs:
987       # Legacy interface, do not modify/extend
988       data = {
989         "remote_node": self._checkStringVariable("remote_node", default=None),
990         "mode": self._checkStringVariable("mode", default=None),
991         "disks": self._checkStringVariable("disks", default=None),
992         "iallocator": self._checkStringVariable("iallocator", default=None),
993         }
994     else:
995       data = {}
996
997     # Parse disks
998     try:
999       raw_disks = data.pop("disks")
1000     except KeyError:
1001       pass
1002     else:
1003       if raw_disks:
1004         if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1005           data["disks"] = raw_disks
1006         else:
1007           # Backwards compatibility for strings of the format "1, 2, 3"
1008           try:
1009             data["disks"] = [int(part) for part in raw_disks.split(",")]
1010           except (TypeError, ValueError), err:
1011             raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1012
1013     return (data, static)
1014
1015
1016 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1017   """/2/instances/[instance_name]/activate-disks resource.
1018
1019   """
1020   PUT_OPCODE = opcodes.OpInstanceActivateDisks
1021
1022   def GetPutOpInput(self):
1023     """Activate disks for an instance.
1024
1025     The URI might contain ignore_size to ignore current recorded size.
1026
1027     """
1028     return ({}, {
1029       "instance_name": self.items[0],
1030       "ignore_size": bool(self._checkIntVariable("ignore_size")),
1031       })
1032
1033
1034 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1035   """/2/instances/[instance_name]/deactivate-disks resource.
1036
1037   """
1038   PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1039
1040   def GetPutOpInput(self):
1041     """Deactivate disks for an instance.
1042
1043     """
1044     return ({}, {
1045       "instance_name": self.items[0],
1046       })
1047
1048
1049 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1050   """/2/instances/[instance_name]/recreate-disks resource.
1051
1052   """
1053   POST_OPCODE = opcodes.OpInstanceRecreateDisks
1054
1055   def GetPostOpInput(self):
1056     """Recreate disks for an instance.
1057
1058     """
1059     return ({}, {
1060       "instance_name": self.items[0],
1061       })
1062
1063
1064 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1065   """/2/instances/[instance_name]/prepare-export resource.
1066
1067   """
1068   PUT_OPCODE = opcodes.OpBackupPrepare
1069
1070   def GetPutOpInput(self):
1071     """Prepares an export for an instance.
1072
1073     """
1074     return ({}, {
1075       "instance_name": self.items[0],
1076       "mode": self._checkStringVariable("mode"),
1077       })
1078
1079
1080 class R_2_instances_name_export(baserlib.OpcodeResource):
1081   """/2/instances/[instance_name]/export resource.
1082
1083   """
1084   PUT_OPCODE = opcodes.OpBackupExport
1085   PUT_RENAME = {
1086     "destination": "target_node",
1087     }
1088
1089   def GetPutOpInput(self):
1090     """Exports an instance.
1091
1092     """
1093     return (self.request_body, {
1094       "instance_name": self.items[0],
1095       })
1096
1097
1098 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1099   """/2/instances/[instance_name]/migrate resource.
1100
1101   """
1102   PUT_OPCODE = opcodes.OpInstanceMigrate
1103
1104   def GetPutOpInput(self):
1105     """Migrates an instance.
1106
1107     """
1108     return (self.request_body, {
1109       "instance_name": self.items[0],
1110       })
1111
1112
1113 class R_2_instances_name_failover(baserlib.OpcodeResource):
1114   """/2/instances/[instance_name]/failover resource.
1115
1116   """
1117   PUT_OPCODE = opcodes.OpInstanceFailover
1118
1119   def GetPutOpInput(self):
1120     """Does a failover of an instance.
1121
1122     """
1123     return (self.request_body, {
1124       "instance_name": self.items[0],
1125       })
1126
1127
1128 class R_2_instances_name_rename(baserlib.OpcodeResource):
1129   """/2/instances/[instance_name]/rename resource.
1130
1131   """
1132   PUT_OPCODE = opcodes.OpInstanceRename
1133
1134   def GetPutOpInput(self):
1135     """Changes the name of an instance.
1136
1137     """
1138     return (self.request_body, {
1139       "instance_name": self.items[0],
1140       })
1141
1142
1143 class R_2_instances_name_modify(baserlib.OpcodeResource):
1144   """/2/instances/[instance_name]/modify resource.
1145
1146   """
1147   PUT_OPCODE = opcodes.OpInstanceSetParams
1148
1149   def GetPutOpInput(self):
1150     """Changes parameters of an instance.
1151
1152     """
1153     return (self.request_body, {
1154       "instance_name": self.items[0],
1155       })
1156
1157
1158 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1159   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1160
1161   """
1162   POST_OPCODE = opcodes.OpInstanceGrowDisk
1163
1164   def GetPostOpInput(self):
1165     """Increases the size of an instance disk.
1166
1167     """
1168     return (self.request_body, {
1169       "instance_name": self.items[0],
1170       "disk": int(self.items[1]),
1171       })
1172
1173
1174 class R_2_instances_name_console(baserlib.ResourceBase):
1175   """/2/instances/[instance_name]/console resource.
1176
1177   """
1178   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1179   GET_OPCODE = opcodes.OpInstanceConsole
1180
1181   def GET(self):
1182     """Request information for connecting to instance's console.
1183
1184     @return: Serialized instance console description, see
1185              L{objects.InstanceConsole}
1186
1187     """
1188     client = self.GetClient()
1189
1190     ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1191
1192     if console is None:
1193       raise http.HttpServiceUnavailable("Instance console unavailable")
1194
1195     assert isinstance(console, dict)
1196     return console
1197
1198
1199 def _GetQueryFields(args):
1200   """
1201
1202   """
1203   try:
1204     fields = args["fields"]
1205   except KeyError:
1206     raise http.HttpBadRequest("Missing 'fields' query argument")
1207
1208   return _SplitQueryFields(fields[0])
1209
1210
1211 def _SplitQueryFields(fields):
1212   """
1213
1214   """
1215   return [i.strip() for i in fields.split(",")]
1216
1217
1218 class R_2_query(baserlib.ResourceBase):
1219   """/2/query/[resource] resource.
1220
1221   """
1222   # Results might contain sensitive information
1223   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1224   GET_OPCODE = opcodes.OpQuery
1225   PUT_OPCODE = opcodes.OpQuery
1226
1227   def _Query(self, fields, qfilter):
1228     return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1229
1230   def GET(self):
1231     """Returns resource information.
1232
1233     @return: Query result, see L{objects.QueryResponse}
1234
1235     """
1236     return self._Query(_GetQueryFields(self.queryargs), None)
1237
1238   def PUT(self):
1239     """Submits job querying for resources.
1240
1241     @return: Query result, see L{objects.QueryResponse}
1242
1243     """
1244     body = self.request_body
1245
1246     baserlib.CheckType(body, dict, "Body contents")
1247
1248     try:
1249       fields = body["fields"]
1250     except KeyError:
1251       fields = _GetQueryFields(self.queryargs)
1252
1253     qfilter = body.get("qfilter", None)
1254     # TODO: remove this after 2.7
1255     if qfilter is None:
1256       qfilter = body.get("filter", None)
1257
1258     return self._Query(fields, qfilter)
1259
1260
1261 class R_2_query_fields(baserlib.ResourceBase):
1262   """/2/query/[resource]/fields resource.
1263
1264   """
1265   GET_OPCODE = opcodes.OpQueryFields
1266
1267   def GET(self):
1268     """Retrieves list of available fields for a resource.
1269
1270     @return: List of serialized L{objects.QueryFieldDefinition}
1271
1272     """
1273     try:
1274       raw_fields = self.queryargs["fields"]
1275     except KeyError:
1276       fields = None
1277     else:
1278       fields = _SplitQueryFields(raw_fields[0])
1279
1280     return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1281
1282
1283 class _R_Tags(baserlib.OpcodeResource):
1284   """Quasiclass for tagging resources.
1285
1286   Manages tags. When inheriting this class you must define the
1287   TAG_LEVEL for it.
1288
1289   """
1290   TAG_LEVEL = None
1291   GET_OPCODE = opcodes.OpTagsGet
1292   PUT_OPCODE = opcodes.OpTagsSet
1293   DELETE_OPCODE = opcodes.OpTagsDel
1294
1295   def __init__(self, items, queryargs, req, **kwargs):
1296     """A tag resource constructor.
1297
1298     We have to override the default to sort out cluster naming case.
1299
1300     """
1301     baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1302
1303     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1304       self.name = None
1305     else:
1306       self.name = items[0]
1307
1308   def GET(self):
1309     """Returns a list of tags.
1310
1311     Example: ["tag1", "tag2", "tag3"]
1312
1313     """
1314     kind = self.TAG_LEVEL
1315
1316     if kind in (constants.TAG_INSTANCE,
1317                 constants.TAG_NODEGROUP,
1318                 constants.TAG_NODE):
1319       if not self.name:
1320         raise http.HttpBadRequest("Missing name on tag request")
1321
1322       cl = self.GetClient()
1323       if kind == constants.TAG_INSTANCE:
1324         fn = cl.QueryInstances
1325       elif kind == constants.TAG_NODEGROUP:
1326         fn = cl.QueryGroups
1327       else:
1328         fn = cl.QueryNodes
1329       result = fn(names=[self.name], fields=["tags"], use_locking=False)
1330       if not result or not result[0]:
1331         raise http.HttpBadGateway("Invalid response from tag query")
1332       tags = result[0][0]
1333
1334     elif kind == constants.TAG_CLUSTER:
1335       assert not self.name
1336       # TODO: Use query API?
1337       ssc = ssconf.SimpleStore()
1338       tags = ssc.GetClusterTags()
1339
1340     return list(tags)
1341
1342   def GetPutOpInput(self):
1343     """Add a set of tags.
1344
1345     The request as a list of strings should be PUT to this URI. And
1346     you'll have back a job id.
1347
1348     """
1349     return ({}, {
1350       "kind": self.TAG_LEVEL,
1351       "name": self.name,
1352       "tags": self.queryargs.get("tag", []),
1353       "dry_run": self.dryRun(),
1354       })
1355
1356   def GetDeleteOpInput(self):
1357     """Delete a tag.
1358
1359     In order to delete a set of tags, the DELETE
1360     request should be addressed to URI like:
1361     /tags?tag=[tag]&tag=[tag]
1362
1363     """
1364     # Re-use code
1365     return self.GetPutOpInput()
1366
1367
1368 class R_2_instances_name_tags(_R_Tags):
1369   """ /2/instances/[instance_name]/tags resource.
1370
1371   Manages per-instance tags.
1372
1373   """
1374   TAG_LEVEL = constants.TAG_INSTANCE
1375
1376
1377 class R_2_nodes_name_tags(_R_Tags):
1378   """ /2/nodes/[node_name]/tags resource.
1379
1380   Manages per-node tags.
1381
1382   """
1383   TAG_LEVEL = constants.TAG_NODE
1384
1385
1386 class R_2_groups_name_tags(_R_Tags):
1387   """ /2/groups/[group_name]/tags resource.
1388
1389   Manages per-nodegroup tags.
1390
1391   """
1392   TAG_LEVEL = constants.TAG_NODEGROUP
1393
1394
1395 class R_2_tags(_R_Tags):
1396   """ /2/tags resource.
1397
1398   Manages cluster tags.
1399
1400   """
1401   TAG_LEVEL = constants.TAG_CLUSTER