Add hypervisors ancillary files list
[ganeti-local] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API resource implementations.
23
24 PUT or POST?
25 ============
26
27 According to RFC2616 the main difference between PUT and POST is that
28 POST can create new resources but PUT can only create the resource the
29 URI was pointing to on the PUT request.
30
31 In the context of this module POST on ``/2/instances`` to change an existing
32 entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
33 new instance) with a name specified in the request.
34
35 Quoting from RFC2616, section 9.6::
36
37   The fundamental difference between the POST and PUT requests is reflected in
38   the different meaning of the Request-URI. The URI in a POST request
39   identifies the resource that will handle the enclosed entity. That resource
40   might be a data-accepting process, a gateway to some other protocol, or a
41   separate entity that accepts annotations. In contrast, the URI in a PUT
42   request identifies the entity enclosed with the request -- the user agent
43   knows what URI is intended and the server MUST NOT attempt to apply the
44   request to some other resource. If the server desires that the request be
45   applied to a different URI, it MUST send a 301 (Moved Permanently) response;
46   the user agent MAY then make its own decision regarding whether or not to
47   redirect the request.
48
49 So when adding new methods, if they are operating on the URI entity itself,
50 PUT should be prefered over POST.
51
52 """
53
54 # pylint: disable=C0103
55
56 # C0103: Invalid name, since the R_* names are not conforming
57
58 from ganeti import opcodes
59 from ganeti import http
60 from ganeti import constants
61 from ganeti import cli
62 from ganeti import rapi
63 from ganeti import ht
64 from ganeti import compat
65 from ganeti.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     auto_promote = bool(self._checkIntVariable("auto-promote"))
419
420     if role == _NR_REGULAR:
421       candidate = False
422       offline = False
423       drained = False
424
425     elif role == _NR_MASTER_CANDIATE:
426       candidate = True
427       offline = drained = None
428
429     elif role == _NR_DRAINED:
430       drained = True
431       candidate = offline = None
432
433     elif role == _NR_OFFLINE:
434       offline = True
435       candidate = drained = None
436
437     else:
438       raise http.HttpBadRequest("Can't set '%s' role" % role)
439
440     op = opcodes.OpNodeSetParams(node_name=node_name,
441                                  master_candidate=candidate,
442                                  offline=offline,
443                                  drained=drained,
444                                  auto_promote=auto_promote,
445                                  force=bool(self.useForce()))
446
447     return baserlib.SubmitJob([op])
448
449
450 class R_2_nodes_name_evacuate(baserlib.R_Generic):
451   """/2/nodes/[node_name]/evacuate resource.
452
453   """
454   def POST(self):
455     """Evacuate all instances off a node.
456
457     """
458     op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
459       "node_name": self.items[0],
460       "dry_run": self.dryRun(),
461       })
462
463     return baserlib.SubmitJob([op])
464
465
466 class R_2_nodes_name_migrate(baserlib.R_Generic):
467   """/2/nodes/[node_name]/migrate resource.
468
469   """
470   def POST(self):
471     """Migrate all primary instances from a node.
472
473     """
474     node_name = self.items[0]
475
476     if self.queryargs:
477       # Support old-style requests
478       if "live" in self.queryargs and "mode" in self.queryargs:
479         raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
480                                   " be passed")
481
482       if "live" in self.queryargs:
483         if self._checkIntVariable("live", default=1):
484           mode = constants.HT_MIGRATION_LIVE
485         else:
486           mode = constants.HT_MIGRATION_NONLIVE
487       else:
488         mode = self._checkStringVariable("mode", default=None)
489
490       data = {
491         "mode": mode,
492         }
493     else:
494       data = self.request_body
495
496     op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
497       "node_name": node_name,
498       })
499
500     return baserlib.SubmitJob([op])
501
502
503 class R_2_nodes_name_modify(baserlib.R_Generic):
504   """/2/nodes/[node_name]/modify resource.
505
506   """
507   def POST(self):
508     """Changes parameters of a node.
509
510     @return: a job id
511
512     """
513     baserlib.CheckType(self.request_body, dict, "Body contents")
514
515     op = baserlib.FillOpcode(opcodes.OpNodeSetParams, self.request_body, {
516       "node_name": self.items[0],
517       })
518
519     return baserlib.SubmitJob([op])
520
521
522 class R_2_nodes_name_storage(baserlib.R_Generic):
523   """/2/nodes/[node_name]/storage resource.
524
525   """
526   # LUNodeQueryStorage acquires locks, hence restricting access to GET
527   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
528
529   def GET(self):
530     node_name = self.items[0]
531
532     storage_type = self._checkStringVariable("storage_type", None)
533     if not storage_type:
534       raise http.HttpBadRequest("Missing the required 'storage_type'"
535                                 " parameter")
536
537     output_fields = self._checkStringVariable("output_fields", None)
538     if not output_fields:
539       raise http.HttpBadRequest("Missing the required 'output_fields'"
540                                 " parameter")
541
542     op = opcodes.OpNodeQueryStorage(nodes=[node_name],
543                                     storage_type=storage_type,
544                                     output_fields=output_fields.split(","))
545     return baserlib.SubmitJob([op])
546
547
548 class R_2_nodes_name_storage_modify(baserlib.R_Generic):
549   """/2/nodes/[node_name]/storage/modify resource.
550
551   """
552   def PUT(self):
553     node_name = self.items[0]
554
555     storage_type = self._checkStringVariable("storage_type", None)
556     if not storage_type:
557       raise http.HttpBadRequest("Missing the required 'storage_type'"
558                                 " parameter")
559
560     name = self._checkStringVariable("name", None)
561     if not name:
562       raise http.HttpBadRequest("Missing the required 'name'"
563                                 " parameter")
564
565     changes = {}
566
567     if "allocatable" in self.queryargs:
568       changes[constants.SF_ALLOCATABLE] = \
569         bool(self._checkIntVariable("allocatable", default=1))
570
571     op = opcodes.OpNodeModifyStorage(node_name=node_name,
572                                      storage_type=storage_type,
573                                      name=name,
574                                      changes=changes)
575     return baserlib.SubmitJob([op])
576
577
578 class R_2_nodes_name_storage_repair(baserlib.R_Generic):
579   """/2/nodes/[node_name]/storage/repair resource.
580
581   """
582   def PUT(self):
583     node_name = self.items[0]
584
585     storage_type = self._checkStringVariable("storage_type", None)
586     if not storage_type:
587       raise http.HttpBadRequest("Missing the required 'storage_type'"
588                                 " parameter")
589
590     name = self._checkStringVariable("name", None)
591     if not name:
592       raise http.HttpBadRequest("Missing the required 'name'"
593                                 " parameter")
594
595     op = opcodes.OpRepairNodeStorage(node_name=node_name,
596                                      storage_type=storage_type,
597                                      name=name)
598     return baserlib.SubmitJob([op])
599
600
601 def _ParseCreateGroupRequest(data, dry_run):
602   """Parses a request for creating a node group.
603
604   @rtype: L{opcodes.OpGroupAdd}
605   @return: Group creation opcode
606
607   """
608   override = {
609     "dry_run": dry_run,
610     }
611
612   rename = {
613     "name": "group_name",
614     }
615
616   return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
617                              rename=rename)
618
619
620 class R_2_groups(baserlib.R_Generic):
621   """/2/groups resource.
622
623   """
624   def GET(self):
625     """Returns a list of all node groups.
626
627     """
628     client = baserlib.GetClient()
629
630     if self.useBulk():
631       bulkdata = client.QueryGroups([], G_FIELDS, False)
632       return baserlib.MapBulkFields(bulkdata, G_FIELDS)
633     else:
634       data = client.QueryGroups([], ["name"], False)
635       groupnames = [row[0] for row in data]
636       return baserlib.BuildUriList(groupnames, "/2/groups/%s",
637                                    uri_fields=("name", "uri"))
638
639   def POST(self):
640     """Create a node group.
641
642     @return: a job id
643
644     """
645     baserlib.CheckType(self.request_body, dict, "Body contents")
646     op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
647     return baserlib.SubmitJob([op])
648
649
650 class R_2_groups_name(baserlib.R_Generic):
651   """/2/groups/[group_name] resource.
652
653   """
654   def GET(self):
655     """Send information about a node group.
656
657     """
658     group_name = self.items[0]
659     client = baserlib.GetClient()
660
661     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
662                                             names=[group_name], fields=G_FIELDS,
663                                             use_locking=self.useLocking())
664
665     return baserlib.MapFields(G_FIELDS, result[0])
666
667   def DELETE(self):
668     """Delete a node group.
669
670     """
671     op = opcodes.OpGroupRemove(group_name=self.items[0],
672                                dry_run=bool(self.dryRun()))
673
674     return baserlib.SubmitJob([op])
675
676
677 def _ParseModifyGroupRequest(name, data):
678   """Parses a request for modifying a node group.
679
680   @rtype: L{opcodes.OpGroupSetParams}
681   @return: Group modify opcode
682
683   """
684   return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
685     "group_name": name,
686     })
687
688
689 class R_2_groups_name_modify(baserlib.R_Generic):
690   """/2/groups/[group_name]/modify resource.
691
692   """
693   def PUT(self):
694     """Changes some parameters of node group.
695
696     @return: a job id
697
698     """
699     baserlib.CheckType(self.request_body, dict, "Body contents")
700
701     op = _ParseModifyGroupRequest(self.items[0], self.request_body)
702
703     return baserlib.SubmitJob([op])
704
705
706 def _ParseRenameGroupRequest(name, data, dry_run):
707   """Parses a request for renaming a node group.
708
709   @type name: string
710   @param name: name of the node group to rename
711   @type data: dict
712   @param data: the body received by the rename request
713   @type dry_run: bool
714   @param dry_run: whether to perform a dry run
715
716   @rtype: L{opcodes.OpGroupRename}
717   @return: Node group rename opcode
718
719   """
720   return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
721     "group_name": name,
722     "dry_run": dry_run,
723     })
724
725
726 class R_2_groups_name_rename(baserlib.R_Generic):
727   """/2/groups/[group_name]/rename resource.
728
729   """
730   def PUT(self):
731     """Changes the name of a node group.
732
733     @return: a job id
734
735     """
736     baserlib.CheckType(self.request_body, dict, "Body contents")
737     op = _ParseRenameGroupRequest(self.items[0], self.request_body,
738                                   self.dryRun())
739     return baserlib.SubmitJob([op])
740
741
742 class R_2_groups_name_assign_nodes(baserlib.R_Generic):
743   """/2/groups/[group_name]/assign-nodes resource.
744
745   """
746   def PUT(self):
747     """Assigns nodes to a group.
748
749     @return: a job id
750
751     """
752     op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
753       "group_name": self.items[0],
754       "dry_run": self.dryRun(),
755       "force": self.useForce(),
756       })
757
758     return baserlib.SubmitJob([op])
759
760
761 def _ParseInstanceCreateRequestVersion1(data, dry_run):
762   """Parses an instance creation request version 1.
763
764   @rtype: L{opcodes.OpInstanceCreate}
765   @return: Instance creation opcode
766
767   """
768   override = {
769     "dry_run": dry_run,
770     }
771
772   rename = {
773     "os": "os_type",
774     "name": "instance_name",
775     }
776
777   return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
778                              rename=rename)
779
780
781 class R_2_instances(baserlib.R_Generic):
782   """/2/instances resource.
783
784   """
785   def GET(self):
786     """Returns a list of all available instances.
787
788     """
789     client = baserlib.GetClient()
790
791     use_locking = self.useLocking()
792     if self.useBulk():
793       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
794       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
795     else:
796       instancesdata = client.QueryInstances([], ["name"], use_locking)
797       instanceslist = [row[0] for row in instancesdata]
798       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
799                                    uri_fields=("id", "uri"))
800
801   def POST(self):
802     """Create an instance.
803
804     @return: a job id
805
806     """
807     if not isinstance(self.request_body, dict):
808       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
809
810     # Default to request data version 0
811     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
812
813     if data_version == 0:
814       raise http.HttpBadRequest("Instance creation request version 0 is no"
815                                 " longer supported")
816     elif data_version == 1:
817       data = self.request_body.copy()
818       # Remove "__version__"
819       data.pop(_REQ_DATA_VERSION, None)
820       op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
821     else:
822       raise http.HttpBadRequest("Unsupported request data version %s" %
823                                 data_version)
824
825     return baserlib.SubmitJob([op])
826
827
828 class R_2_instances_name(baserlib.R_Generic):
829   """/2/instances/[instance_name] resource.
830
831   """
832   def GET(self):
833     """Send information about an instance.
834
835     """
836     client = baserlib.GetClient()
837     instance_name = self.items[0]
838
839     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
840                                             names=[instance_name],
841                                             fields=I_FIELDS,
842                                             use_locking=self.useLocking())
843
844     return baserlib.MapFields(I_FIELDS, result[0])
845
846   def DELETE(self):
847     """Delete an instance.
848
849     """
850     op = opcodes.OpInstanceRemove(instance_name=self.items[0],
851                                   ignore_failures=False,
852                                   dry_run=bool(self.dryRun()))
853     return baserlib.SubmitJob([op])
854
855
856 class R_2_instances_name_info(baserlib.R_Generic):
857   """/2/instances/[instance_name]/info resource.
858
859   """
860   def GET(self):
861     """Request detailed instance information.
862
863     """
864     instance_name = self.items[0]
865     static = bool(self._checkIntVariable("static", default=0))
866
867     op = opcodes.OpInstanceQueryData(instances=[instance_name],
868                                      static=static)
869     return baserlib.SubmitJob([op])
870
871
872 class R_2_instances_name_reboot(baserlib.R_Generic):
873   """/2/instances/[instance_name]/reboot resource.
874
875   Implements an instance reboot.
876
877   """
878   def POST(self):
879     """Reboot an instance.
880
881     The URI takes type=[hard|soft|full] and
882     ignore_secondaries=[False|True] parameters.
883
884     """
885     instance_name = self.items[0]
886     reboot_type = self.queryargs.get("type",
887                                      [constants.INSTANCE_REBOOT_HARD])[0]
888     ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
889     op = opcodes.OpInstanceReboot(instance_name=instance_name,
890                                   reboot_type=reboot_type,
891                                   ignore_secondaries=ignore_secondaries,
892                                   dry_run=bool(self.dryRun()))
893
894     return baserlib.SubmitJob([op])
895
896
897 class R_2_instances_name_startup(baserlib.R_Generic):
898   """/2/instances/[instance_name]/startup resource.
899
900   Implements an instance startup.
901
902   """
903   def PUT(self):
904     """Startup an instance.
905
906     The URI takes force=[False|True] parameter to start the instance
907     if even if secondary disks are failing.
908
909     """
910     instance_name = self.items[0]
911     force_startup = bool(self._checkIntVariable("force"))
912     no_remember = bool(self._checkIntVariable("no_remember"))
913     op = opcodes.OpInstanceStartup(instance_name=instance_name,
914                                    force=force_startup,
915                                    dry_run=bool(self.dryRun()),
916                                    no_remember=no_remember)
917
918     return baserlib.SubmitJob([op])
919
920
921 def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
922   """Parses a request for an instance shutdown.
923
924   @rtype: L{opcodes.OpInstanceShutdown}
925   @return: Instance shutdown opcode
926
927   """
928   return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
929     "instance_name": name,
930     "dry_run": dry_run,
931     "no_remember": no_remember,
932     })
933
934
935 class R_2_instances_name_shutdown(baserlib.R_Generic):
936   """/2/instances/[instance_name]/shutdown resource.
937
938   Implements an instance shutdown.
939
940   """
941   def PUT(self):
942     """Shutdown an instance.
943
944     @return: a job id
945
946     """
947     no_remember = bool(self._checkIntVariable("no_remember"))
948     op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
949                                        bool(self.dryRun()), no_remember)
950
951     return baserlib.SubmitJob([op])
952
953
954 def _ParseInstanceReinstallRequest(name, data):
955   """Parses a request for reinstalling an instance.
956
957   """
958   if not isinstance(data, dict):
959     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
960
961   ostype = baserlib.CheckParameter(data, "os", default=None)
962   start = baserlib.CheckParameter(data, "start", exptype=bool,
963                                   default=True)
964   osparams = baserlib.CheckParameter(data, "osparams", default=None)
965
966   ops = [
967     opcodes.OpInstanceShutdown(instance_name=name),
968     opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
969                                 osparams=osparams),
970     ]
971
972   if start:
973     ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
974
975   return ops
976
977
978 class R_2_instances_name_reinstall(baserlib.R_Generic):
979   """/2/instances/[instance_name]/reinstall resource.
980
981   Implements an instance reinstall.
982
983   """
984   def POST(self):
985     """Reinstall an instance.
986
987     The URI takes os=name and nostartup=[0|1] optional
988     parameters. By default, the instance will be started
989     automatically.
990
991     """
992     if self.request_body:
993       if self.queryargs:
994         raise http.HttpBadRequest("Can't combine query and body parameters")
995
996       body = self.request_body
997     elif self.queryargs:
998       # Legacy interface, do not modify/extend
999       body = {
1000         "os": self._checkStringVariable("os"),
1001         "start": not self._checkIntVariable("nostartup"),
1002         }
1003     else:
1004       body = {}
1005
1006     ops = _ParseInstanceReinstallRequest(self.items[0], body)
1007
1008     return baserlib.SubmitJob(ops)
1009
1010
1011 def _ParseInstanceReplaceDisksRequest(name, data):
1012   """Parses a request for an instance export.
1013
1014   @rtype: L{opcodes.OpInstanceReplaceDisks}
1015   @return: Instance export opcode
1016
1017   """
1018   override = {
1019     "instance_name": name,
1020     }
1021
1022   # Parse disks
1023   try:
1024     raw_disks = data["disks"]
1025   except KeyError:
1026     pass
1027   else:
1028     if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1029       # Backwards compatibility for strings of the format "1, 2, 3"
1030       try:
1031         data["disks"] = [int(part) for part in raw_disks.split(",")]
1032       except (TypeError, ValueError), err:
1033         raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1034
1035   return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1036
1037
1038 class R_2_instances_name_replace_disks(baserlib.R_Generic):
1039   """/2/instances/[instance_name]/replace-disks resource.
1040
1041   """
1042   def POST(self):
1043     """Replaces disks on an instance.
1044
1045     """
1046     op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1047
1048     return baserlib.SubmitJob([op])
1049
1050
1051 class R_2_instances_name_activate_disks(baserlib.R_Generic):
1052   """/2/instances/[instance_name]/activate-disks resource.
1053
1054   """
1055   def PUT(self):
1056     """Activate disks for an instance.
1057
1058     The URI might contain ignore_size to ignore current recorded size.
1059
1060     """
1061     instance_name = self.items[0]
1062     ignore_size = bool(self._checkIntVariable("ignore_size"))
1063
1064     op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1065                                          ignore_size=ignore_size)
1066
1067     return baserlib.SubmitJob([op])
1068
1069
1070 class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1071   """/2/instances/[instance_name]/deactivate-disks resource.
1072
1073   """
1074   def PUT(self):
1075     """Deactivate disks for an instance.
1076
1077     """
1078     instance_name = self.items[0]
1079
1080     op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1081
1082     return baserlib.SubmitJob([op])
1083
1084
1085 class R_2_instances_name_prepare_export(baserlib.R_Generic):
1086   """/2/instances/[instance_name]/prepare-export resource.
1087
1088   """
1089   def PUT(self):
1090     """Prepares an export for an instance.
1091
1092     @return: a job id
1093
1094     """
1095     instance_name = self.items[0]
1096     mode = self._checkStringVariable("mode")
1097
1098     op = opcodes.OpBackupPrepare(instance_name=instance_name,
1099                                  mode=mode)
1100
1101     return baserlib.SubmitJob([op])
1102
1103
1104 def _ParseExportInstanceRequest(name, data):
1105   """Parses a request for an instance export.
1106
1107   @rtype: L{opcodes.OpBackupExport}
1108   @return: Instance export opcode
1109
1110   """
1111   # Rename "destination" to "target_node"
1112   try:
1113     data["target_node"] = data.pop("destination")
1114   except KeyError:
1115     pass
1116
1117   return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1118     "instance_name": name,
1119     })
1120
1121
1122 class R_2_instances_name_export(baserlib.R_Generic):
1123   """/2/instances/[instance_name]/export resource.
1124
1125   """
1126   def PUT(self):
1127     """Exports an instance.
1128
1129     @return: a job id
1130
1131     """
1132     if not isinstance(self.request_body, dict):
1133       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1134
1135     op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1136
1137     return baserlib.SubmitJob([op])
1138
1139
1140 def _ParseMigrateInstanceRequest(name, data):
1141   """Parses a request for an instance migration.
1142
1143   @rtype: L{opcodes.OpInstanceMigrate}
1144   @return: Instance migration opcode
1145
1146   """
1147   return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1148     "instance_name": name,
1149     })
1150
1151
1152 class R_2_instances_name_migrate(baserlib.R_Generic):
1153   """/2/instances/[instance_name]/migrate resource.
1154
1155   """
1156   def PUT(self):
1157     """Migrates an instance.
1158
1159     @return: a job id
1160
1161     """
1162     baserlib.CheckType(self.request_body, dict, "Body contents")
1163
1164     op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1165
1166     return baserlib.SubmitJob([op])
1167
1168
1169 class R_2_instances_name_failover(baserlib.R_Generic):
1170   """/2/instances/[instance_name]/failover resource.
1171
1172   """
1173   def PUT(self):
1174     """Does a failover of an instance.
1175
1176     @return: a job id
1177
1178     """
1179     baserlib.CheckType(self.request_body, dict, "Body contents")
1180
1181     op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1182       "instance_name": self.items[0],
1183       })
1184
1185     return baserlib.SubmitJob([op])
1186
1187
1188 def _ParseRenameInstanceRequest(name, data):
1189   """Parses a request for renaming an instance.
1190
1191   @rtype: L{opcodes.OpInstanceRename}
1192   @return: Instance rename opcode
1193
1194   """
1195   return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1196     "instance_name": name,
1197     })
1198
1199
1200 class R_2_instances_name_rename(baserlib.R_Generic):
1201   """/2/instances/[instance_name]/rename resource.
1202
1203   """
1204   def PUT(self):
1205     """Changes the name of an instance.
1206
1207     @return: a job id
1208
1209     """
1210     baserlib.CheckType(self.request_body, dict, "Body contents")
1211
1212     op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1213
1214     return baserlib.SubmitJob([op])
1215
1216
1217 def _ParseModifyInstanceRequest(name, data):
1218   """Parses a request for modifying an instance.
1219
1220   @rtype: L{opcodes.OpInstanceSetParams}
1221   @return: Instance modify opcode
1222
1223   """
1224   return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1225     "instance_name": name,
1226     })
1227
1228
1229 class R_2_instances_name_modify(baserlib.R_Generic):
1230   """/2/instances/[instance_name]/modify resource.
1231
1232   """
1233   def PUT(self):
1234     """Changes some parameters of an instance.
1235
1236     @return: a job id
1237
1238     """
1239     baserlib.CheckType(self.request_body, dict, "Body contents")
1240
1241     op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1242
1243     return baserlib.SubmitJob([op])
1244
1245
1246 class R_2_instances_name_disk_grow(baserlib.R_Generic):
1247   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1248
1249   """
1250   def POST(self):
1251     """Increases the size of an instance disk.
1252
1253     @return: a job id
1254
1255     """
1256     op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1257       "instance_name": self.items[0],
1258       "disk": int(self.items[1]),
1259       })
1260
1261     return baserlib.SubmitJob([op])
1262
1263
1264 class R_2_instances_name_console(baserlib.R_Generic):
1265   """/2/instances/[instance_name]/console resource.
1266
1267   """
1268   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1269
1270   def GET(self):
1271     """Request information for connecting to instance's console.
1272
1273     @return: Serialized instance console description, see
1274              L{objects.InstanceConsole}
1275
1276     """
1277     client = baserlib.GetClient()
1278
1279     ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1280
1281     if console is None:
1282       raise http.HttpServiceUnavailable("Instance console unavailable")
1283
1284     assert isinstance(console, dict)
1285     return console
1286
1287
1288 def _GetQueryFields(args):
1289   """
1290
1291   """
1292   try:
1293     fields = args["fields"]
1294   except KeyError:
1295     raise http.HttpBadRequest("Missing 'fields' query argument")
1296
1297   return _SplitQueryFields(fields[0])
1298
1299
1300 def _SplitQueryFields(fields):
1301   """
1302
1303   """
1304   return [i.strip() for i in fields.split(",")]
1305
1306
1307 class R_2_query(baserlib.R_Generic):
1308   """/2/query/[resource] resource.
1309
1310   """
1311   # Results might contain sensitive information
1312   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1313
1314   def _Query(self, fields, filter_):
1315     return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1316
1317   def GET(self):
1318     """Returns resource information.
1319
1320     @return: Query result, see L{objects.QueryResponse}
1321
1322     """
1323     return self._Query(_GetQueryFields(self.queryargs), None)
1324
1325   def PUT(self):
1326     """Submits job querying for resources.
1327
1328     @return: Query result, see L{objects.QueryResponse}
1329
1330     """
1331     body = self.request_body
1332
1333     baserlib.CheckType(body, dict, "Body contents")
1334
1335     try:
1336       fields = body["fields"]
1337     except KeyError:
1338       fields = _GetQueryFields(self.queryargs)
1339
1340     return self._Query(fields, self.request_body.get("filter", None))
1341
1342
1343 class R_2_query_fields(baserlib.R_Generic):
1344   """/2/query/[resource]/fields resource.
1345
1346   """
1347   def GET(self):
1348     """Retrieves list of available fields for a resource.
1349
1350     @return: List of serialized L{objects.QueryFieldDefinition}
1351
1352     """
1353     try:
1354       raw_fields = self.queryargs["fields"]
1355     except KeyError:
1356       fields = None
1357     else:
1358       fields = _SplitQueryFields(raw_fields[0])
1359
1360     return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1361
1362
1363 class _R_Tags(baserlib.R_Generic):
1364   """ Quasiclass for tagging resources
1365
1366   Manages tags. When inheriting this class you must define the
1367   TAG_LEVEL for it.
1368
1369   """
1370   TAG_LEVEL = None
1371
1372   def __init__(self, items, queryargs, req):
1373     """A tag resource constructor.
1374
1375     We have to override the default to sort out cluster naming case.
1376
1377     """
1378     baserlib.R_Generic.__init__(self, items, queryargs, req)
1379
1380     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1381       self.name = None
1382     else:
1383       self.name = items[0]
1384
1385   def GET(self):
1386     """Returns a list of tags.
1387
1388     Example: ["tag1", "tag2", "tag3"]
1389
1390     """
1391     # pylint: disable=W0212
1392     return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1393
1394   def PUT(self):
1395     """Add a set of tags.
1396
1397     The request as a list of strings should be PUT to this URI. And
1398     you'll have back a job id.
1399
1400     """
1401     # pylint: disable=W0212
1402     if "tag" not in self.queryargs:
1403       raise http.HttpBadRequest("Please specify tag(s) to add using the"
1404                                 " the 'tag' parameter")
1405     return baserlib._Tags_PUT(self.TAG_LEVEL,
1406                               self.queryargs["tag"], name=self.name,
1407                               dry_run=bool(self.dryRun()))
1408
1409   def DELETE(self):
1410     """Delete a tag.
1411
1412     In order to delete a set of tags, the DELETE
1413     request should be addressed to URI like:
1414     /tags?tag=[tag]&tag=[tag]
1415
1416     """
1417     # pylint: disable=W0212
1418     if "tag" not in self.queryargs:
1419       # no we not gonna delete all tags
1420       raise http.HttpBadRequest("Cannot delete all tags - please specify"
1421                                 " tag(s) using the 'tag' parameter")
1422     return baserlib._Tags_DELETE(self.TAG_LEVEL,
1423                                  self.queryargs["tag"],
1424                                  name=self.name,
1425                                  dry_run=bool(self.dryRun()))
1426
1427
1428 class R_2_instances_name_tags(_R_Tags):
1429   """ /2/instances/[instance_name]/tags resource.
1430
1431   Manages per-instance tags.
1432
1433   """
1434   TAG_LEVEL = constants.TAG_INSTANCE
1435
1436
1437 class R_2_nodes_name_tags(_R_Tags):
1438   """ /2/nodes/[node_name]/tags resource.
1439
1440   Manages per-node tags.
1441
1442   """
1443   TAG_LEVEL = constants.TAG_NODE
1444
1445
1446 class R_2_groups_name_tags(_R_Tags):
1447   """ /2/groups/[group_name]/tags resource.
1448
1449   Manages per-nodegroup tags.
1450
1451   """
1452   TAG_LEVEL = constants.TAG_NODEGROUP
1453
1454
1455 class R_2_tags(_R_Tags):
1456   """ /2/tags resource.
1457
1458   Manages cluster tags.
1459
1460   """
1461   TAG_LEVEL = constants.TAG_CLUSTER