rlib2: Convert /2/instances/[inst]/info 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_CANDIATE = "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_CANDIATE,
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.ResourceBase):
387   """ /2/nodes/[node_name]/role resource.
388
389   """
390   def GET(self):
391     """Returns the current node role.
392
393     @return: Node role
394
395     """
396     node_name = self.items[0]
397     client = self.GetClient()
398     result = client.QueryNodes(names=[node_name], fields=["role"],
399                                use_locking=self.useLocking())
400
401     return _NR_MAP[result[0][0]]
402
403   def PUT(self):
404     """Sets the node role.
405
406     @return: a job id
407
408     """
409     if not isinstance(self.request_body, basestring):
410       raise http.HttpBadRequest("Invalid body contents, not a string")
411
412     node_name = self.items[0]
413     role = self.request_body
414
415     if role == _NR_REGULAR:
416       candidate = False
417       offline = False
418       drained = False
419
420     elif role == _NR_MASTER_CANDIATE:
421       candidate = True
422       offline = drained = None
423
424     elif role == _NR_DRAINED:
425       drained = True
426       candidate = offline = None
427
428     elif role == _NR_OFFLINE:
429       offline = True
430       candidate = drained = None
431
432     else:
433       raise http.HttpBadRequest("Can't set '%s' role" % role)
434
435     op = opcodes.OpNodeSetParams(node_name=node_name,
436                                  master_candidate=candidate,
437                                  offline=offline,
438                                  drained=drained,
439                                  force=bool(self.useForce()))
440
441     return self.SubmitJob([op])
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.ResourceBase):
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
502   def GET(self):
503     node_name = self.items[0]
504
505     storage_type = self._checkStringVariable("storage_type", None)
506     if not storage_type:
507       raise http.HttpBadRequest("Missing the required 'storage_type'"
508                                 " parameter")
509
510     output_fields = self._checkStringVariable("output_fields", None)
511     if not output_fields:
512       raise http.HttpBadRequest("Missing the required 'output_fields'"
513                                 " parameter")
514
515     op = opcodes.OpNodeQueryStorage(nodes=[node_name],
516                                     storage_type=storage_type,
517                                     output_fields=output_fields.split(","))
518     return self.SubmitJob([op])
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.ResourceBase):
609   """/2/groups/[group_name] resource.
610
611   """
612   def GET(self):
613     """Send information about a node group.
614
615     """
616     group_name = self.items[0]
617     client = self.GetClient()
618
619     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
620                                             names=[group_name], fields=G_FIELDS,
621                                             use_locking=self.useLocking())
622
623     return baserlib.MapFields(G_FIELDS, result[0])
624
625   def DELETE(self):
626     """Delete a node group.
627
628     """
629     op = opcodes.OpGroupRemove(group_name=self.items[0],
630                                dry_run=bool(self.dryRun()))
631
632     return self.SubmitJob([op])
633
634
635 class R_2_groups_name_modify(baserlib.OpcodeResource):
636   """/2/groups/[group_name]/modify resource.
637
638   """
639   PUT_OPCODE = opcodes.OpGroupSetParams
640
641   def GetPutOpInput(self):
642     """Changes some parameters of node group.
643
644     """
645     assert self.items
646     return (self.request_body, {
647       "group_name": self.items[0],
648       })
649
650
651 class R_2_groups_name_rename(baserlib.OpcodeResource):
652   """/2/groups/[group_name]/rename resource.
653
654   """
655   PUT_OPCODE = opcodes.OpGroupRename
656
657   def GetPutOpInput(self):
658     """Changes the name of a node group.
659
660     """
661     assert len(self.items) == 1
662     return (self.request_body, {
663       "group_name": self.items[0],
664       "dry_run": self.dryRun(),
665       })
666
667
668 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
669   """/2/groups/[group_name]/assign-nodes resource.
670
671   """
672   PUT_OPCODE = opcodes.OpGroupAssignNodes
673
674   def GetPutOpInput(self):
675     """Assigns nodes to a group.
676
677     """
678     assert len(self.items) == 1
679     return (self.request_body, {
680       "group_name": self.items[0],
681       "dry_run": self.dryRun(),
682       "force": self.useForce(),
683       })
684
685
686 def _ParseInstanceCreateRequestVersion1(data, dry_run):
687   """Parses an instance creation request version 1.
688
689   @rtype: L{opcodes.OpInstanceCreate}
690   @return: Instance creation opcode
691
692   """
693   override = {
694     "dry_run": dry_run,
695     }
696
697   rename = {
698     "os": "os_type",
699     "name": "instance_name",
700     }
701
702   return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
703                              rename=rename)
704
705
706 class R_2_instances(baserlib.ResourceBase):
707   """/2/instances resource.
708
709   """
710   def GET(self):
711     """Returns a list of all available instances.
712
713     """
714     client = self.GetClient()
715
716     use_locking = self.useLocking()
717     if self.useBulk():
718       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
719       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
720     else:
721       instancesdata = client.QueryInstances([], ["name"], use_locking)
722       instanceslist = [row[0] for row in instancesdata]
723       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
724                                    uri_fields=("id", "uri"))
725
726   def POST(self):
727     """Create an instance.
728
729     @return: a job id
730
731     """
732     if not isinstance(self.request_body, dict):
733       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
734
735     # Default to request data version 0
736     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
737
738     if data_version == 0:
739       raise http.HttpBadRequest("Instance creation request version 0 is no"
740                                 " longer supported")
741     elif data_version == 1:
742       data = self.request_body.copy()
743       # Remove "__version__"
744       data.pop(_REQ_DATA_VERSION, None)
745       op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
746     else:
747       raise http.HttpBadRequest("Unsupported request data version %s" %
748                                 data_version)
749
750     return self.SubmitJob([op])
751
752
753 class R_2_instances_name(baserlib.OpcodeResource):
754   """/2/instances/[instance_name] resource.
755
756   """
757   DELETE_OPCODE = opcodes.OpInstanceRemove
758
759   def GET(self):
760     """Send information about an instance.
761
762     """
763     client = self.GetClient()
764     instance_name = self.items[0]
765
766     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
767                                             names=[instance_name],
768                                             fields=I_FIELDS,
769                                             use_locking=self.useLocking())
770
771     return baserlib.MapFields(I_FIELDS, result[0])
772
773   def GetDeleteOpInput(self):
774     """Delete an instance.
775
776     """
777     assert len(self.items) == 1
778     return ({}, {
779       "instance_name": self.items[0],
780       "ignore_failures": False,
781       "dry_run": self.dryRun(),
782       })
783
784
785 class R_2_instances_name_info(baserlib.OpcodeResource):
786   """/2/instances/[instance_name]/info resource.
787
788   """
789   GET_OPCODE = opcodes.OpInstanceQueryData
790
791   def GetGetOpInput(self):
792     """Request detailed instance information.
793
794     """
795     assert len(self.items) == 1
796     return ({}, {
797       "instances": [self.items[0]],
798       "static": bool(self._checkIntVariable("static", default=0)),
799       })
800
801
802 class R_2_instances_name_reboot(baserlib.ResourceBase):
803   """/2/instances/[instance_name]/reboot resource.
804
805   Implements an instance reboot.
806
807   """
808   def POST(self):
809     """Reboot an instance.
810
811     The URI takes type=[hard|soft|full] and
812     ignore_secondaries=[False|True] parameters.
813
814     """
815     instance_name = self.items[0]
816     reboot_type = self.queryargs.get("type",
817                                      [constants.INSTANCE_REBOOT_HARD])[0]
818     ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
819     op = opcodes.OpInstanceReboot(instance_name=instance_name,
820                                   reboot_type=reboot_type,
821                                   ignore_secondaries=ignore_secondaries,
822                                   dry_run=bool(self.dryRun()))
823
824     return self.SubmitJob([op])
825
826
827 class R_2_instances_name_startup(baserlib.ResourceBase):
828   """/2/instances/[instance_name]/startup resource.
829
830   Implements an instance startup.
831
832   """
833   def PUT(self):
834     """Startup an instance.
835
836     The URI takes force=[False|True] parameter to start the instance
837     if even if secondary disks are failing.
838
839     """
840     instance_name = self.items[0]
841     force_startup = bool(self._checkIntVariable("force"))
842     no_remember = bool(self._checkIntVariable("no_remember"))
843     op = opcodes.OpInstanceStartup(instance_name=instance_name,
844                                    force=force_startup,
845                                    dry_run=bool(self.dryRun()),
846                                    no_remember=no_remember)
847
848     return self.SubmitJob([op])
849
850
851 def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
852   """Parses a request for an instance shutdown.
853
854   @rtype: L{opcodes.OpInstanceShutdown}
855   @return: Instance shutdown opcode
856
857   """
858   return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
859     "instance_name": name,
860     "dry_run": dry_run,
861     "no_remember": no_remember,
862     })
863
864
865 class R_2_instances_name_shutdown(baserlib.ResourceBase):
866   """/2/instances/[instance_name]/shutdown resource.
867
868   Implements an instance shutdown.
869
870   """
871   def PUT(self):
872     """Shutdown an instance.
873
874     @return: a job id
875
876     """
877     baserlib.CheckType(self.request_body, dict, "Body contents")
878
879     no_remember = bool(self._checkIntVariable("no_remember"))
880     op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
881                                        bool(self.dryRun()), no_remember)
882
883     return self.SubmitJob([op])
884
885
886 def _ParseInstanceReinstallRequest(name, data):
887   """Parses a request for reinstalling an instance.
888
889   """
890   if not isinstance(data, dict):
891     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
892
893   ostype = baserlib.CheckParameter(data, "os", default=None)
894   start = baserlib.CheckParameter(data, "start", exptype=bool,
895                                   default=True)
896   osparams = baserlib.CheckParameter(data, "osparams", default=None)
897
898   ops = [
899     opcodes.OpInstanceShutdown(instance_name=name),
900     opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
901                                 osparams=osparams),
902     ]
903
904   if start:
905     ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
906
907   return ops
908
909
910 class R_2_instances_name_reinstall(baserlib.ResourceBase):
911   """/2/instances/[instance_name]/reinstall resource.
912
913   Implements an instance reinstall.
914
915   """
916   def POST(self):
917     """Reinstall an instance.
918
919     The URI takes os=name and nostartup=[0|1] optional
920     parameters. By default, the instance will be started
921     automatically.
922
923     """
924     if self.request_body:
925       if self.queryargs:
926         raise http.HttpBadRequest("Can't combine query and body parameters")
927
928       body = self.request_body
929     elif self.queryargs:
930       # Legacy interface, do not modify/extend
931       body = {
932         "os": self._checkStringVariable("os"),
933         "start": not self._checkIntVariable("nostartup"),
934         }
935     else:
936       body = {}
937
938     ops = _ParseInstanceReinstallRequest(self.items[0], body)
939
940     return self.SubmitJob(ops)
941
942
943 def _ParseInstanceReplaceDisksRequest(name, data):
944   """Parses a request for an instance export.
945
946   @rtype: L{opcodes.OpInstanceReplaceDisks}
947   @return: Instance export opcode
948
949   """
950   override = {
951     "instance_name": name,
952     }
953
954   # Parse disks
955   try:
956     raw_disks = data["disks"]
957   except KeyError:
958     pass
959   else:
960     if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
961       # Backwards compatibility for strings of the format "1, 2, 3"
962       try:
963         data["disks"] = [int(part) for part in raw_disks.split(",")]
964       except (TypeError, ValueError), err:
965         raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
966
967   return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
968
969
970 class R_2_instances_name_replace_disks(baserlib.ResourceBase):
971   """/2/instances/[instance_name]/replace-disks resource.
972
973   """
974   def POST(self):
975     """Replaces disks on an instance.
976
977     """
978     op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
979
980     return self.SubmitJob([op])
981
982
983 class R_2_instances_name_activate_disks(baserlib.ResourceBase):
984   """/2/instances/[instance_name]/activate-disks resource.
985
986   """
987   def PUT(self):
988     """Activate disks for an instance.
989
990     The URI might contain ignore_size to ignore current recorded size.
991
992     """
993     instance_name = self.items[0]
994     ignore_size = bool(self._checkIntVariable("ignore_size"))
995
996     op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
997                                          ignore_size=ignore_size)
998
999     return self.SubmitJob([op])
1000
1001
1002 class R_2_instances_name_deactivate_disks(baserlib.ResourceBase):
1003   """/2/instances/[instance_name]/deactivate-disks resource.
1004
1005   """
1006   def PUT(self):
1007     """Deactivate disks for an instance.
1008
1009     """
1010     instance_name = self.items[0]
1011
1012     op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1013
1014     return self.SubmitJob([op])
1015
1016
1017 class R_2_instances_name_prepare_export(baserlib.ResourceBase):
1018   """/2/instances/[instance_name]/prepare-export resource.
1019
1020   """
1021   def PUT(self):
1022     """Prepares an export for an instance.
1023
1024     @return: a job id
1025
1026     """
1027     instance_name = self.items[0]
1028     mode = self._checkStringVariable("mode")
1029
1030     op = opcodes.OpBackupPrepare(instance_name=instance_name,
1031                                  mode=mode)
1032
1033     return self.SubmitJob([op])
1034
1035
1036 def _ParseExportInstanceRequest(name, data):
1037   """Parses a request for an instance export.
1038
1039   @rtype: L{opcodes.OpBackupExport}
1040   @return: Instance export opcode
1041
1042   """
1043   # Rename "destination" to "target_node"
1044   try:
1045     data["target_node"] = data.pop("destination")
1046   except KeyError:
1047     pass
1048
1049   return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1050     "instance_name": name,
1051     })
1052
1053
1054 class R_2_instances_name_export(baserlib.ResourceBase):
1055   """/2/instances/[instance_name]/export resource.
1056
1057   """
1058   def PUT(self):
1059     """Exports an instance.
1060
1061     @return: a job id
1062
1063     """
1064     if not isinstance(self.request_body, dict):
1065       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1066
1067     op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1068
1069     return self.SubmitJob([op])
1070
1071
1072 def _ParseMigrateInstanceRequest(name, data):
1073   """Parses a request for an instance migration.
1074
1075   @rtype: L{opcodes.OpInstanceMigrate}
1076   @return: Instance migration opcode
1077
1078   """
1079   return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1080     "instance_name": name,
1081     })
1082
1083
1084 class R_2_instances_name_migrate(baserlib.ResourceBase):
1085   """/2/instances/[instance_name]/migrate resource.
1086
1087   """
1088   def PUT(self):
1089     """Migrates an instance.
1090
1091     @return: a job id
1092
1093     """
1094     baserlib.CheckType(self.request_body, dict, "Body contents")
1095
1096     op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1097
1098     return self.SubmitJob([op])
1099
1100
1101 class R_2_instances_name_failover(baserlib.ResourceBase):
1102   """/2/instances/[instance_name]/failover resource.
1103
1104   """
1105   def PUT(self):
1106     """Does a failover of an instance.
1107
1108     @return: a job id
1109
1110     """
1111     baserlib.CheckType(self.request_body, dict, "Body contents")
1112
1113     op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1114       "instance_name": self.items[0],
1115       })
1116
1117     return self.SubmitJob([op])
1118
1119
1120 def _ParseRenameInstanceRequest(name, data):
1121   """Parses a request for renaming an instance.
1122
1123   @rtype: L{opcodes.OpInstanceRename}
1124   @return: Instance rename opcode
1125
1126   """
1127   return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1128     "instance_name": name,
1129     })
1130
1131
1132 class R_2_instances_name_rename(baserlib.ResourceBase):
1133   """/2/instances/[instance_name]/rename resource.
1134
1135   """
1136   def PUT(self):
1137     """Changes the name of an instance.
1138
1139     @return: a job id
1140
1141     """
1142     baserlib.CheckType(self.request_body, dict, "Body contents")
1143
1144     op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1145
1146     return self.SubmitJob([op])
1147
1148
1149 def _ParseModifyInstanceRequest(name, data):
1150   """Parses a request for modifying an instance.
1151
1152   @rtype: L{opcodes.OpInstanceSetParams}
1153   @return: Instance modify opcode
1154
1155   """
1156   return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1157     "instance_name": name,
1158     })
1159
1160
1161 class R_2_instances_name_modify(baserlib.ResourceBase):
1162   """/2/instances/[instance_name]/modify resource.
1163
1164   """
1165   def PUT(self):
1166     """Changes some parameters of an instance.
1167
1168     @return: a job id
1169
1170     """
1171     baserlib.CheckType(self.request_body, dict, "Body contents")
1172
1173     op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1174
1175     return self.SubmitJob([op])
1176
1177
1178 class R_2_instances_name_disk_grow(baserlib.ResourceBase):
1179   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1180
1181   """
1182   def POST(self):
1183     """Increases the size of an instance disk.
1184
1185     @return: a job id
1186
1187     """
1188     op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1189       "instance_name": self.items[0],
1190       "disk": int(self.items[1]),
1191       })
1192
1193     return self.SubmitJob([op])
1194
1195
1196 class R_2_instances_name_console(baserlib.ResourceBase):
1197   """/2/instances/[instance_name]/console resource.
1198
1199   """
1200   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1201
1202   def GET(self):
1203     """Request information for connecting to instance's console.
1204
1205     @return: Serialized instance console description, see
1206              L{objects.InstanceConsole}
1207
1208     """
1209     client = self.GetClient()
1210
1211     ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1212
1213     if console is None:
1214       raise http.HttpServiceUnavailable("Instance console unavailable")
1215
1216     assert isinstance(console, dict)
1217     return console
1218
1219
1220 def _GetQueryFields(args):
1221   """
1222
1223   """
1224   try:
1225     fields = args["fields"]
1226   except KeyError:
1227     raise http.HttpBadRequest("Missing 'fields' query argument")
1228
1229   return _SplitQueryFields(fields[0])
1230
1231
1232 def _SplitQueryFields(fields):
1233   """
1234
1235   """
1236   return [i.strip() for i in fields.split(",")]
1237
1238
1239 class R_2_query(baserlib.ResourceBase):
1240   """/2/query/[resource] resource.
1241
1242   """
1243   # Results might contain sensitive information
1244   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1245
1246   def _Query(self, fields, filter_):
1247     return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1248
1249   def GET(self):
1250     """Returns resource information.
1251
1252     @return: Query result, see L{objects.QueryResponse}
1253
1254     """
1255     return self._Query(_GetQueryFields(self.queryargs), None)
1256
1257   def PUT(self):
1258     """Submits job querying for resources.
1259
1260     @return: Query result, see L{objects.QueryResponse}
1261
1262     """
1263     body = self.request_body
1264
1265     baserlib.CheckType(body, dict, "Body contents")
1266
1267     try:
1268       fields = body["fields"]
1269     except KeyError:
1270       fields = _GetQueryFields(self.queryargs)
1271
1272     return self._Query(fields, self.request_body.get("filter", None))
1273
1274
1275 class R_2_query_fields(baserlib.ResourceBase):
1276   """/2/query/[resource]/fields resource.
1277
1278   """
1279   def GET(self):
1280     """Retrieves list of available fields for a resource.
1281
1282     @return: List of serialized L{objects.QueryFieldDefinition}
1283
1284     """
1285     try:
1286       raw_fields = self.queryargs["fields"]
1287     except KeyError:
1288       fields = None
1289     else:
1290       fields = _SplitQueryFields(raw_fields[0])
1291
1292     return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1293
1294
1295 class _R_Tags(baserlib.ResourceBase):
1296   """ Quasiclass for tagging resources
1297
1298   Manages tags. When inheriting this class you must define the
1299   TAG_LEVEL for it.
1300
1301   """
1302   TAG_LEVEL = None
1303
1304   def __init__(self, items, queryargs, req):
1305     """A tag resource constructor.
1306
1307     We have to override the default to sort out cluster naming case.
1308
1309     """
1310     baserlib.ResourceBase.__init__(self, items, queryargs, req)
1311
1312     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1313       self.name = None
1314     else:
1315       self.name = items[0]
1316
1317   def GET(self):
1318     """Returns a list of tags.
1319
1320     Example: ["tag1", "tag2", "tag3"]
1321
1322     """
1323     kind = self.TAG_LEVEL
1324
1325     if kind in (constants.TAG_INSTANCE,
1326                 constants.TAG_NODEGROUP,
1327                 constants.TAG_NODE):
1328       if not self.name:
1329         raise http.HttpBadRequest("Missing name on tag request")
1330
1331       cl = self.GetClient()
1332       if kind == constants.TAG_INSTANCE:
1333         fn = cl.QueryInstances
1334       elif kind == constants.TAG_NODEGROUP:
1335         fn = cl.QueryGroups
1336       else:
1337         fn = cl.QueryNodes
1338       result = fn(names=[self.name], fields=["tags"], use_locking=False)
1339       if not result or not result[0]:
1340         raise http.HttpBadGateway("Invalid response from tag query")
1341       tags = result[0][0]
1342
1343     elif kind == constants.TAG_CLUSTER:
1344       assert not self.name
1345       # TODO: Use query API?
1346       ssc = ssconf.SimpleStore()
1347       tags = ssc.GetClusterTags()
1348
1349     return list(tags)
1350
1351   def PUT(self):
1352     """Add a set of tags.
1353
1354     The request as a list of strings should be PUT to this URI. And
1355     you'll have back a job id.
1356
1357     """
1358     # pylint: disable-msg=W0212
1359     if "tag" not in self.queryargs:
1360       raise http.HttpBadRequest("Please specify tag(s) to add using the"
1361                                 " the 'tag' parameter")
1362     op = opcodes.OpTagsSet(kind=self.TAG_LEVEL, name=self.name,
1363                            tags=self.queryargs["tag"], dry_run=self.dryRun())
1364     return self.SubmitJob([op])
1365
1366   def DELETE(self):
1367     """Delete a tag.
1368
1369     In order to delete a set of tags, the DELETE
1370     request should be addressed to URI like:
1371     /tags?tag=[tag]&tag=[tag]
1372
1373     """
1374     # pylint: disable-msg=W0212
1375     if "tag" not in self.queryargs:
1376       # no we not gonna delete all tags
1377       raise http.HttpBadRequest("Cannot delete all tags - please specify"
1378                                 " tag(s) using the 'tag' parameter")
1379     op = opcodes.OpTagsDel(kind=self.TAG_LEVEL, name=self.name,
1380                            tags=self.queryargs["tag"], dry_run=self.dryRun())
1381     return self.SubmitJob([op])
1382
1383
1384 class R_2_instances_name_tags(_R_Tags):
1385   """ /2/instances/[instance_name]/tags resource.
1386
1387   Manages per-instance tags.
1388
1389   """
1390   TAG_LEVEL = constants.TAG_INSTANCE
1391
1392
1393 class R_2_nodes_name_tags(_R_Tags):
1394   """ /2/nodes/[node_name]/tags resource.
1395
1396   Manages per-node tags.
1397
1398   """
1399   TAG_LEVEL = constants.TAG_NODE
1400
1401
1402 class R_2_groups_name_tags(_R_Tags):
1403   """ /2/groups/[group_name]/tags resource.
1404
1405   Manages per-nodegroup tags.
1406
1407   """
1408   TAG_LEVEL = constants.TAG_NODEGROUP
1409
1410
1411 class R_2_tags(_R_Tags):
1412   """ /2/tags resource.
1413
1414   Manages cluster tags.
1415
1416   """
1417   TAG_LEVEL = constants.TAG_CLUSTER