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