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