4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
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.
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.
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
22 """Remote API resource implementations.
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.
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.
35 Quoting from RFC2616, section 9.6::
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
49 So when adding new methods, if they are operating on the URI entity itself,
50 PUT should be prefered over POST.
54 # pylint: disable=C0103
56 # C0103: Invalid name, since the R_* names are not conforming
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
64 from ganeti import compat
65 from ganeti import ssconf
66 from ganeti.rapi import baserlib
69 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
70 I_FIELDS = ["name", "admin_state", "os",
73 "nic.ips", "nic.macs", "nic.modes",
74 "nic.links", "nic.networks", "nic.bridges",
76 "disk.sizes", "disk_usage",
77 "beparams", "hvparams",
78 "oper_state", "oper_ram", "oper_vcpus", "status",
79 "custom_hvparams", "custom_beparams", "custom_nicparams",
82 N_FIELDS = ["name", "offline", "master_candidate", "drained",
84 "mtotal", "mnode", "mfree",
85 "pinst_cnt", "sinst_cnt",
86 "ctotal", "cnodes", "csockets",
88 "pinst_list", "sinst_list",
89 "master_capable", "vm_capable",
94 NET_FIELDS = ["name", "network", "gateway",
95 "network6", "gateway6",
96 "mac_prefix", "network_type",
97 "free_count", "reserved_count",
98 "map", "group_list", "inst_list",
99 "external_reservations",
116 "id", "ops", "status", "summary",
118 "received_ts", "start_ts", "end_ts",
121 J_FIELDS = J_FIELDS_BULK + [
126 _NR_DRAINED = "drained"
127 _NR_MASTER_CANDIDATE = "master-candidate"
128 _NR_MASTER = "master"
129 _NR_OFFLINE = "offline"
130 _NR_REGULAR = "regular"
133 constants.NR_MASTER: _NR_MASTER,
134 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
135 constants.NR_DRAINED: _NR_DRAINED,
136 constants.NR_OFFLINE: _NR_OFFLINE,
137 constants.NR_REGULAR: _NR_REGULAR,
140 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
142 # Request data version field
143 _REQ_DATA_VERSION = "__version__"
145 # Feature string for instance creation request data version 1
146 _INST_CREATE_REQV1 = "instance-create-reqv1"
148 # Feature string for instance reinstall request version 1
149 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
151 # Feature string for node migration version 1
152 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
154 # Feature string for node evacuation with LU-generated jobs
155 _NODE_EVAC_RES1 = "node-evac-res1"
157 ALL_FEATURES = frozenset([
159 _INST_REINSTALL_REQV1,
164 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
168 # FIXME: For compatibility we update the beparams/memory field. Needs to be
169 # removed in Ganeti 2.7
170 def _UpdateBeparams(inst):
171 """Updates the beparams dict of inst to support the memory field.
173 @param inst: Inst dict
174 @return: Updated inst dict
177 beparams = inst["beparams"]
178 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
183 class R_root(baserlib.ResourceBase):
189 """Supported for legacy reasons.
201 class R_version(baserlib.ResourceBase):
202 """/version resource.
204 This resource should be used to determine the remote API version and
205 to adapt clients accordingly.
210 """Returns the remote API version.
213 return constants.RAPI_VERSION
216 class R_2_info(baserlib.OpcodeResource):
220 GET_OPCODE = opcodes.OpClusterQuery
223 """Returns cluster information.
226 client = self.GetClient()
227 return client.QueryClusterInfo()
230 class R_2_features(baserlib.ResourceBase):
231 """/2/features resource.
236 """Returns list of optional RAPI features implemented.
239 return list(ALL_FEATURES)
242 class R_2_os(baserlib.OpcodeResource):
246 GET_OPCODE = opcodes.OpOsDiagnose
249 """Return a list of all OSes.
251 Can return error 500 in case of a problem.
253 Example: ["debian-etch"]
256 cl = self.GetClient()
257 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
258 job_id = self.SubmitJob([op], cl=cl)
259 # we use custom feedback function, instead of print we log the status
260 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
261 diagnose_data = result[0]
263 if not isinstance(diagnose_data, list):
264 raise http.HttpBadGateway(message="Can't get OS list")
267 for (name, variants) in diagnose_data:
268 os_names.extend(cli.CalculateOSNames(name, variants))
273 class R_2_redist_config(baserlib.OpcodeResource):
274 """/2/redistribute-config resource.
277 PUT_OPCODE = opcodes.OpClusterRedistConf
280 class R_2_cluster_modify(baserlib.OpcodeResource):
281 """/2/modify resource.
284 PUT_OPCODE = opcodes.OpClusterSetParams
287 class R_2_jobs(baserlib.ResourceBase):
292 """Returns a dictionary of jobs.
294 @return: a dictionary with jobs id and uri.
297 client = self.GetClient()
300 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
301 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
303 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
304 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
305 uri_fields=("id", "uri"))
308 class R_2_jobs_id(baserlib.ResourceBase):
309 """/2/jobs/[job_id] resource.
313 """Returns a job status.
315 @return: a dictionary with job parameters.
317 - id: job ID as a number
318 - status: current job status as a string
319 - ops: involved OpCodes as a list of dictionaries for each
321 - opstatus: OpCodes status as a list
322 - opresult: OpCodes results as a list of lists
325 job_id = self.items[0]
326 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
328 raise http.HttpNotFound()
329 return baserlib.MapFields(J_FIELDS, result)
332 """Cancel not-yet-started job.
335 job_id = self.items[0]
336 result = self.GetClient().CancelJob(job_id)
340 class R_2_jobs_id_wait(baserlib.ResourceBase):
341 """/2/jobs/[job_id]/wait resource.
344 # WaitForJobChange provides access to sensitive information and blocks
345 # machine resources (it's a blocking RAPI call), hence restricting access.
346 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
349 """Waits for job changes.
352 job_id = self.items[0]
354 fields = self.getBodyParameter("fields")
355 prev_job_info = self.getBodyParameter("previous_job_info", None)
356 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
358 if not isinstance(fields, list):
359 raise http.HttpBadRequest("The 'fields' parameter should be a list")
361 if not (prev_job_info is None or isinstance(prev_job_info, list)):
362 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
365 if not (prev_log_serial is None or
366 isinstance(prev_log_serial, (int, long))):
367 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
370 client = self.GetClient()
371 result = client.WaitForJobChangeOnce(job_id, fields,
372 prev_job_info, prev_log_serial,
373 timeout=_WFJC_TIMEOUT)
375 raise http.HttpNotFound()
377 if result == constants.JOB_NOTCHANGED:
381 (job_info, log_entries) = result
384 "job_info": job_info,
385 "log_entries": log_entries,
389 class R_2_nodes(baserlib.OpcodeResource):
390 """/2/nodes resource.
393 GET_OPCODE = opcodes.OpNodeQuery
396 """Returns a list of all nodes.
399 client = self.GetClient()
402 bulkdata = client.QueryNodes([], N_FIELDS, False)
403 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
405 nodesdata = client.QueryNodes([], ["name"], False)
406 nodeslist = [row[0] for row in nodesdata]
407 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
408 uri_fields=("id", "uri"))
411 class R_2_nodes_name(baserlib.OpcodeResource):
412 """/2/nodes/[node_name] resource.
415 GET_OPCODE = opcodes.OpNodeQuery
418 """Send information about a node.
421 node_name = self.items[0]
422 client = self.GetClient()
424 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
425 names=[node_name], fields=N_FIELDS,
426 use_locking=self.useLocking())
428 return baserlib.MapFields(N_FIELDS, result[0])
431 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
432 """/2/nodes/[node_name]/powercycle resource.
435 POST_OPCODE = opcodes.OpNodePowercycle
437 def GetPostOpInput(self):
438 """Tries to powercycle a node.
441 return (self.request_body, {
442 "node_name": self.items[0],
443 "force": self.useForce(),
447 class R_2_nodes_name_role(baserlib.OpcodeResource):
448 """/2/nodes/[node_name]/role resource.
451 PUT_OPCODE = opcodes.OpNodeSetParams
454 """Returns the current node role.
459 node_name = self.items[0]
460 client = self.GetClient()
461 result = client.QueryNodes(names=[node_name], fields=["role"],
462 use_locking=self.useLocking())
464 return _NR_MAP[result[0][0]]
466 def GetPutOpInput(self):
467 """Sets the node role.
470 baserlib.CheckType(self.request_body, basestring, "Body contents")
472 role = self.request_body
474 if role == _NR_REGULAR:
479 elif role == _NR_MASTER_CANDIDATE:
481 offline = drained = None
483 elif role == _NR_DRAINED:
485 candidate = offline = None
487 elif role == _NR_OFFLINE:
489 candidate = drained = None
492 raise http.HttpBadRequest("Can't set '%s' role" % role)
494 assert len(self.items) == 1
497 "node_name": self.items[0],
498 "master_candidate": candidate,
501 "force": self.useForce(),
502 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
506 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
507 """/2/nodes/[node_name]/evacuate resource.
510 POST_OPCODE = opcodes.OpNodeEvacuate
512 def GetPostOpInput(self):
513 """Evacuate all instances off a node.
516 return (self.request_body, {
517 "node_name": self.items[0],
518 "dry_run": self.dryRun(),
522 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
523 """/2/nodes/[node_name]/migrate resource.
526 POST_OPCODE = opcodes.OpNodeMigrate
528 def GetPostOpInput(self):
529 """Migrate all primary instances from a node.
533 # Support old-style requests
534 if "live" in self.queryargs and "mode" in self.queryargs:
535 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
538 if "live" in self.queryargs:
539 if self._checkIntVariable("live", default=1):
540 mode = constants.HT_MIGRATION_LIVE
542 mode = constants.HT_MIGRATION_NONLIVE
544 mode = self._checkStringVariable("mode", default=None)
550 data = self.request_body
553 "node_name": self.items[0],
557 class R_2_nodes_name_modify(baserlib.OpcodeResource):
558 """/2/nodes/[node_name]/modify resource.
561 POST_OPCODE = opcodes.OpNodeSetParams
563 def GetPostOpInput(self):
564 """Changes parameters of a node.
567 assert len(self.items) == 1
569 return (self.request_body, {
570 "node_name": self.items[0],
574 class R_2_nodes_name_storage(baserlib.OpcodeResource):
575 """/2/nodes/[node_name]/storage resource.
578 # LUNodeQueryStorage acquires locks, hence restricting access to GET
579 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
580 GET_OPCODE = opcodes.OpNodeQueryStorage
582 def GetGetOpInput(self):
583 """List storage available on a node.
586 storage_type = self._checkStringVariable("storage_type", None)
587 output_fields = self._checkStringVariable("output_fields", None)
589 if not output_fields:
590 raise http.HttpBadRequest("Missing the required 'output_fields'"
594 "nodes": [self.items[0]],
595 "storage_type": storage_type,
596 "output_fields": output_fields.split(","),
600 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
601 """/2/nodes/[node_name]/storage/modify resource.
604 PUT_OPCODE = opcodes.OpNodeModifyStorage
606 def GetPutOpInput(self):
607 """Modifies a storage volume on a node.
610 storage_type = self._checkStringVariable("storage_type", None)
611 name = self._checkStringVariable("name", None)
614 raise http.HttpBadRequest("Missing the required 'name'"
619 if "allocatable" in self.queryargs:
620 changes[constants.SF_ALLOCATABLE] = \
621 bool(self._checkIntVariable("allocatable", default=1))
624 "node_name": self.items[0],
625 "storage_type": storage_type,
631 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
632 """/2/nodes/[node_name]/storage/repair resource.
635 PUT_OPCODE = opcodes.OpRepairNodeStorage
637 def GetPutOpInput(self):
638 """Repairs a storage volume on a node.
641 storage_type = self._checkStringVariable("storage_type", None)
642 name = self._checkStringVariable("name", None)
644 raise http.HttpBadRequest("Missing the required 'name'"
648 "node_name": self.items[0],
649 "storage_type": storage_type,
654 class R_2_networks(baserlib.OpcodeResource):
655 """/2/networks resource.
658 GET_OPCODE = opcodes.OpNetworkQuery
659 POST_OPCODE = opcodes.OpNetworkAdd
661 "name": "network_name",
664 def GetPostOpInput(self):
668 assert not self.items
669 return (self.request_body, {
670 "dry_run": self.dryRun(),
674 """Returns a list of all networks.
677 client = self.GetClient()
680 bulkdata = client.QueryNetworks([], NET_FIELDS, False)
681 return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
683 data = client.QueryNetworks([], ["name"], False)
684 networknames = [row[0] for row in data]
685 return baserlib.BuildUriList(networknames, "/2/networks/%s",
686 uri_fields=("name", "uri"))
689 class R_2_networks_name(baserlib.OpcodeResource):
690 """/2/network/[network_name] resource.
693 DELETE_OPCODE = opcodes.OpNetworkRemove
696 """Send information about a network.
699 network_name = self.items[0]
700 client = self.GetClient()
702 result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
703 names=[network_name],
705 use_locking=self.useLocking())
707 return baserlib.MapFields(NET_FIELDS, result[0])
709 def GetDeleteOpInput(self):
713 assert len(self.items) == 1
714 return (self.request_body, {
715 "network_name": self.items[0],
716 "dry_run": self.dryRun(),
719 class R_2_networks_name_connect(baserlib.OpcodeResource):
720 """/2/network/[network_name]/connect.
723 PUT_OPCODE = opcodes.OpNetworkConnect
725 def GetPutOpInput(self):
726 """Changes some parameters of node group.
730 return (self.request_body, {
731 "network_name": self.items[0],
734 class R_2_networks_name_disconnect(baserlib.OpcodeResource):
735 """/2/network/[network_name]/disconnect.
738 PUT_OPCODE = opcodes.OpNetworkDisconnect
740 def GetPutOpInput(self):
741 """Changes some parameters of node group.
745 return (self.request_body, {
746 "network_name": self.items[0],
749 class R_2_groups(baserlib.OpcodeResource):
750 """/2/groups resource.
753 GET_OPCODE = opcodes.OpGroupQuery
754 POST_OPCODE = opcodes.OpGroupAdd
756 "name": "group_name",
759 def GetPostOpInput(self):
760 """Create a node group.
764 assert not self.items
765 return (self.request_body, {
766 "dry_run": self.dryRun(),
770 """Returns a list of all node groups.
773 client = self.GetClient()
776 bulkdata = client.QueryGroups([], G_FIELDS, False)
777 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
779 data = client.QueryGroups([], ["name"], False)
780 groupnames = [row[0] for row in data]
781 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
782 uri_fields=("name", "uri"))
785 class R_2_groups_name(baserlib.OpcodeResource):
786 """/2/groups/[group_name] resource.
789 DELETE_OPCODE = opcodes.OpGroupRemove
792 """Send information about a node group.
795 group_name = self.items[0]
796 client = self.GetClient()
798 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
799 names=[group_name], fields=G_FIELDS,
800 use_locking=self.useLocking())
802 return baserlib.MapFields(G_FIELDS, result[0])
804 def GetDeleteOpInput(self):
805 """Delete a node group.
808 assert len(self.items) == 1
810 "group_name": self.items[0],
811 "dry_run": self.dryRun(),
815 class R_2_groups_name_modify(baserlib.OpcodeResource):
816 """/2/groups/[group_name]/modify resource.
819 PUT_OPCODE = opcodes.OpGroupSetParams
821 def GetPutOpInput(self):
822 """Changes some parameters of node group.
826 return (self.request_body, {
827 "group_name": self.items[0],
831 class R_2_groups_name_rename(baserlib.OpcodeResource):
832 """/2/groups/[group_name]/rename resource.
835 PUT_OPCODE = opcodes.OpGroupRename
837 def GetPutOpInput(self):
838 """Changes the name of a node group.
841 assert len(self.items) == 1
842 return (self.request_body, {
843 "group_name": self.items[0],
844 "dry_run": self.dryRun(),
848 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
849 """/2/groups/[group_name]/assign-nodes resource.
852 PUT_OPCODE = opcodes.OpGroupAssignNodes
854 def GetPutOpInput(self):
855 """Assigns nodes to a group.
858 assert len(self.items) == 1
859 return (self.request_body, {
860 "group_name": self.items[0],
861 "dry_run": self.dryRun(),
862 "force": self.useForce(),
866 class R_2_instances(baserlib.OpcodeResource):
867 """/2/instances resource.
870 GET_OPCODE = opcodes.OpInstanceQuery
871 POST_OPCODE = opcodes.OpInstanceCreate
874 "name": "instance_name",
878 """Returns a list of all available instances.
881 client = self.GetClient()
883 use_locking = self.useLocking()
885 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
886 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
888 instancesdata = client.QueryInstances([], ["name"], use_locking)
889 instanceslist = [row[0] for row in instancesdata]
890 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
891 uri_fields=("id", "uri"))
893 def GetPostOpInput(self):
894 """Create an instance.
899 baserlib.CheckType(self.request_body, dict, "Body contents")
901 # Default to request data version 0
902 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
904 if data_version == 0:
905 raise http.HttpBadRequest("Instance creation request version 0 is no"
907 elif data_version != 1:
908 raise http.HttpBadRequest("Unsupported request data version %s" %
911 data = self.request_body.copy()
912 # Remove "__version__"
913 data.pop(_REQ_DATA_VERSION, None)
916 "dry_run": self.dryRun(),
920 class R_2_instances_name(baserlib.OpcodeResource):
921 """/2/instances/[instance_name] resource.
924 GET_OPCODE = opcodes.OpInstanceQuery
925 DELETE_OPCODE = opcodes.OpInstanceRemove
928 """Send information about an instance.
931 client = self.GetClient()
932 instance_name = self.items[0]
934 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
935 names=[instance_name],
937 use_locking=self.useLocking())
939 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
941 def GetDeleteOpInput(self):
942 """Delete an instance.
945 assert len(self.items) == 1
947 "instance_name": self.items[0],
948 "ignore_failures": False,
949 "dry_run": self.dryRun(),
953 class R_2_instances_name_info(baserlib.OpcodeResource):
954 """/2/instances/[instance_name]/info resource.
957 GET_OPCODE = opcodes.OpInstanceQueryData
959 def GetGetOpInput(self):
960 """Request detailed instance information.
963 assert len(self.items) == 1
965 "instances": [self.items[0]],
966 "static": bool(self._checkIntVariable("static", default=0)),
970 class R_2_instances_name_reboot(baserlib.OpcodeResource):
971 """/2/instances/[instance_name]/reboot resource.
973 Implements an instance reboot.
976 POST_OPCODE = opcodes.OpInstanceReboot
978 def GetPostOpInput(self):
979 """Reboot an instance.
981 The URI takes type=[hard|soft|full] and
982 ignore_secondaries=[False|True] parameters.
986 "instance_name": self.items[0],
988 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
989 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
990 "dry_run": self.dryRun(),
994 class R_2_instances_name_startup(baserlib.OpcodeResource):
995 """/2/instances/[instance_name]/startup resource.
997 Implements an instance startup.
1000 PUT_OPCODE = opcodes.OpInstanceStartup
1002 def GetPutOpInput(self):
1003 """Startup an instance.
1005 The URI takes force=[False|True] parameter to start the instance
1006 if even if secondary disks are failing.
1010 "instance_name": self.items[0],
1011 "force": self.useForce(),
1012 "dry_run": self.dryRun(),
1013 "no_remember": bool(self._checkIntVariable("no_remember")),
1017 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1018 """/2/instances/[instance_name]/shutdown resource.
1020 Implements an instance shutdown.
1023 PUT_OPCODE = opcodes.OpInstanceShutdown
1025 def GetPutOpInput(self):
1026 """Shutdown an instance.
1029 return (self.request_body, {
1030 "instance_name": self.items[0],
1031 "no_remember": bool(self._checkIntVariable("no_remember")),
1032 "dry_run": self.dryRun(),
1036 def _ParseInstanceReinstallRequest(name, data):
1037 """Parses a request for reinstalling an instance.
1040 if not isinstance(data, dict):
1041 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1043 ostype = baserlib.CheckParameter(data, "os", default=None)
1044 start = baserlib.CheckParameter(data, "start", exptype=bool,
1046 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1049 opcodes.OpInstanceShutdown(instance_name=name),
1050 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1055 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1060 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1061 """/2/instances/[instance_name]/reinstall resource.
1063 Implements an instance reinstall.
1066 POST_OPCODE = opcodes.OpInstanceReinstall
1069 """Reinstall an instance.
1071 The URI takes os=name and nostartup=[0|1] optional
1072 parameters. By default, the instance will be started
1076 if self.request_body:
1078 raise http.HttpBadRequest("Can't combine query and body parameters")
1080 body = self.request_body
1081 elif self.queryargs:
1082 # Legacy interface, do not modify/extend
1084 "os": self._checkStringVariable("os"),
1085 "start": not self._checkIntVariable("nostartup"),
1090 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1092 return self.SubmitJob(ops)
1095 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1096 """/2/instances/[instance_name]/replace-disks resource.
1099 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1101 def GetPostOpInput(self):
1102 """Replaces disks on an instance.
1106 "instance_name": self.items[0],
1109 if self.request_body:
1110 data = self.request_body
1111 elif self.queryargs:
1112 # Legacy interface, do not modify/extend
1114 "remote_node": self._checkStringVariable("remote_node", default=None),
1115 "mode": self._checkStringVariable("mode", default=None),
1116 "disks": self._checkStringVariable("disks", default=None),
1117 "iallocator": self._checkStringVariable("iallocator", default=None),
1124 raw_disks = data.pop("disks")
1129 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1130 data["disks"] = raw_disks
1132 # Backwards compatibility for strings of the format "1, 2, 3"
1134 data["disks"] = [int(part) for part in raw_disks.split(",")]
1135 except (TypeError, ValueError), err:
1136 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1138 return (data, static)
1141 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1142 """/2/instances/[instance_name]/activate-disks resource.
1145 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1147 def GetPutOpInput(self):
1148 """Activate disks for an instance.
1150 The URI might contain ignore_size to ignore current recorded size.
1154 "instance_name": self.items[0],
1155 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1159 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1160 """/2/instances/[instance_name]/deactivate-disks resource.
1163 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1165 def GetPutOpInput(self):
1166 """Deactivate disks for an instance.
1170 "instance_name": self.items[0],
1174 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1175 """/2/instances/[instance_name]/recreate-disks resource.
1178 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1180 def GetPostOpInput(self):
1181 """Recreate disks for an instance.
1185 "instance_name": self.items[0],
1189 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1190 """/2/instances/[instance_name]/prepare-export resource.
1193 PUT_OPCODE = opcodes.OpBackupPrepare
1195 def GetPutOpInput(self):
1196 """Prepares an export for an instance.
1200 "instance_name": self.items[0],
1201 "mode": self._checkStringVariable("mode"),
1205 class R_2_instances_name_export(baserlib.OpcodeResource):
1206 """/2/instances/[instance_name]/export resource.
1209 PUT_OPCODE = opcodes.OpBackupExport
1211 "destination": "target_node",
1214 def GetPutOpInput(self):
1215 """Exports an instance.
1218 return (self.request_body, {
1219 "instance_name": self.items[0],
1223 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1224 """/2/instances/[instance_name]/migrate resource.
1227 PUT_OPCODE = opcodes.OpInstanceMigrate
1229 def GetPutOpInput(self):
1230 """Migrates an instance.
1233 return (self.request_body, {
1234 "instance_name": self.items[0],
1238 class R_2_instances_name_failover(baserlib.OpcodeResource):
1239 """/2/instances/[instance_name]/failover resource.
1242 PUT_OPCODE = opcodes.OpInstanceFailover
1244 def GetPutOpInput(self):
1245 """Does a failover of an instance.
1248 return (self.request_body, {
1249 "instance_name": self.items[0],
1253 class R_2_instances_name_rename(baserlib.OpcodeResource):
1254 """/2/instances/[instance_name]/rename resource.
1257 PUT_OPCODE = opcodes.OpInstanceRename
1259 def GetPutOpInput(self):
1260 """Changes the name of an instance.
1263 return (self.request_body, {
1264 "instance_name": self.items[0],
1268 class R_2_instances_name_modify(baserlib.OpcodeResource):
1269 """/2/instances/[instance_name]/modify resource.
1272 PUT_OPCODE = opcodes.OpInstanceSetParams
1274 def GetPutOpInput(self):
1275 """Changes parameters of an instance.
1278 return (self.request_body, {
1279 "instance_name": self.items[0],
1283 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1284 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1287 POST_OPCODE = opcodes.OpInstanceGrowDisk
1289 def GetPostOpInput(self):
1290 """Increases the size of an instance disk.
1293 return (self.request_body, {
1294 "instance_name": self.items[0],
1295 "disk": int(self.items[1]),
1299 class R_2_instances_name_console(baserlib.ResourceBase):
1300 """/2/instances/[instance_name]/console resource.
1303 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1304 GET_OPCODE = opcodes.OpInstanceConsole
1307 """Request information for connecting to instance's console.
1309 @return: Serialized instance console description, see
1310 L{objects.InstanceConsole}
1313 client = self.GetClient()
1315 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1318 raise http.HttpServiceUnavailable("Instance console unavailable")
1320 assert isinstance(console, dict)
1324 def _GetQueryFields(args):
1329 fields = args["fields"]
1331 raise http.HttpBadRequest("Missing 'fields' query argument")
1333 return _SplitQueryFields(fields[0])
1336 def _SplitQueryFields(fields):
1340 return [i.strip() for i in fields.split(",")]
1343 class R_2_query(baserlib.ResourceBase):
1344 """/2/query/[resource] resource.
1347 # Results might contain sensitive information
1348 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1349 GET_OPCODE = opcodes.OpQuery
1350 PUT_OPCODE = opcodes.OpQuery
1352 def _Query(self, fields, qfilter):
1353 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1356 """Returns resource information.
1358 @return: Query result, see L{objects.QueryResponse}
1361 return self._Query(_GetQueryFields(self.queryargs), None)
1364 """Submits job querying for resources.
1366 @return: Query result, see L{objects.QueryResponse}
1369 body = self.request_body
1371 baserlib.CheckType(body, dict, "Body contents")
1374 fields = body["fields"]
1376 fields = _GetQueryFields(self.queryargs)
1378 qfilter = body.get("qfilter", None)
1379 # TODO: remove this after 2.7
1381 qfilter = body.get("filter", None)
1383 return self._Query(fields, qfilter)
1386 class R_2_query_fields(baserlib.ResourceBase):
1387 """/2/query/[resource]/fields resource.
1390 GET_OPCODE = opcodes.OpQueryFields
1393 """Retrieves list of available fields for a resource.
1395 @return: List of serialized L{objects.QueryFieldDefinition}
1399 raw_fields = self.queryargs["fields"]
1403 fields = _SplitQueryFields(raw_fields[0])
1405 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1408 class _R_Tags(baserlib.OpcodeResource):
1409 """Quasiclass for tagging resources.
1411 Manages tags. When inheriting this class you must define the
1416 GET_OPCODE = opcodes.OpTagsGet
1417 PUT_OPCODE = opcodes.OpTagsSet
1418 DELETE_OPCODE = opcodes.OpTagsDel
1420 def __init__(self, items, queryargs, req, **kwargs):
1421 """A tag resource constructor.
1423 We have to override the default to sort out cluster naming case.
1426 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1428 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1431 self.name = items[0]
1434 """Returns a list of tags.
1436 Example: ["tag1", "tag2", "tag3"]
1439 kind = self.TAG_LEVEL
1441 if kind in (constants.TAG_INSTANCE,
1442 constants.TAG_NODEGROUP,
1443 constants.TAG_NODE):
1445 raise http.HttpBadRequest("Missing name on tag request")
1447 cl = self.GetClient()
1448 if kind == constants.TAG_INSTANCE:
1449 fn = cl.QueryInstances
1450 elif kind == constants.TAG_NODEGROUP:
1454 result = fn(names=[self.name], fields=["tags"], use_locking=False)
1455 if not result or not result[0]:
1456 raise http.HttpBadGateway("Invalid response from tag query")
1459 elif kind == constants.TAG_CLUSTER:
1460 assert not self.name
1461 # TODO: Use query API?
1462 ssc = ssconf.SimpleStore()
1463 tags = ssc.GetClusterTags()
1467 def GetPutOpInput(self):
1468 """Add a set of tags.
1470 The request as a list of strings should be PUT to this URI. And
1471 you'll have back a job id.
1475 "kind": self.TAG_LEVEL,
1477 "tags": self.queryargs.get("tag", []),
1478 "dry_run": self.dryRun(),
1481 def GetDeleteOpInput(self):
1484 In order to delete a set of tags, the DELETE
1485 request should be addressed to URI like:
1486 /tags?tag=[tag]&tag=[tag]
1490 return self.GetPutOpInput()
1493 class R_2_instances_name_tags(_R_Tags):
1494 """ /2/instances/[instance_name]/tags resource.
1496 Manages per-instance tags.
1499 TAG_LEVEL = constants.TAG_INSTANCE
1502 class R_2_nodes_name_tags(_R_Tags):
1503 """ /2/nodes/[node_name]/tags resource.
1505 Manages per-node tags.
1508 TAG_LEVEL = constants.TAG_NODE
1511 class R_2_groups_name_tags(_R_Tags):
1512 """ /2/groups/[group_name]/tags resource.
1514 Manages per-nodegroup tags.
1517 TAG_LEVEL = constants.TAG_NODEGROUP
1520 class R_2_tags(_R_Tags):
1521 """ /2/tags resource.
1523 Manages cluster tags.
1526 TAG_LEVEL = constants.TAG_CLUSTER