Merge branch 'devel-2.5'
[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   ] + _COMMON_FIELDS
98
99 J_FIELDS_BULK = [
100   "id", "ops", "status", "summary",
101   "opstatus",
102   "received_ts", "start_ts", "end_ts",
103   ]
104
105 J_FIELDS = J_FIELDS_BULK + [
106   "oplog",
107   "opresult",
108   ]
109
110 _NR_DRAINED = "drained"
111 _NR_MASTER_CANDIDATE = "master-candidate"
112 _NR_MASTER = "master"
113 _NR_OFFLINE = "offline"
114 _NR_REGULAR = "regular"
115
116 _NR_MAP = {
117   constants.NR_MASTER: _NR_MASTER,
118   constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
119   constants.NR_DRAINED: _NR_DRAINED,
120   constants.NR_OFFLINE: _NR_OFFLINE,
121   constants.NR_REGULAR: _NR_REGULAR,
122   }
123
124 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
125
126 # Request data version field
127 _REQ_DATA_VERSION = "__version__"
128
129 # Feature string for instance creation request data version 1
130 _INST_CREATE_REQV1 = "instance-create-reqv1"
131
132 # Feature string for instance reinstall request version 1
133 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
134
135 # Feature string for node migration version 1
136 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
137
138 # Feature string for node evacuation with LU-generated jobs
139 _NODE_EVAC_RES1 = "node-evac-res1"
140
141 ALL_FEATURES = frozenset([
142   _INST_CREATE_REQV1,
143   _INST_REINSTALL_REQV1,
144   _NODE_MIGRATE_REQV1,
145   _NODE_EVAC_RES1,
146   ])
147
148 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
149 _WFJC_TIMEOUT = 10
150
151
152 class R_root(baserlib.ResourceBase):
153   """/ resource.
154
155   """
156   @staticmethod
157   def GET():
158     """Supported for legacy reasons.
159
160     """
161     return None
162
163
164 class R_2(R_root):
165   """/2 resource.
166
167   """
168
169
170 class R_version(baserlib.ResourceBase):
171   """/version resource.
172
173   This resource should be used to determine the remote API version and
174   to adapt clients accordingly.
175
176   """
177   @staticmethod
178   def GET():
179     """Returns the remote API version.
180
181     """
182     return constants.RAPI_VERSION
183
184
185 class R_2_info(baserlib.OpcodeResource):
186   """/2/info resource.
187
188   """
189   GET_OPCODE = opcodes.OpClusterQuery
190
191   def GET(self):
192     """Returns cluster information.
193
194     """
195     client = self.GetClient()
196     return client.QueryClusterInfo()
197
198
199 class R_2_features(baserlib.ResourceBase):
200   """/2/features resource.
201
202   """
203   @staticmethod
204   def GET():
205     """Returns list of optional RAPI features implemented.
206
207     """
208     return list(ALL_FEATURES)
209
210
211 class R_2_os(baserlib.OpcodeResource):
212   """/2/os resource.
213
214   """
215   GET_OPCODE = opcodes.OpOsDiagnose
216
217   def GET(self):
218     """Return a list of all OSes.
219
220     Can return error 500 in case of a problem.
221
222     Example: ["debian-etch"]
223
224     """
225     cl = self.GetClient()
226     op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
227     job_id = self.SubmitJob([op], cl=cl)
228     # we use custom feedback function, instead of print we log the status
229     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
230     diagnose_data = result[0]
231
232     if not isinstance(diagnose_data, list):
233       raise http.HttpBadGateway(message="Can't get OS list")
234
235     os_names = []
236     for (name, variants) in diagnose_data:
237       os_names.extend(cli.CalculateOSNames(name, variants))
238
239     return os_names
240
241
242 class R_2_redist_config(baserlib.OpcodeResource):
243   """/2/redistribute-config resource.
244
245   """
246   PUT_OPCODE = opcodes.OpClusterRedistConf
247
248
249 class R_2_cluster_modify(baserlib.OpcodeResource):
250   """/2/modify resource.
251
252   """
253   PUT_OPCODE = opcodes.OpClusterSetParams
254
255
256 class R_2_jobs(baserlib.ResourceBase):
257   """/2/jobs resource.
258
259   """
260   def GET(self):
261     """Returns a dictionary of jobs.
262
263     @return: a dictionary with jobs id and uri.
264
265     """
266     client = self.GetClient()
267
268     if self.useBulk():
269       bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
270       return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
271     else:
272       jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
273       return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
274                                    uri_fields=("id", "uri"))
275
276
277 class R_2_jobs_id(baserlib.ResourceBase):
278   """/2/jobs/[job_id] resource.
279
280   """
281   def GET(self):
282     """Returns a job status.
283
284     @return: a dictionary with job parameters.
285         The result includes:
286             - id: job ID as a number
287             - status: current job status as a string
288             - ops: involved OpCodes as a list of dictionaries for each
289               opcodes in the job
290             - opstatus: OpCodes status as a list
291             - opresult: OpCodes results as a list of lists
292
293     """
294     job_id = self.items[0]
295     result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
296     if result is None:
297       raise http.HttpNotFound()
298     return baserlib.MapFields(J_FIELDS, result)
299
300   def DELETE(self):
301     """Cancel not-yet-started job.
302
303     """
304     job_id = self.items[0]
305     result = self.GetClient().CancelJob(job_id)
306     return result
307
308
309 class R_2_jobs_id_wait(baserlib.ResourceBase):
310   """/2/jobs/[job_id]/wait resource.
311
312   """
313   # WaitForJobChange provides access to sensitive information and blocks
314   # machine resources (it's a blocking RAPI call), hence restricting access.
315   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
316
317   def GET(self):
318     """Waits for job changes.
319
320     """
321     job_id = self.items[0]
322
323     fields = self.getBodyParameter("fields")
324     prev_job_info = self.getBodyParameter("previous_job_info", None)
325     prev_log_serial = self.getBodyParameter("previous_log_serial", None)
326
327     if not isinstance(fields, list):
328       raise http.HttpBadRequest("The 'fields' parameter should be a list")
329
330     if not (prev_job_info is None or isinstance(prev_job_info, list)):
331       raise http.HttpBadRequest("The 'previous_job_info' parameter should"
332                                 " be a list")
333
334     if not (prev_log_serial is None or
335             isinstance(prev_log_serial, (int, long))):
336       raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
337                                 " be a number")
338
339     client = self.GetClient()
340     result = client.WaitForJobChangeOnce(job_id, fields,
341                                          prev_job_info, prev_log_serial,
342                                          timeout=_WFJC_TIMEOUT)
343     if not result:
344       raise http.HttpNotFound()
345
346     if result == constants.JOB_NOTCHANGED:
347       # No changes
348       return None
349
350     (job_info, log_entries) = result
351
352     return {
353       "job_info": job_info,
354       "log_entries": log_entries,
355       }
356
357
358 class R_2_nodes(baserlib.OpcodeResource):
359   """/2/nodes resource.
360
361   """
362   GET_OPCODE = opcodes.OpNodeQuery
363
364   def GET(self):
365     """Returns a list of all nodes.
366
367     """
368     client = self.GetClient()
369
370     if self.useBulk():
371       bulkdata = client.QueryNodes([], N_FIELDS, False)
372       return baserlib.MapBulkFields(bulkdata, N_FIELDS)
373     else:
374       nodesdata = client.QueryNodes([], ["name"], False)
375       nodeslist = [row[0] for row in nodesdata]
376       return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
377                                    uri_fields=("id", "uri"))
378
379
380 class R_2_nodes_name(baserlib.OpcodeResource):
381   """/2/nodes/[node_name] resource.
382
383   """
384   GET_OPCODE = opcodes.OpNodeQuery
385
386   def GET(self):
387     """Send information about a node.
388
389     """
390     node_name = self.items[0]
391     client = self.GetClient()
392
393     result = baserlib.HandleItemQueryErrors(client.QueryNodes,
394                                             names=[node_name], fields=N_FIELDS,
395                                             use_locking=self.useLocking())
396
397     return baserlib.MapFields(N_FIELDS, result[0])
398
399
400 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
401   """/2/nodes/[node_name]/powercycle resource.
402
403   """
404   POST_OPCODE = opcodes.OpNodePowercycle
405
406   def GetPostOpInput(self):
407     """Tries to powercycle a node.
408
409     """
410     return (self.request_body, {
411       "node_name": self.items[0],
412       "force": self.useForce(),
413       })
414
415
416 class R_2_nodes_name_role(baserlib.OpcodeResource):
417   """/2/nodes/[node_name]/role resource.
418
419   """
420   PUT_OPCODE = opcodes.OpNodeSetParams
421
422   def GET(self):
423     """Returns the current node role.
424
425     @return: Node role
426
427     """
428     node_name = self.items[0]
429     client = self.GetClient()
430     result = client.QueryNodes(names=[node_name], fields=["role"],
431                                use_locking=self.useLocking())
432
433     return _NR_MAP[result[0][0]]
434
435   def GetPutOpInput(self):
436     """Sets the node role.
437
438     """
439     baserlib.CheckType(self.request_body, basestring, "Body contents")
440
441     role = self.request_body
442
443     if role == _NR_REGULAR:
444       candidate = False
445       offline = False
446       drained = False
447
448     elif role == _NR_MASTER_CANDIDATE:
449       candidate = True
450       offline = drained = None
451
452     elif role == _NR_DRAINED:
453       drained = True
454       candidate = offline = None
455
456     elif role == _NR_OFFLINE:
457       offline = True
458       candidate = drained = None
459
460     else:
461       raise http.HttpBadRequest("Can't set '%s' role" % role)
462
463     assert len(self.items) == 1
464
465     return ({}, {
466       "node_name": self.items[0],
467       "master_candidate": candidate,
468       "offline": offline,
469       "drained": drained,
470       "force": self.useForce(),
471       "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
472       })
473
474
475 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
476   """/2/nodes/[node_name]/evacuate resource.
477
478   """
479   POST_OPCODE = opcodes.OpNodeEvacuate
480
481   def GetPostOpInput(self):
482     """Evacuate all instances off a node.
483
484     """
485     return (self.request_body, {
486       "node_name": self.items[0],
487       "dry_run": self.dryRun(),
488       })
489
490
491 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
492   """/2/nodes/[node_name]/migrate resource.
493
494   """
495   POST_OPCODE = opcodes.OpNodeMigrate
496
497   def GetPostOpInput(self):
498     """Migrate all primary instances from a node.
499
500     """
501     if self.queryargs:
502       # Support old-style requests
503       if "live" in self.queryargs and "mode" in self.queryargs:
504         raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
505                                   " be passed")
506
507       if "live" in self.queryargs:
508         if self._checkIntVariable("live", default=1):
509           mode = constants.HT_MIGRATION_LIVE
510         else:
511           mode = constants.HT_MIGRATION_NONLIVE
512       else:
513         mode = self._checkStringVariable("mode", default=None)
514
515       data = {
516         "mode": mode,
517         }
518     else:
519       data = self.request_body
520
521     return (data, {
522       "node_name": self.items[0],
523       })
524
525
526 class R_2_nodes_name_modify(baserlib.OpcodeResource):
527   """/2/nodes/[node_name]/modify resource.
528
529   """
530   POST_OPCODE = opcodes.OpNodeSetParams
531
532   def GetPostOpInput(self):
533     """Changes parameters of a node.
534
535     """
536     assert len(self.items) == 1
537
538     return (self.request_body, {
539       "node_name": self.items[0],
540       })
541
542
543 class R_2_nodes_name_storage(baserlib.OpcodeResource):
544   """/2/nodes/[node_name]/storage resource.
545
546   """
547   # LUNodeQueryStorage acquires locks, hence restricting access to GET
548   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
549   GET_OPCODE = opcodes.OpNodeQueryStorage
550
551   def GetGetOpInput(self):
552     """List storage available on a node.
553
554     """
555     storage_type = self._checkStringVariable("storage_type", None)
556     output_fields = self._checkStringVariable("output_fields", None)
557
558     if not output_fields:
559       raise http.HttpBadRequest("Missing the required 'output_fields'"
560                                 " parameter")
561
562     return ({}, {
563       "nodes": [self.items[0]],
564       "storage_type": storage_type,
565       "output_fields": output_fields.split(","),
566       })
567
568
569 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
570   """/2/nodes/[node_name]/storage/modify resource.
571
572   """
573   PUT_OPCODE = opcodes.OpNodeModifyStorage
574
575   def GetPutOpInput(self):
576     """Modifies a storage volume on a node.
577
578     """
579     storage_type = self._checkStringVariable("storage_type", None)
580     name = self._checkStringVariable("name", None)
581
582     if not name:
583       raise http.HttpBadRequest("Missing the required 'name'"
584                                 " parameter")
585
586     changes = {}
587
588     if "allocatable" in self.queryargs:
589       changes[constants.SF_ALLOCATABLE] = \
590         bool(self._checkIntVariable("allocatable", default=1))
591
592     return ({}, {
593       "node_name": self.items[0],
594       "storage_type": storage_type,
595       "name": name,
596       "changes": changes,
597       })
598
599
600 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
601   """/2/nodes/[node_name]/storage/repair resource.
602
603   """
604   PUT_OPCODE = opcodes.OpRepairNodeStorage
605
606   def GetPutOpInput(self):
607     """Repairs a storage volume on a node.
608
609     """
610     storage_type = self._checkStringVariable("storage_type", None)
611     name = self._checkStringVariable("name", None)
612     if not name:
613       raise http.HttpBadRequest("Missing the required 'name'"
614                                 " parameter")
615
616     return ({}, {
617       "node_name": self.items[0],
618       "storage_type": storage_type,
619       "name": name,
620       })
621
622
623 class R_2_groups(baserlib.OpcodeResource):
624   """/2/groups resource.
625
626   """
627   GET_OPCODE = opcodes.OpGroupQuery
628   POST_OPCODE = opcodes.OpGroupAdd
629   POST_RENAME = {
630     "name": "group_name",
631     }
632
633   def GetPostOpInput(self):
634     """Create a node group.
635
636     """
637     assert not self.items
638     return (self.request_body, {
639       "dry_run": self.dryRun(),
640       })
641
642   def GET(self):
643     """Returns a list of all node groups.
644
645     """
646     client = self.GetClient()
647
648     if self.useBulk():
649       bulkdata = client.QueryGroups([], G_FIELDS, False)
650       return baserlib.MapBulkFields(bulkdata, G_FIELDS)
651     else:
652       data = client.QueryGroups([], ["name"], False)
653       groupnames = [row[0] for row in data]
654       return baserlib.BuildUriList(groupnames, "/2/groups/%s",
655                                    uri_fields=("name", "uri"))
656
657
658 class R_2_groups_name(baserlib.OpcodeResource):
659   """/2/groups/[group_name] resource.
660
661   """
662   DELETE_OPCODE = opcodes.OpGroupRemove
663
664   def GET(self):
665     """Send information about a node group.
666
667     """
668     group_name = self.items[0]
669     client = self.GetClient()
670
671     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
672                                             names=[group_name], fields=G_FIELDS,
673                                             use_locking=self.useLocking())
674
675     return baserlib.MapFields(G_FIELDS, result[0])
676
677   def GetDeleteOpInput(self):
678     """Delete a node group.
679
680     """
681     assert len(self.items) == 1
682     return ({}, {
683       "group_name": self.items[0],
684       "dry_run": self.dryRun(),
685       })
686
687
688 class R_2_groups_name_modify(baserlib.OpcodeResource):
689   """/2/groups/[group_name]/modify resource.
690
691   """
692   PUT_OPCODE = opcodes.OpGroupSetParams
693
694   def GetPutOpInput(self):
695     """Changes some parameters of node group.
696
697     """
698     assert self.items
699     return (self.request_body, {
700       "group_name": self.items[0],
701       })
702
703
704 class R_2_groups_name_rename(baserlib.OpcodeResource):
705   """/2/groups/[group_name]/rename resource.
706
707   """
708   PUT_OPCODE = opcodes.OpGroupRename
709
710   def GetPutOpInput(self):
711     """Changes the name of a node group.
712
713     """
714     assert len(self.items) == 1
715     return (self.request_body, {
716       "group_name": self.items[0],
717       "dry_run": self.dryRun(),
718       })
719
720
721 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
722   """/2/groups/[group_name]/assign-nodes resource.
723
724   """
725   PUT_OPCODE = opcodes.OpGroupAssignNodes
726
727   def GetPutOpInput(self):
728     """Assigns nodes to a group.
729
730     """
731     assert len(self.items) == 1
732     return (self.request_body, {
733       "group_name": self.items[0],
734       "dry_run": self.dryRun(),
735       "force": self.useForce(),
736       })
737
738
739 class R_2_instances(baserlib.OpcodeResource):
740   """/2/instances resource.
741
742   """
743   GET_OPCODE = opcodes.OpInstanceQuery
744   POST_OPCODE = opcodes.OpInstanceCreate
745   POST_RENAME = {
746     "os": "os_type",
747     "name": "instance_name",
748     }
749
750   def GET(self):
751     """Returns a list of all available instances.
752
753     """
754     client = self.GetClient()
755
756     use_locking = self.useLocking()
757     if self.useBulk():
758       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
759       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
760     else:
761       instancesdata = client.QueryInstances([], ["name"], use_locking)
762       instanceslist = [row[0] for row in instancesdata]
763       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
764                                    uri_fields=("id", "uri"))
765
766   def GetPostOpInput(self):
767     """Create an instance.
768
769     @return: a job id
770
771     """
772     baserlib.CheckType(self.request_body, dict, "Body contents")
773
774     # Default to request data version 0
775     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
776
777     if data_version == 0:
778       raise http.HttpBadRequest("Instance creation request version 0 is no"
779                                 " longer supported")
780     elif data_version != 1:
781       raise http.HttpBadRequest("Unsupported request data version %s" %
782                                 data_version)
783
784     data = self.request_body.copy()
785     # Remove "__version__"
786     data.pop(_REQ_DATA_VERSION, None)
787
788     return (data, {
789       "dry_run": self.dryRun(),
790       })
791
792
793 class R_2_instances_name(baserlib.OpcodeResource):
794   """/2/instances/[instance_name] resource.
795
796   """
797   GET_OPCODE = opcodes.OpInstanceQuery
798   DELETE_OPCODE = opcodes.OpInstanceRemove
799
800   def GET(self):
801     """Send information about an instance.
802
803     """
804     client = self.GetClient()
805     instance_name = self.items[0]
806
807     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
808                                             names=[instance_name],
809                                             fields=I_FIELDS,
810                                             use_locking=self.useLocking())
811
812     return baserlib.MapFields(I_FIELDS, result[0])
813
814   def GetDeleteOpInput(self):
815     """Delete an instance.
816
817     """
818     assert len(self.items) == 1
819     return ({}, {
820       "instance_name": self.items[0],
821       "ignore_failures": False,
822       "dry_run": self.dryRun(),
823       })
824
825
826 class R_2_instances_name_info(baserlib.OpcodeResource):
827   """/2/instances/[instance_name]/info resource.
828
829   """
830   GET_OPCODE = opcodes.OpInstanceQueryData
831
832   def GetGetOpInput(self):
833     """Request detailed instance information.
834
835     """
836     assert len(self.items) == 1
837     return ({}, {
838       "instances": [self.items[0]],
839       "static": bool(self._checkIntVariable("static", default=0)),
840       })
841
842
843 class R_2_instances_name_reboot(baserlib.OpcodeResource):
844   """/2/instances/[instance_name]/reboot resource.
845
846   Implements an instance reboot.
847
848   """
849   POST_OPCODE = opcodes.OpInstanceReboot
850
851   def GetPostOpInput(self):
852     """Reboot an instance.
853
854     The URI takes type=[hard|soft|full] and
855     ignore_secondaries=[False|True] parameters.
856
857     """
858     return ({}, {
859       "instance_name": self.items[0],
860       "reboot_type":
861         self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
862       "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
863       "dry_run": self.dryRun(),
864       })
865
866
867 class R_2_instances_name_startup(baserlib.OpcodeResource):
868   """/2/instances/[instance_name]/startup resource.
869
870   Implements an instance startup.
871
872   """
873   PUT_OPCODE = opcodes.OpInstanceStartup
874
875   def GetPutOpInput(self):
876     """Startup an instance.
877
878     The URI takes force=[False|True] parameter to start the instance
879     if even if secondary disks are failing.
880
881     """
882     return ({}, {
883       "instance_name": self.items[0],
884       "force": self.useForce(),
885       "dry_run": self.dryRun(),
886       "no_remember": bool(self._checkIntVariable("no_remember")),
887       })
888
889
890 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
891   """/2/instances/[instance_name]/shutdown resource.
892
893   Implements an instance shutdown.
894
895   """
896   PUT_OPCODE = opcodes.OpInstanceShutdown
897
898   def GetPutOpInput(self):
899     """Shutdown an instance.
900
901     """
902     return (self.request_body, {
903       "instance_name": self.items[0],
904       "no_remember": bool(self._checkIntVariable("no_remember")),
905       "dry_run": self.dryRun(),
906       })
907
908
909 def _ParseInstanceReinstallRequest(name, data):
910   """Parses a request for reinstalling an instance.
911
912   """
913   if not isinstance(data, dict):
914     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
915
916   ostype = baserlib.CheckParameter(data, "os", default=None)
917   start = baserlib.CheckParameter(data, "start", exptype=bool,
918                                   default=True)
919   osparams = baserlib.CheckParameter(data, "osparams", default=None)
920
921   ops = [
922     opcodes.OpInstanceShutdown(instance_name=name),
923     opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
924                                 osparams=osparams),
925     ]
926
927   if start:
928     ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
929
930   return ops
931
932
933 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
934   """/2/instances/[instance_name]/reinstall resource.
935
936   Implements an instance reinstall.
937
938   """
939   POST_OPCODE = opcodes.OpInstanceReinstall
940
941   def POST(self):
942     """Reinstall an instance.
943
944     The URI takes os=name and nostartup=[0|1] optional
945     parameters. By default, the instance will be started
946     automatically.
947
948     """
949     if self.request_body:
950       if self.queryargs:
951         raise http.HttpBadRequest("Can't combine query and body parameters")
952
953       body = self.request_body
954     elif self.queryargs:
955       # Legacy interface, do not modify/extend
956       body = {
957         "os": self._checkStringVariable("os"),
958         "start": not self._checkIntVariable("nostartup"),
959         }
960     else:
961       body = {}
962
963     ops = _ParseInstanceReinstallRequest(self.items[0], body)
964
965     return self.SubmitJob(ops)
966
967
968 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
969   """/2/instances/[instance_name]/replace-disks resource.
970
971   """
972   POST_OPCODE = opcodes.OpInstanceReplaceDisks
973
974   def GetPostOpInput(self):
975     """Replaces disks on an instance.
976
977     """
978     data = self.request_body.copy()
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