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