4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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 objects
60 from ganeti import http
61 from ganeti import constants
62 from ganeti import cli
63 from ganeti import rapi
65 from ganeti import compat
66 from ganeti import ssconf
67 from ganeti.rapi import baserlib
70 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
71 I_FIELDS = ["name", "admin_state", "os",
74 "nic.ips", "nic.macs", "nic.modes", "nic.uuids", "nic.names",
75 "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
77 "disk.sizes", "disk_usage", "disk.uuids", "disk.names",
78 "beparams", "hvparams",
79 "oper_state", "oper_ram", "oper_vcpus", "status",
80 "custom_hvparams", "custom_beparams", "custom_nicparams",
83 N_FIELDS = ["name", "offline", "master_candidate", "drained",
85 "mtotal", "mnode", "mfree",
86 "pinst_cnt", "sinst_cnt",
87 "ctotal", "cnodes", "csockets",
89 "pinst_list", "sinst_list",
90 "master_capable", "vm_capable",
95 NET_FIELDS = ["name", "network", "gateway",
96 "network6", "gateway6",
98 "free_count", "reserved_count",
99 "map", "group_list", "inst_list",
100 "external_reservations",
117 "id", "ops", "status", "summary",
119 "received_ts", "start_ts", "end_ts",
122 J_FIELDS = J_FIELDS_BULK + [
127 _NR_DRAINED = "drained"
128 _NR_MASTER_CANDIDATE = "master-candidate"
129 _NR_MASTER = "master"
130 _NR_OFFLINE = "offline"
131 _NR_REGULAR = "regular"
134 constants.NR_MASTER: _NR_MASTER,
135 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
136 constants.NR_DRAINED: _NR_DRAINED,
137 constants.NR_OFFLINE: _NR_OFFLINE,
138 constants.NR_REGULAR: _NR_REGULAR,
141 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
143 # Request data version field
144 _REQ_DATA_VERSION = "__version__"
146 # Feature string for instance creation request data version 1
147 _INST_CREATE_REQV1 = "instance-create-reqv1"
149 # Feature string for instance reinstall request version 1
150 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
152 # Feature string for node migration version 1
153 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
155 # Feature string for node evacuation with LU-generated jobs
156 _NODE_EVAC_RES1 = "node-evac-res1"
158 ALL_FEATURES = compat.UniqueFrozenset([
160 _INST_REINSTALL_REQV1,
165 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
169 # FIXME: For compatibility we update the beparams/memory field. Needs to be
170 # removed in Ganeti 2.8
171 def _UpdateBeparams(inst):
172 """Updates the beparams dict of inst to support the memory field.
174 @param inst: Inst dict
175 @return: Updated inst dict
178 beparams = inst["beparams"]
179 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
184 class R_root(baserlib.ResourceBase):
190 """Supported for legacy reasons.
202 class R_version(baserlib.ResourceBase):
203 """/version resource.
205 This resource should be used to determine the remote API version and
206 to adapt clients accordingly.
211 """Returns the remote API version.
214 return constants.RAPI_VERSION
217 class R_2_info(baserlib.OpcodeResource):
221 GET_OPCODE = opcodes.OpClusterQuery
224 """Returns cluster information.
227 client = self.GetClient(query=True)
228 return client.QueryClusterInfo()
231 class R_2_features(baserlib.ResourceBase):
232 """/2/features resource.
237 """Returns list of optional RAPI features implemented.
240 return list(ALL_FEATURES)
243 class R_2_os(baserlib.OpcodeResource):
247 GET_OPCODE = opcodes.OpOsDiagnose
250 """Return a list of all OSes.
252 Can return error 500 in case of a problem.
254 Example: ["debian-etch"]
257 cl = self.GetClient()
258 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
259 job_id = self.SubmitJob([op], cl=cl)
260 # we use custom feedback function, instead of print we log the status
261 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
262 diagnose_data = result[0]
264 if not isinstance(diagnose_data, list):
265 raise http.HttpBadGateway(message="Can't get OS list")
268 for (name, variants) in diagnose_data:
269 os_names.extend(cli.CalculateOSNames(name, variants))
274 class R_2_redist_config(baserlib.OpcodeResource):
275 """/2/redistribute-config resource.
278 PUT_OPCODE = opcodes.OpClusterRedistConf
281 class R_2_cluster_modify(baserlib.OpcodeResource):
282 """/2/modify resource.
285 PUT_OPCODE = opcodes.OpClusterSetParams
288 class R_2_jobs(baserlib.ResourceBase):
293 """Returns a dictionary of jobs.
295 @return: a dictionary with jobs id and uri.
298 client = self.GetClient(query=True)
301 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
302 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
304 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
305 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
306 uri_fields=("id", "uri"))
309 class R_2_jobs_id(baserlib.ResourceBase):
310 """/2/jobs/[job_id] resource.
314 """Returns a job status.
316 @return: a dictionary with job parameters.
318 - id: job ID as a number
319 - status: current job status as a string
320 - ops: involved OpCodes as a list of dictionaries for each
322 - opstatus: OpCodes status as a list
323 - opresult: OpCodes results as a list of lists
326 job_id = self.items[0]
327 result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
329 raise http.HttpNotFound()
330 return baserlib.MapFields(J_FIELDS, result)
333 """Cancel not-yet-started job.
336 job_id = self.items[0]
337 result = self.GetClient().CancelJob(job_id)
341 class R_2_jobs_id_wait(baserlib.ResourceBase):
342 """/2/jobs/[job_id]/wait resource.
345 # WaitForJobChange provides access to sensitive information and blocks
346 # machine resources (it's a blocking RAPI call), hence restricting access.
347 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
350 """Waits for job changes.
353 job_id = self.items[0]
355 fields = self.getBodyParameter("fields")
356 prev_job_info = self.getBodyParameter("previous_job_info", None)
357 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
359 if not isinstance(fields, list):
360 raise http.HttpBadRequest("The 'fields' parameter should be a list")
362 if not (prev_job_info is None or isinstance(prev_job_info, list)):
363 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
366 if not (prev_log_serial is None or
367 isinstance(prev_log_serial, (int, long))):
368 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
371 client = self.GetClient()
372 result = client.WaitForJobChangeOnce(job_id, fields,
373 prev_job_info, prev_log_serial,
374 timeout=_WFJC_TIMEOUT)
376 raise http.HttpNotFound()
378 if result == constants.JOB_NOTCHANGED:
382 (job_info, log_entries) = result
385 "job_info": job_info,
386 "log_entries": log_entries,
390 class R_2_nodes(baserlib.OpcodeResource):
391 """/2/nodes resource.
394 GET_OPCODE = opcodes.OpNodeQuery
397 """Returns a list of all nodes.
400 client = self.GetClient(query=True)
403 bulkdata = client.QueryNodes([], N_FIELDS, False)
404 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
406 nodesdata = client.QueryNodes([], ["name"], False)
407 nodeslist = [row[0] for row in nodesdata]
408 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
409 uri_fields=("id", "uri"))
412 class R_2_nodes_name(baserlib.OpcodeResource):
413 """/2/nodes/[node_name] resource.
416 GET_OPCODE = opcodes.OpNodeQuery
419 """Send information about a node.
422 node_name = self.items[0]
423 client = self.GetClient(query=True)
425 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
426 names=[node_name], fields=N_FIELDS,
427 use_locking=self.useLocking())
429 return baserlib.MapFields(N_FIELDS, result[0])
432 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
433 """/2/nodes/[node_name]/powercycle resource.
436 POST_OPCODE = opcodes.OpNodePowercycle
438 def GetPostOpInput(self):
439 """Tries to powercycle a node.
442 return (self.request_body, {
443 "node_name": self.items[0],
444 "force": self.useForce(),
448 class R_2_nodes_name_role(baserlib.OpcodeResource):
449 """/2/nodes/[node_name]/role resource.
452 PUT_OPCODE = opcodes.OpNodeSetParams
455 """Returns the current node role.
460 node_name = self.items[0]
461 client = self.GetClient(query=True)
462 result = client.QueryNodes(names=[node_name], fields=["role"],
463 use_locking=self.useLocking())
465 return _NR_MAP[result[0][0]]
467 def GetPutOpInput(self):
468 """Sets the node role.
471 baserlib.CheckType(self.request_body, basestring, "Body contents")
473 role = self.request_body
475 if role == _NR_REGULAR:
480 elif role == _NR_MASTER_CANDIDATE:
482 offline = drained = None
484 elif role == _NR_DRAINED:
486 candidate = offline = None
488 elif role == _NR_OFFLINE:
490 candidate = drained = None
493 raise http.HttpBadRequest("Can't set '%s' role" % role)
495 assert len(self.items) == 1
498 "node_name": self.items[0],
499 "master_candidate": candidate,
502 "force": self.useForce(),
503 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
507 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
508 """/2/nodes/[node_name]/evacuate resource.
511 POST_OPCODE = opcodes.OpNodeEvacuate
513 def GetPostOpInput(self):
514 """Evacuate all instances off a node.
517 return (self.request_body, {
518 "node_name": self.items[0],
519 "dry_run": self.dryRun(),
523 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
524 """/2/nodes/[node_name]/migrate resource.
527 POST_OPCODE = opcodes.OpNodeMigrate
529 def GetPostOpInput(self):
530 """Migrate all primary instances from a node.
534 # Support old-style requests
535 if "live" in self.queryargs and "mode" in self.queryargs:
536 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
539 if "live" in self.queryargs:
540 if self._checkIntVariable("live", default=1):
541 mode = constants.HT_MIGRATION_LIVE
543 mode = constants.HT_MIGRATION_NONLIVE
545 mode = self._checkStringVariable("mode", default=None)
551 data = self.request_body
554 "node_name": self.items[0],
558 class R_2_nodes_name_modify(baserlib.OpcodeResource):
559 """/2/nodes/[node_name]/modify resource.
562 POST_OPCODE = opcodes.OpNodeSetParams
564 def GetPostOpInput(self):
565 """Changes parameters of a node.
568 assert len(self.items) == 1
570 return (self.request_body, {
571 "node_name": self.items[0],
575 class R_2_nodes_name_storage(baserlib.OpcodeResource):
576 """/2/nodes/[node_name]/storage resource.
579 # LUNodeQueryStorage acquires locks, hence restricting access to GET
580 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
581 GET_OPCODE = opcodes.OpNodeQueryStorage
583 def GetGetOpInput(self):
584 """List storage available on a node.
587 storage_type = self._checkStringVariable("storage_type", None)
588 output_fields = self._checkStringVariable("output_fields", None)
590 if not output_fields:
591 raise http.HttpBadRequest("Missing the required 'output_fields'"
595 "nodes": [self.items[0]],
596 "storage_type": storage_type,
597 "output_fields": output_fields.split(","),
601 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
602 """/2/nodes/[node_name]/storage/modify resource.
605 PUT_OPCODE = opcodes.OpNodeModifyStorage
607 def GetPutOpInput(self):
608 """Modifies a storage volume on a node.
611 storage_type = self._checkStringVariable("storage_type", None)
612 name = self._checkStringVariable("name", None)
615 raise http.HttpBadRequest("Missing the required 'name'"
620 if "allocatable" in self.queryargs:
621 changes[constants.SF_ALLOCATABLE] = \
622 bool(self._checkIntVariable("allocatable", default=1))
625 "node_name": self.items[0],
626 "storage_type": storage_type,
632 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
633 """/2/nodes/[node_name]/storage/repair resource.
636 PUT_OPCODE = opcodes.OpRepairNodeStorage
638 def GetPutOpInput(self):
639 """Repairs a storage volume on a node.
642 storage_type = self._checkStringVariable("storage_type", None)
643 name = self._checkStringVariable("name", None)
645 raise http.HttpBadRequest("Missing the required 'name'"
649 "node_name": self.items[0],
650 "storage_type": storage_type,
655 class R_2_networks(baserlib.OpcodeResource):
656 """/2/networks resource.
659 GET_OPCODE = opcodes.OpNetworkQuery
660 POST_OPCODE = opcodes.OpNetworkAdd
662 "name": "network_name",
665 def GetPostOpInput(self):
669 assert not self.items
670 return (self.request_body, {
671 "dry_run": self.dryRun(),
675 """Returns a list of all networks.
678 client = self.GetClient(query=True)
681 bulkdata = client.QueryNetworks([], NET_FIELDS, False)
682 return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
684 data = client.QueryNetworks([], ["name"], False)
685 networknames = [row[0] for row in data]
686 return baserlib.BuildUriList(networknames, "/2/networks/%s",
687 uri_fields=("name", "uri"))
690 class R_2_networks_name(baserlib.OpcodeResource):
691 """/2/networks/[network_name] resource.
694 DELETE_OPCODE = opcodes.OpNetworkRemove
697 """Send information about a network.
700 network_name = self.items[0]
701 client = self.GetClient(query=True)
703 result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
704 names=[network_name],
706 use_locking=self.useLocking())
708 return baserlib.MapFields(NET_FIELDS, result[0])
710 def GetDeleteOpInput(self):
714 assert len(self.items) == 1
715 return (self.request_body, {
716 "network_name": self.items[0],
717 "dry_run": self.dryRun(),
721 class R_2_networks_name_connect(baserlib.OpcodeResource):
722 """/2/networks/[network_name]/connect resource.
725 PUT_OPCODE = opcodes.OpNetworkConnect
727 def GetPutOpInput(self):
728 """Changes some parameters of node group.
732 return (self.request_body, {
733 "network_name": self.items[0],
734 "dry_run": self.dryRun(),
738 class R_2_networks_name_disconnect(baserlib.OpcodeResource):
739 """/2/networks/[network_name]/disconnect resource.
742 PUT_OPCODE = opcodes.OpNetworkDisconnect
744 def GetPutOpInput(self):
745 """Changes some parameters of node group.
749 return (self.request_body, {
750 "network_name": self.items[0],
751 "dry_run": self.dryRun(),
755 class R_2_networks_name_modify(baserlib.OpcodeResource):
756 """/2/networks/[network_name]/modify resource.
759 PUT_OPCODE = opcodes.OpNetworkSetParams
761 def GetPutOpInput(self):
762 """Changes some parameters of network.
766 return (self.request_body, {
767 "network_name": self.items[0],
771 class R_2_groups(baserlib.OpcodeResource):
772 """/2/groups resource.
775 GET_OPCODE = opcodes.OpGroupQuery
776 POST_OPCODE = opcodes.OpGroupAdd
778 "name": "group_name",
781 def GetPostOpInput(self):
782 """Create a node group.
786 assert not self.items
787 return (self.request_body, {
788 "dry_run": self.dryRun(),
792 """Returns a list of all node groups.
795 client = self.GetClient(query=True)
798 bulkdata = client.QueryGroups([], G_FIELDS, False)
799 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
801 data = client.QueryGroups([], ["name"], False)
802 groupnames = [row[0] for row in data]
803 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
804 uri_fields=("name", "uri"))
807 class R_2_groups_name(baserlib.OpcodeResource):
808 """/2/groups/[group_name] resource.
811 DELETE_OPCODE = opcodes.OpGroupRemove
814 """Send information about a node group.
817 group_name = self.items[0]
818 client = self.GetClient(query=True)
820 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
821 names=[group_name], fields=G_FIELDS,
822 use_locking=self.useLocking())
824 return baserlib.MapFields(G_FIELDS, result[0])
826 def GetDeleteOpInput(self):
827 """Delete a node group.
830 assert len(self.items) == 1
832 "group_name": self.items[0],
833 "dry_run": self.dryRun(),
837 class R_2_groups_name_modify(baserlib.OpcodeResource):
838 """/2/groups/[group_name]/modify resource.
841 PUT_OPCODE = opcodes.OpGroupSetParams
843 def GetPutOpInput(self):
844 """Changes some parameters of node group.
848 return (self.request_body, {
849 "group_name": self.items[0],
853 class R_2_groups_name_rename(baserlib.OpcodeResource):
854 """/2/groups/[group_name]/rename resource.
857 PUT_OPCODE = opcodes.OpGroupRename
859 def GetPutOpInput(self):
860 """Changes the name of a node group.
863 assert len(self.items) == 1
864 return (self.request_body, {
865 "group_name": self.items[0],
866 "dry_run": self.dryRun(),
870 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
871 """/2/groups/[group_name]/assign-nodes resource.
874 PUT_OPCODE = opcodes.OpGroupAssignNodes
876 def GetPutOpInput(self):
877 """Assigns nodes to a group.
880 assert len(self.items) == 1
881 return (self.request_body, {
882 "group_name": self.items[0],
883 "dry_run": self.dryRun(),
884 "force": self.useForce(),
888 def _ConvertUsbDevices(data):
889 """Convert in place the usb_devices string to the proper format.
891 In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
892 comma to space because commas cannot be accepted on the command line
893 (they already act as the separator between different hvparams). RAPI
894 should be able to accept commas for backwards compatibility, but we want
895 it to also accept the new space separator. Therefore, we convert
896 spaces into commas here and keep the old parsing logic elsewhere.
900 hvparams = data["hvparams"]
901 usb_devices = hvparams[constants.HV_USB_DEVICES]
902 hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
903 data["hvparams"] = hvparams
905 #No usb_devices, no modification required
909 class R_2_instances(baserlib.OpcodeResource):
910 """/2/instances resource.
913 GET_OPCODE = opcodes.OpInstanceQuery
914 POST_OPCODE = opcodes.OpInstanceCreate
917 "name": "instance_name",
921 """Returns a list of all available instances.
924 client = self.GetClient()
926 use_locking = self.useLocking()
928 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
929 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
931 instancesdata = client.QueryInstances([], ["name"], use_locking)
932 instanceslist = [row[0] for row in instancesdata]
933 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
934 uri_fields=("id", "uri"))
936 def GetPostOpInput(self):
937 """Create an instance.
942 baserlib.CheckType(self.request_body, dict, "Body contents")
944 # Default to request data version 0
945 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
947 if data_version == 0:
948 raise http.HttpBadRequest("Instance creation request version 0 is no"
950 elif data_version != 1:
951 raise http.HttpBadRequest("Unsupported request data version %s" %
954 data = self.request_body.copy()
955 # Remove "__version__"
956 data.pop(_REQ_DATA_VERSION, None)
958 _ConvertUsbDevices(data)
961 "dry_run": self.dryRun(),
965 class R_2_instances_multi_alloc(baserlib.OpcodeResource):
966 """/2/instances-multi-alloc resource.
969 POST_OPCODE = opcodes.OpInstanceMultiAlloc
971 def GetPostOpInput(self):
972 """Try to allocate multiple instances.
974 @return: A dict with submitted jobs, allocatable instances and failed
978 if "instances" not in self.request_body:
979 raise http.HttpBadRequest("Request is missing required 'instances' field"
983 "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
985 body = objects.FillDict(self.request_body, {
986 "instances": [objects.FillDict(inst, op_id)
987 for inst in self.request_body["instances"]],
991 "dry_run": self.dryRun(),
995 class R_2_instances_name(baserlib.OpcodeResource):
996 """/2/instances/[instance_name] resource.
999 GET_OPCODE = opcodes.OpInstanceQuery
1000 DELETE_OPCODE = opcodes.OpInstanceRemove
1003 """Send information about an instance.
1006 client = self.GetClient()
1007 instance_name = self.items[0]
1009 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1010 names=[instance_name],
1012 use_locking=self.useLocking())
1014 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1016 def GetDeleteOpInput(self):
1017 """Delete an instance.
1020 assert len(self.items) == 1
1022 "instance_name": self.items[0],
1023 "ignore_failures": False,
1024 "dry_run": self.dryRun(),
1028 class R_2_instances_name_info(baserlib.OpcodeResource):
1029 """/2/instances/[instance_name]/info resource.
1032 GET_OPCODE = opcodes.OpInstanceQueryData
1034 def GetGetOpInput(self):
1035 """Request detailed instance information.
1038 assert len(self.items) == 1
1040 "instances": [self.items[0]],
1041 "static": bool(self._checkIntVariable("static", default=0)),
1045 class R_2_instances_name_reboot(baserlib.OpcodeResource):
1046 """/2/instances/[instance_name]/reboot resource.
1048 Implements an instance reboot.
1051 POST_OPCODE = opcodes.OpInstanceReboot
1053 def GetPostOpInput(self):
1054 """Reboot an instance.
1056 The URI takes type=[hard|soft|full] and
1057 ignore_secondaries=[False|True] parameters.
1061 "instance_name": self.items[0],
1063 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1064 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1065 "dry_run": self.dryRun(),
1069 class R_2_instances_name_startup(baserlib.OpcodeResource):
1070 """/2/instances/[instance_name]/startup resource.
1072 Implements an instance startup.
1075 PUT_OPCODE = opcodes.OpInstanceStartup
1077 def GetPutOpInput(self):
1078 """Startup an instance.
1080 The URI takes force=[False|True] parameter to start the instance
1081 if even if secondary disks are failing.
1085 "instance_name": self.items[0],
1086 "force": self.useForce(),
1087 "dry_run": self.dryRun(),
1088 "no_remember": bool(self._checkIntVariable("no_remember")),
1092 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1093 """/2/instances/[instance_name]/shutdown resource.
1095 Implements an instance shutdown.
1098 PUT_OPCODE = opcodes.OpInstanceShutdown
1100 def GetPutOpInput(self):
1101 """Shutdown an instance.
1104 return (self.request_body, {
1105 "instance_name": self.items[0],
1106 "no_remember": bool(self._checkIntVariable("no_remember")),
1107 "dry_run": self.dryRun(),
1111 def _ParseInstanceReinstallRequest(name, data):
1112 """Parses a request for reinstalling an instance.
1115 if not isinstance(data, dict):
1116 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1118 ostype = baserlib.CheckParameter(data, "os", default=None)
1119 start = baserlib.CheckParameter(data, "start", exptype=bool,
1121 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1124 opcodes.OpInstanceShutdown(instance_name=name),
1125 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1130 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1135 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1136 """/2/instances/[instance_name]/reinstall resource.
1138 Implements an instance reinstall.
1141 POST_OPCODE = opcodes.OpInstanceReinstall
1144 """Reinstall an instance.
1146 The URI takes os=name and nostartup=[0|1] optional
1147 parameters. By default, the instance will be started
1151 if self.request_body:
1153 raise http.HttpBadRequest("Can't combine query and body parameters")
1155 body = self.request_body
1156 elif self.queryargs:
1157 # Legacy interface, do not modify/extend
1159 "os": self._checkStringVariable("os"),
1160 "start": not self._checkIntVariable("nostartup"),
1165 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1167 return self.SubmitJob(ops)
1170 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1171 """/2/instances/[instance_name]/replace-disks resource.
1174 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1176 def GetPostOpInput(self):
1177 """Replaces disks on an instance.
1181 "instance_name": self.items[0],
1184 if self.request_body:
1185 data = self.request_body
1186 elif self.queryargs:
1187 # Legacy interface, do not modify/extend
1189 "remote_node": self._checkStringVariable("remote_node", default=None),
1190 "mode": self._checkStringVariable("mode", default=None),
1191 "disks": self._checkStringVariable("disks", default=None),
1192 "iallocator": self._checkStringVariable("iallocator", default=None),
1199 raw_disks = data.pop("disks")
1204 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1205 data["disks"] = raw_disks
1207 # Backwards compatibility for strings of the format "1, 2, 3"
1209 data["disks"] = [int(part) for part in raw_disks.split(",")]
1210 except (TypeError, ValueError), err:
1211 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1213 return (data, static)
1216 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1217 """/2/instances/[instance_name]/activate-disks resource.
1220 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1222 def GetPutOpInput(self):
1223 """Activate disks for an instance.
1225 The URI might contain ignore_size to ignore current recorded size.
1229 "instance_name": self.items[0],
1230 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1234 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1235 """/2/instances/[instance_name]/deactivate-disks resource.
1238 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1240 def GetPutOpInput(self):
1241 """Deactivate disks for an instance.
1245 "instance_name": self.items[0],
1249 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1250 """/2/instances/[instance_name]/recreate-disks resource.
1253 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1255 def GetPostOpInput(self):
1256 """Recreate disks for an instance.
1260 "instance_name": self.items[0],
1264 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1265 """/2/instances/[instance_name]/prepare-export resource.
1268 PUT_OPCODE = opcodes.OpBackupPrepare
1270 def GetPutOpInput(self):
1271 """Prepares an export for an instance.
1275 "instance_name": self.items[0],
1276 "mode": self._checkStringVariable("mode"),
1280 class R_2_instances_name_export(baserlib.OpcodeResource):
1281 """/2/instances/[instance_name]/export resource.
1284 PUT_OPCODE = opcodes.OpBackupExport
1286 "destination": "target_node",
1289 def GetPutOpInput(self):
1290 """Exports an instance.
1293 return (self.request_body, {
1294 "instance_name": self.items[0],
1298 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1299 """/2/instances/[instance_name]/migrate resource.
1302 PUT_OPCODE = opcodes.OpInstanceMigrate
1304 def GetPutOpInput(self):
1305 """Migrates an instance.
1308 return (self.request_body, {
1309 "instance_name": self.items[0],
1313 class R_2_instances_name_failover(baserlib.OpcodeResource):
1314 """/2/instances/[instance_name]/failover resource.
1317 PUT_OPCODE = opcodes.OpInstanceFailover
1319 def GetPutOpInput(self):
1320 """Does a failover of an instance.
1323 return (self.request_body, {
1324 "instance_name": self.items[0],
1328 class R_2_instances_name_rename(baserlib.OpcodeResource):
1329 """/2/instances/[instance_name]/rename resource.
1332 PUT_OPCODE = opcodes.OpInstanceRename
1334 def GetPutOpInput(self):
1335 """Changes the name of an instance.
1338 return (self.request_body, {
1339 "instance_name": self.items[0],
1343 class R_2_instances_name_modify(baserlib.OpcodeResource):
1344 """/2/instances/[instance_name]/modify resource.
1347 PUT_OPCODE = opcodes.OpInstanceSetParams
1349 def GetPutOpInput(self):
1350 """Changes parameters of an instance.
1353 data = self.request_body.copy()
1354 _ConvertUsbDevices(data)
1357 "instance_name": self.items[0],
1361 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1362 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1365 POST_OPCODE = opcodes.OpInstanceGrowDisk
1367 def GetPostOpInput(self):
1368 """Increases the size of an instance disk.
1371 return (self.request_body, {
1372 "instance_name": self.items[0],
1373 "disk": int(self.items[1]),
1377 class R_2_instances_name_console(baserlib.ResourceBase):
1378 """/2/instances/[instance_name]/console resource.
1381 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1382 GET_OPCODE = opcodes.OpInstanceConsole
1385 """Request information for connecting to instance's console.
1387 @return: Serialized instance console description, see
1388 L{objects.InstanceConsole}
1391 client = self.GetClient()
1393 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1396 raise http.HttpServiceUnavailable("Instance console unavailable")
1398 assert isinstance(console, dict)
1402 def _GetQueryFields(args):
1403 """Tries to extract C{fields} query parameter.
1405 @type args: dictionary
1406 @rtype: list of string
1407 @raise http.HttpBadRequest: When parameter can't be found
1411 fields = args["fields"]
1413 raise http.HttpBadRequest("Missing 'fields' query argument")
1415 return _SplitQueryFields(fields[0])
1418 def _SplitQueryFields(fields):
1419 """Splits fields as given for a query request.
1421 @type fields: string
1422 @rtype: list of string
1425 return [i.strip() for i in fields.split(",")]
1428 class R_2_query(baserlib.ResourceBase):
1429 """/2/query/[resource] resource.
1432 # Results might contain sensitive information
1433 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1434 PUT_ACCESS = GET_ACCESS
1435 GET_OPCODE = opcodes.OpQuery
1436 PUT_OPCODE = opcodes.OpQuery
1438 def _Query(self, fields, qfilter):
1439 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1442 """Returns resource information.
1444 @return: Query result, see L{objects.QueryResponse}
1447 return self._Query(_GetQueryFields(self.queryargs), None)
1450 """Submits job querying for resources.
1452 @return: Query result, see L{objects.QueryResponse}
1455 body = self.request_body
1457 baserlib.CheckType(body, dict, "Body contents")
1460 fields = body["fields"]
1462 fields = _GetQueryFields(self.queryargs)
1464 qfilter = body.get("qfilter", None)
1465 # TODO: remove this after 2.7
1467 qfilter = body.get("filter", None)
1469 return self._Query(fields, qfilter)
1472 class R_2_query_fields(baserlib.ResourceBase):
1473 """/2/query/[resource]/fields resource.
1476 GET_OPCODE = opcodes.OpQueryFields
1479 """Retrieves list of available fields for a resource.
1481 @return: List of serialized L{objects.QueryFieldDefinition}
1485 raw_fields = self.queryargs["fields"]
1489 fields = _SplitQueryFields(raw_fields[0])
1491 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1494 class _R_Tags(baserlib.OpcodeResource):
1495 """Quasiclass for tagging resources.
1497 Manages tags. When inheriting this class you must define the
1502 GET_OPCODE = opcodes.OpTagsGet
1503 PUT_OPCODE = opcodes.OpTagsSet
1504 DELETE_OPCODE = opcodes.OpTagsDel
1506 def __init__(self, items, queryargs, req, **kwargs):
1507 """A tag resource constructor.
1509 We have to override the default to sort out cluster naming case.
1512 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1514 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1517 self.name = items[0]
1520 """Returns a list of tags.
1522 Example: ["tag1", "tag2", "tag3"]
1525 kind = self.TAG_LEVEL
1527 if kind in (constants.TAG_INSTANCE,
1528 constants.TAG_NODEGROUP,
1530 constants.TAG_NETWORK):
1532 raise http.HttpBadRequest("Missing name on tag request")
1534 cl = self.GetClient(query=True)
1535 tags = list(cl.QueryTags(kind, self.name))
1537 elif kind == constants.TAG_CLUSTER:
1538 assert not self.name
1539 # TODO: Use query API?
1540 ssc = ssconf.SimpleStore()
1541 tags = ssc.GetClusterTags()
1544 raise http.HttpBadRequest("Unhandled tag type!")
1548 def GetPutOpInput(self):
1549 """Add a set of tags.
1551 The request as a list of strings should be PUT to this URI. And
1552 you'll have back a job id.
1556 "kind": self.TAG_LEVEL,
1558 "tags": self.queryargs.get("tag", []),
1559 "dry_run": self.dryRun(),
1562 def GetDeleteOpInput(self):
1565 In order to delete a set of tags, the DELETE
1566 request should be addressed to URI like:
1567 /tags?tag=[tag]&tag=[tag]
1571 return self.GetPutOpInput()
1574 class R_2_instances_name_tags(_R_Tags):
1575 """ /2/instances/[instance_name]/tags resource.
1577 Manages per-instance tags.
1580 TAG_LEVEL = constants.TAG_INSTANCE
1583 class R_2_nodes_name_tags(_R_Tags):
1584 """ /2/nodes/[node_name]/tags resource.
1586 Manages per-node tags.
1589 TAG_LEVEL = constants.TAG_NODE
1592 class R_2_groups_name_tags(_R_Tags):
1593 """ /2/groups/[group_name]/tags resource.
1595 Manages per-nodegroup tags.
1598 TAG_LEVEL = constants.TAG_NODEGROUP
1601 class R_2_networks_name_tags(_R_Tags):
1602 """ /2/networks/[network_name]/tags resource.
1604 Manages per-network tags.
1607 TAG_LEVEL = constants.TAG_NETWORK
1610 class R_2_tags(_R_Tags):
1611 """ /2/tags resource.
1613 Manages cluster tags.
1616 TAG_LEVEL = constants.TAG_CLUSTER