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