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.spindles", "disk_usage", "disk.uuids",
79 "beparams", "hvparams",
80 "oper_state", "oper_ram", "oper_vcpus", "status",
81 "custom_hvparams", "custom_beparams", "custom_nicparams",
84 N_FIELDS = ["name", "offline", "master_candidate", "drained",
85 "dtotal", "dfree", "sptotal", "spfree",
86 "mtotal", "mnode", "mfree",
87 "pinst_cnt", "sinst_cnt",
88 "ctotal", "cnos", "cnodes", "csockets",
90 "pinst_list", "sinst_list",
91 "master_capable", "vm_capable",
96 NET_FIELDS = ["name", "network", "gateway",
97 "network6", "gateway6",
99 "free_count", "reserved_count",
100 "map", "group_list", "inst_list",
101 "external_reservations",
118 "id", "ops", "status", "summary",
120 "received_ts", "start_ts", "end_ts",
123 J_FIELDS = J_FIELDS_BULK + [
128 _NR_DRAINED = "drained"
129 _NR_MASTER_CANDIDATE = "master-candidate"
130 _NR_MASTER = "master"
131 _NR_OFFLINE = "offline"
132 _NR_REGULAR = "regular"
135 constants.NR_MASTER: _NR_MASTER,
136 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
137 constants.NR_DRAINED: _NR_DRAINED,
138 constants.NR_OFFLINE: _NR_OFFLINE,
139 constants.NR_REGULAR: _NR_REGULAR,
142 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
144 # Request data version field
145 _REQ_DATA_VERSION = "__version__"
147 # Feature string for instance creation request data version 1
148 _INST_CREATE_REQV1 = "instance-create-reqv1"
150 # Feature string for instance reinstall request version 1
151 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
153 # Feature string for node migration version 1
154 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
156 # Feature string for node evacuation with LU-generated jobs
157 _NODE_EVAC_RES1 = "node-evac-res1"
159 ALL_FEATURES = compat.UniqueFrozenset([
161 _INST_REINSTALL_REQV1,
166 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
170 # FIXME: For compatibility we update the beparams/memory field. Needs to be
171 # removed in Ganeti 2.8
172 def _UpdateBeparams(inst):
173 """Updates the beparams dict of inst to support the memory field.
175 @param inst: Inst dict
176 @return: Updated inst dict
179 beparams = inst["beparams"]
180 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
185 class R_root(baserlib.ResourceBase):
191 """Supported for legacy reasons.
203 class R_version(baserlib.ResourceBase):
204 """/version resource.
206 This resource should be used to determine the remote API version and
207 to adapt clients accordingly.
212 """Returns the remote API version.
215 return constants.RAPI_VERSION
218 class R_2_info(baserlib.OpcodeResource):
222 GET_OPCODE = opcodes.OpClusterQuery
225 """Returns cluster information.
228 client = self.GetClient(query=True)
229 return client.QueryClusterInfo()
232 class R_2_features(baserlib.ResourceBase):
233 """/2/features resource.
238 """Returns list of optional RAPI features implemented.
241 return list(ALL_FEATURES)
244 class R_2_os(baserlib.OpcodeResource):
248 GET_OPCODE = opcodes.OpOsDiagnose
251 """Return a list of all OSes.
253 Can return error 500 in case of a problem.
255 Example: ["debian-etch"]
258 cl = self.GetClient()
259 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
260 job_id = self.SubmitJob([op], cl=cl)
261 # we use custom feedback function, instead of print we log the status
262 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
263 diagnose_data = result[0]
265 if not isinstance(diagnose_data, list):
266 raise http.HttpBadGateway(message="Can't get OS list")
269 for (name, variants) in diagnose_data:
270 os_names.extend(cli.CalculateOSNames(name, variants))
275 class R_2_redist_config(baserlib.OpcodeResource):
276 """/2/redistribute-config resource.
279 PUT_OPCODE = opcodes.OpClusterRedistConf
282 class R_2_cluster_modify(baserlib.OpcodeResource):
283 """/2/modify resource.
286 PUT_OPCODE = opcodes.OpClusterSetParams
289 class R_2_jobs(baserlib.ResourceBase):
294 """Returns a dictionary of jobs.
296 @return: a dictionary with jobs id and uri.
299 client = self.GetClient(query=True)
302 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
303 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
305 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
306 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
307 uri_fields=("id", "uri"))
310 class R_2_jobs_id(baserlib.ResourceBase):
311 """/2/jobs/[job_id] resource.
315 """Returns a job status.
317 @return: a dictionary with job parameters.
319 - id: job ID as a number
320 - status: current job status as a string
321 - ops: involved OpCodes as a list of dictionaries for each
323 - opstatus: OpCodes status as a list
324 - opresult: OpCodes results as a list of lists
327 job_id = self.items[0]
328 result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
330 raise http.HttpNotFound()
331 return baserlib.MapFields(J_FIELDS, result)
334 """Cancel not-yet-started job.
337 job_id = self.items[0]
338 result = self.GetClient().CancelJob(job_id)
342 class R_2_jobs_id_wait(baserlib.ResourceBase):
343 """/2/jobs/[job_id]/wait resource.
346 # WaitForJobChange provides access to sensitive information and blocks
347 # machine resources (it's a blocking RAPI call), hence restricting access.
348 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
351 """Waits for job changes.
354 job_id = self.items[0]
356 fields = self.getBodyParameter("fields")
357 prev_job_info = self.getBodyParameter("previous_job_info", None)
358 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
360 if not isinstance(fields, list):
361 raise http.HttpBadRequest("The 'fields' parameter should be a list")
363 if not (prev_job_info is None or isinstance(prev_job_info, list)):
364 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
367 if not (prev_log_serial is None or
368 isinstance(prev_log_serial, (int, long))):
369 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
372 client = self.GetClient()
373 result = client.WaitForJobChangeOnce(job_id, fields,
374 prev_job_info, prev_log_serial,
375 timeout=_WFJC_TIMEOUT)
377 raise http.HttpNotFound()
379 if result == constants.JOB_NOTCHANGED:
383 (job_info, log_entries) = result
386 "job_info": job_info,
387 "log_entries": log_entries,
391 class R_2_nodes(baserlib.OpcodeResource):
392 """/2/nodes resource.
395 GET_OPCODE = opcodes.OpNodeQuery
398 """Returns a list of all nodes.
401 client = self.GetClient(query=True)
404 bulkdata = client.QueryNodes([], N_FIELDS, False)
405 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
407 nodesdata = client.QueryNodes([], ["name"], False)
408 nodeslist = [row[0] for row in nodesdata]
409 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
410 uri_fields=("id", "uri"))
413 class R_2_nodes_name(baserlib.OpcodeResource):
414 """/2/nodes/[node_name] resource.
417 GET_OPCODE = opcodes.OpNodeQuery
420 """Send information about a node.
423 node_name = self.items[0]
424 client = self.GetClient(query=True)
426 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
427 names=[node_name], fields=N_FIELDS,
428 use_locking=self.useLocking())
430 return baserlib.MapFields(N_FIELDS, result[0])
433 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
434 """/2/nodes/[node_name]/powercycle resource.
437 POST_OPCODE = opcodes.OpNodePowercycle
439 def GetPostOpInput(self):
440 """Tries to powercycle a node.
443 return (self.request_body, {
444 "node_name": self.items[0],
445 "force": self.useForce(),
449 class R_2_nodes_name_role(baserlib.OpcodeResource):
450 """/2/nodes/[node_name]/role resource.
453 PUT_OPCODE = opcodes.OpNodeSetParams
456 """Returns the current node role.
461 node_name = self.items[0]
462 client = self.GetClient(query=True)
463 result = client.QueryNodes(names=[node_name], fields=["role"],
464 use_locking=self.useLocking())
466 return _NR_MAP[result[0][0]]
468 def GetPutOpInput(self):
469 """Sets the node role.
472 baserlib.CheckType(self.request_body, basestring, "Body contents")
474 role = self.request_body
476 if role == _NR_REGULAR:
481 elif role == _NR_MASTER_CANDIDATE:
483 offline = drained = None
485 elif role == _NR_DRAINED:
487 candidate = offline = None
489 elif role == _NR_OFFLINE:
491 candidate = drained = None
494 raise http.HttpBadRequest("Can't set '%s' role" % role)
496 assert len(self.items) == 1
499 "node_name": self.items[0],
500 "master_candidate": candidate,
503 "force": self.useForce(),
504 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
508 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
509 """/2/nodes/[node_name]/evacuate resource.
512 POST_OPCODE = opcodes.OpNodeEvacuate
514 def GetPostOpInput(self):
515 """Evacuate all instances off a node.
518 return (self.request_body, {
519 "node_name": self.items[0],
520 "dry_run": self.dryRun(),
524 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
525 """/2/nodes/[node_name]/migrate resource.
528 POST_OPCODE = opcodes.OpNodeMigrate
530 def GetPostOpInput(self):
531 """Migrate all primary instances from a node.
535 # Support old-style requests
536 if "live" in self.queryargs and "mode" in self.queryargs:
537 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
540 if "live" in self.queryargs:
541 if self._checkIntVariable("live", default=1):
542 mode = constants.HT_MIGRATION_LIVE
544 mode = constants.HT_MIGRATION_NONLIVE
546 mode = self._checkStringVariable("mode", default=None)
552 data = self.request_body
555 "node_name": self.items[0],
559 class R_2_nodes_name_modify(baserlib.OpcodeResource):
560 """/2/nodes/[node_name]/modify resource.
563 POST_OPCODE = opcodes.OpNodeSetParams
565 def GetPostOpInput(self):
566 """Changes parameters of a node.
569 assert len(self.items) == 1
571 return (self.request_body, {
572 "node_name": self.items[0],
576 class R_2_nodes_name_storage(baserlib.OpcodeResource):
577 """/2/nodes/[node_name]/storage resource.
580 # LUNodeQueryStorage acquires locks, hence restricting access to GET
581 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
582 GET_OPCODE = opcodes.OpNodeQueryStorage
584 def GetGetOpInput(self):
585 """List storage available on a node.
588 storage_type = self._checkStringVariable("storage_type", None)
589 output_fields = self._checkStringVariable("output_fields", None)
591 if not output_fields:
592 raise http.HttpBadRequest("Missing the required 'output_fields'"
596 "nodes": [self.items[0]],
597 "storage_type": storage_type,
598 "output_fields": output_fields.split(","),
602 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
603 """/2/nodes/[node_name]/storage/modify resource.
606 PUT_OPCODE = opcodes.OpNodeModifyStorage
608 def GetPutOpInput(self):
609 """Modifies a storage volume on a node.
612 storage_type = self._checkStringVariable("storage_type", None)
613 name = self._checkStringVariable("name", None)
616 raise http.HttpBadRequest("Missing the required 'name'"
621 if "allocatable" in self.queryargs:
622 changes[constants.SF_ALLOCATABLE] = \
623 bool(self._checkIntVariable("allocatable", default=1))
626 "node_name": self.items[0],
627 "storage_type": storage_type,
633 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
634 """/2/nodes/[node_name]/storage/repair resource.
637 PUT_OPCODE = opcodes.OpRepairNodeStorage
639 def GetPutOpInput(self):
640 """Repairs a storage volume on a node.
643 storage_type = self._checkStringVariable("storage_type", None)
644 name = self._checkStringVariable("name", None)
646 raise http.HttpBadRequest("Missing the required 'name'"
650 "node_name": self.items[0],
651 "storage_type": storage_type,
656 class R_2_networks(baserlib.OpcodeResource):
657 """/2/networks resource.
660 GET_OPCODE = opcodes.OpNetworkQuery
661 POST_OPCODE = opcodes.OpNetworkAdd
663 "name": "network_name",
666 def GetPostOpInput(self):
670 assert not self.items
671 return (self.request_body, {
672 "dry_run": self.dryRun(),
676 """Returns a list of all networks.
679 client = self.GetClient(query=True)
682 bulkdata = client.QueryNetworks([], NET_FIELDS, False)
683 return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
685 data = client.QueryNetworks([], ["name"], False)
686 networknames = [row[0] for row in data]
687 return baserlib.BuildUriList(networknames, "/2/networks/%s",
688 uri_fields=("name", "uri"))
691 class R_2_networks_name(baserlib.OpcodeResource):
692 """/2/networks/[network_name] resource.
695 DELETE_OPCODE = opcodes.OpNetworkRemove
698 """Send information about a network.
701 network_name = self.items[0]
702 client = self.GetClient(query=True)
704 result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
705 names=[network_name],
707 use_locking=self.useLocking())
709 return baserlib.MapFields(NET_FIELDS, result[0])
711 def GetDeleteOpInput(self):
715 assert len(self.items) == 1
716 return (self.request_body, {
717 "network_name": self.items[0],
718 "dry_run": self.dryRun(),
722 class R_2_networks_name_connect(baserlib.OpcodeResource):
723 """/2/networks/[network_name]/connect resource.
726 PUT_OPCODE = opcodes.OpNetworkConnect
728 def GetPutOpInput(self):
729 """Changes some parameters of node group.
733 return (self.request_body, {
734 "network_name": self.items[0],
735 "dry_run": self.dryRun(),
739 class R_2_networks_name_disconnect(baserlib.OpcodeResource):
740 """/2/networks/[network_name]/disconnect resource.
743 PUT_OPCODE = opcodes.OpNetworkDisconnect
745 def GetPutOpInput(self):
746 """Changes some parameters of node group.
750 return (self.request_body, {
751 "network_name": self.items[0],
752 "dry_run": self.dryRun(),
756 class R_2_networks_name_modify(baserlib.OpcodeResource):
757 """/2/networks/[network_name]/modify resource.
760 PUT_OPCODE = opcodes.OpNetworkSetParams
762 def GetPutOpInput(self):
763 """Changes some parameters of network.
767 return (self.request_body, {
768 "network_name": self.items[0],
772 class R_2_groups(baserlib.OpcodeResource):
773 """/2/groups resource.
776 GET_OPCODE = opcodes.OpGroupQuery
777 POST_OPCODE = opcodes.OpGroupAdd
779 "name": "group_name",
782 def GetPostOpInput(self):
783 """Create a node group.
787 assert not self.items
788 return (self.request_body, {
789 "dry_run": self.dryRun(),
793 """Returns a list of all node groups.
796 client = self.GetClient(query=True)
799 bulkdata = client.QueryGroups([], G_FIELDS, False)
800 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
802 data = client.QueryGroups([], ["name"], False)
803 groupnames = [row[0] for row in data]
804 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
805 uri_fields=("name", "uri"))
808 class R_2_groups_name(baserlib.OpcodeResource):
809 """/2/groups/[group_name] resource.
812 DELETE_OPCODE = opcodes.OpGroupRemove
815 """Send information about a node group.
818 group_name = self.items[0]
819 client = self.GetClient(query=True)
821 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
822 names=[group_name], fields=G_FIELDS,
823 use_locking=self.useLocking())
825 return baserlib.MapFields(G_FIELDS, result[0])
827 def GetDeleteOpInput(self):
828 """Delete a node group.
831 assert len(self.items) == 1
833 "group_name": self.items[0],
834 "dry_run": self.dryRun(),
838 class R_2_groups_name_modify(baserlib.OpcodeResource):
839 """/2/groups/[group_name]/modify resource.
842 PUT_OPCODE = opcodes.OpGroupSetParams
844 def GetPutOpInput(self):
845 """Changes some parameters of node group.
849 return (self.request_body, {
850 "group_name": self.items[0],
854 class R_2_groups_name_rename(baserlib.OpcodeResource):
855 """/2/groups/[group_name]/rename resource.
858 PUT_OPCODE = opcodes.OpGroupRename
860 def GetPutOpInput(self):
861 """Changes the name of a node group.
864 assert len(self.items) == 1
865 return (self.request_body, {
866 "group_name": self.items[0],
867 "dry_run": self.dryRun(),
871 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
872 """/2/groups/[group_name]/assign-nodes resource.
875 PUT_OPCODE = opcodes.OpGroupAssignNodes
877 def GetPutOpInput(self):
878 """Assigns nodes to a group.
881 assert len(self.items) == 1
882 return (self.request_body, {
883 "group_name": self.items[0],
884 "dry_run": self.dryRun(),
885 "force": self.useForce(),
889 def _ConvertUsbDevices(data):
890 """Convert in place the usb_devices string to the proper format.
892 In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
893 comma to space because commas cannot be accepted on the command line
894 (they already act as the separator between different hvparams). RAPI
895 should be able to accept commas for backwards compatibility, but we want
896 it to also accept the new space separator. Therefore, we convert
897 spaces into commas here and keep the old parsing logic elsewhere.
901 hvparams = data["hvparams"]
902 usb_devices = hvparams[constants.HV_USB_DEVICES]
903 hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
904 data["hvparams"] = hvparams
906 #No usb_devices, no modification required
910 class R_2_instances(baserlib.OpcodeResource):
911 """/2/instances resource.
914 GET_OPCODE = opcodes.OpInstanceQuery
915 POST_OPCODE = opcodes.OpInstanceCreate
918 "name": "instance_name",
922 """Returns a list of all available instances.
925 client = self.GetClient()
927 use_locking = self.useLocking()
929 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
930 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
932 instancesdata = client.QueryInstances([], ["name"], use_locking)
933 instanceslist = [row[0] for row in instancesdata]
934 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
935 uri_fields=("id", "uri"))
937 def GetPostOpInput(self):
938 """Create an instance.
943 baserlib.CheckType(self.request_body, dict, "Body contents")
945 # Default to request data version 0
946 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
948 if data_version == 0:
949 raise http.HttpBadRequest("Instance creation request version 0 is no"
951 elif data_version != 1:
952 raise http.HttpBadRequest("Unsupported request data version %s" %
955 data = self.request_body.copy()
956 # Remove "__version__"
957 data.pop(_REQ_DATA_VERSION, None)
959 _ConvertUsbDevices(data)
962 "dry_run": self.dryRun(),
966 class R_2_instances_multi_alloc(baserlib.OpcodeResource):
967 """/2/instances-multi-alloc resource.
970 POST_OPCODE = opcodes.OpInstanceMultiAlloc
972 def GetPostOpInput(self):
973 """Try to allocate multiple instances.
975 @return: A dict with submitted jobs, allocatable instances and failed
979 if "instances" not in self.request_body:
980 raise http.HttpBadRequest("Request is missing required 'instances' field"
983 # Unlike most other RAPI calls, this one is composed of individual opcodes,
984 # and we have to do the filling ourselves
987 "name": "instance_name",
990 body = objects.FillDict(self.request_body, {
992 baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {}, OPCODE_RENAME)
993 for inst in self.request_body["instances"]
998 "dry_run": self.dryRun(),
1002 class R_2_instances_name(baserlib.OpcodeResource):
1003 """/2/instances/[instance_name] resource.
1006 GET_OPCODE = opcodes.OpInstanceQuery
1007 DELETE_OPCODE = opcodes.OpInstanceRemove
1010 """Send information about an instance.
1013 client = self.GetClient()
1014 instance_name = self.items[0]
1016 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
1017 names=[instance_name],
1019 use_locking=self.useLocking())
1021 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
1023 def GetDeleteOpInput(self):
1024 """Delete an instance.
1027 assert len(self.items) == 1
1029 "instance_name": self.items[0],
1030 "ignore_failures": False,
1031 "dry_run": self.dryRun(),
1035 class R_2_instances_name_info(baserlib.OpcodeResource):
1036 """/2/instances/[instance_name]/info resource.
1039 GET_OPCODE = opcodes.OpInstanceQueryData
1041 def GetGetOpInput(self):
1042 """Request detailed instance information.
1045 assert len(self.items) == 1
1047 "instances": [self.items[0]],
1048 "static": bool(self._checkIntVariable("static", default=0)),
1052 class R_2_instances_name_reboot(baserlib.OpcodeResource):
1053 """/2/instances/[instance_name]/reboot resource.
1055 Implements an instance reboot.
1058 POST_OPCODE = opcodes.OpInstanceReboot
1060 def GetPostOpInput(self):
1061 """Reboot an instance.
1063 The URI takes type=[hard|soft|full] and
1064 ignore_secondaries=[False|True] parameters.
1068 "instance_name": self.items[0],
1070 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1071 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1072 "dry_run": self.dryRun(),
1076 class R_2_instances_name_startup(baserlib.OpcodeResource):
1077 """/2/instances/[instance_name]/startup resource.
1079 Implements an instance startup.
1082 PUT_OPCODE = opcodes.OpInstanceStartup
1084 def GetPutOpInput(self):
1085 """Startup an instance.
1087 The URI takes force=[False|True] parameter to start the instance
1088 if even if secondary disks are failing.
1092 "instance_name": self.items[0],
1093 "force": self.useForce(),
1094 "dry_run": self.dryRun(),
1095 "no_remember": bool(self._checkIntVariable("no_remember")),
1099 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1100 """/2/instances/[instance_name]/shutdown resource.
1102 Implements an instance shutdown.
1105 PUT_OPCODE = opcodes.OpInstanceShutdown
1107 def GetPutOpInput(self):
1108 """Shutdown an instance.
1111 return (self.request_body, {
1112 "instance_name": self.items[0],
1113 "no_remember": bool(self._checkIntVariable("no_remember")),
1114 "dry_run": self.dryRun(),
1118 def _ParseInstanceReinstallRequest(name, data):
1119 """Parses a request for reinstalling an instance.
1122 if not isinstance(data, dict):
1123 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1125 ostype = baserlib.CheckParameter(data, "os", default=None)
1126 start = baserlib.CheckParameter(data, "start", exptype=bool,
1128 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1131 opcodes.OpInstanceShutdown(instance_name=name),
1132 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1137 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1142 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1143 """/2/instances/[instance_name]/reinstall resource.
1145 Implements an instance reinstall.
1148 POST_OPCODE = opcodes.OpInstanceReinstall
1151 """Reinstall an instance.
1153 The URI takes os=name and nostartup=[0|1] optional
1154 parameters. By default, the instance will be started
1158 if self.request_body:
1160 raise http.HttpBadRequest("Can't combine query and body parameters")
1162 body = self.request_body
1163 elif self.queryargs:
1164 # Legacy interface, do not modify/extend
1166 "os": self._checkStringVariable("os"),
1167 "start": not self._checkIntVariable("nostartup"),
1172 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1174 return self.SubmitJob(ops)
1177 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1178 """/2/instances/[instance_name]/replace-disks resource.
1181 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1183 def GetPostOpInput(self):
1184 """Replaces disks on an instance.
1188 "instance_name": self.items[0],
1191 if self.request_body:
1192 data = self.request_body
1193 elif self.queryargs:
1194 # Legacy interface, do not modify/extend
1196 "remote_node": self._checkStringVariable("remote_node", default=None),
1197 "mode": self._checkStringVariable("mode", default=None),
1198 "disks": self._checkStringVariable("disks", default=None),
1199 "iallocator": self._checkStringVariable("iallocator", default=None),
1206 raw_disks = data.pop("disks")
1211 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1212 data["disks"] = raw_disks
1214 # Backwards compatibility for strings of the format "1, 2, 3"
1216 data["disks"] = [int(part) for part in raw_disks.split(",")]
1217 except (TypeError, ValueError), err:
1218 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1220 return (data, static)
1223 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1224 """/2/instances/[instance_name]/activate-disks resource.
1227 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1229 def GetPutOpInput(self):
1230 """Activate disks for an instance.
1232 The URI might contain ignore_size to ignore current recorded size.
1236 "instance_name": self.items[0],
1237 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1241 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1242 """/2/instances/[instance_name]/deactivate-disks resource.
1245 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1247 def GetPutOpInput(self):
1248 """Deactivate disks for an instance.
1252 "instance_name": self.items[0],
1256 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1257 """/2/instances/[instance_name]/recreate-disks resource.
1260 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1262 def GetPostOpInput(self):
1263 """Recreate disks for an instance.
1267 "instance_name": self.items[0],
1271 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1272 """/2/instances/[instance_name]/prepare-export resource.
1275 PUT_OPCODE = opcodes.OpBackupPrepare
1277 def GetPutOpInput(self):
1278 """Prepares an export for an instance.
1282 "instance_name": self.items[0],
1283 "mode": self._checkStringVariable("mode"),
1287 class R_2_instances_name_export(baserlib.OpcodeResource):
1288 """/2/instances/[instance_name]/export resource.
1291 PUT_OPCODE = opcodes.OpBackupExport
1293 "destination": "target_node",
1296 def GetPutOpInput(self):
1297 """Exports an instance.
1300 return (self.request_body, {
1301 "instance_name": self.items[0],
1305 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1306 """/2/instances/[instance_name]/migrate resource.
1309 PUT_OPCODE = opcodes.OpInstanceMigrate
1311 def GetPutOpInput(self):
1312 """Migrates an instance.
1315 return (self.request_body, {
1316 "instance_name": self.items[0],
1320 class R_2_instances_name_failover(baserlib.OpcodeResource):
1321 """/2/instances/[instance_name]/failover resource.
1324 PUT_OPCODE = opcodes.OpInstanceFailover
1326 def GetPutOpInput(self):
1327 """Does a failover of an instance.
1330 return (self.request_body, {
1331 "instance_name": self.items[0],
1335 class R_2_instances_name_rename(baserlib.OpcodeResource):
1336 """/2/instances/[instance_name]/rename resource.
1339 PUT_OPCODE = opcodes.OpInstanceRename
1341 def GetPutOpInput(self):
1342 """Changes the name of an instance.
1345 return (self.request_body, {
1346 "instance_name": self.items[0],
1350 class R_2_instances_name_modify(baserlib.OpcodeResource):
1351 """/2/instances/[instance_name]/modify resource.
1354 PUT_OPCODE = opcodes.OpInstanceSetParams
1356 def GetPutOpInput(self):
1357 """Changes parameters of an instance.
1360 data = self.request_body.copy()
1361 _ConvertUsbDevices(data)
1364 "instance_name": self.items[0],
1368 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1369 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1372 POST_OPCODE = opcodes.OpInstanceGrowDisk
1374 def GetPostOpInput(self):
1375 """Increases the size of an instance disk.
1378 return (self.request_body, {
1379 "instance_name": self.items[0],
1380 "disk": int(self.items[1]),
1384 class R_2_instances_name_console(baserlib.ResourceBase):
1385 """/2/instances/[instance_name]/console resource.
1388 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1389 GET_OPCODE = opcodes.OpInstanceConsole
1392 """Request information for connecting to instance's console.
1394 @return: Serialized instance console description, see
1395 L{objects.InstanceConsole}
1398 client = self.GetClient()
1400 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1403 raise http.HttpServiceUnavailable("Instance console unavailable")
1405 assert isinstance(console, dict)
1409 def _GetQueryFields(args):
1410 """Tries to extract C{fields} query parameter.
1412 @type args: dictionary
1413 @rtype: list of string
1414 @raise http.HttpBadRequest: When parameter can't be found
1418 fields = args["fields"]
1420 raise http.HttpBadRequest("Missing 'fields' query argument")
1422 return _SplitQueryFields(fields[0])
1425 def _SplitQueryFields(fields):
1426 """Splits fields as given for a query request.
1428 @type fields: string
1429 @rtype: list of string
1432 return [i.strip() for i in fields.split(",")]
1435 class R_2_query(baserlib.ResourceBase):
1436 """/2/query/[resource] resource.
1439 # Results might contain sensitive information
1440 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1441 PUT_ACCESS = GET_ACCESS
1442 GET_OPCODE = opcodes.OpQuery
1443 PUT_OPCODE = opcodes.OpQuery
1445 def _Query(self, fields, qfilter):
1446 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1449 """Returns resource information.
1451 @return: Query result, see L{objects.QueryResponse}
1454 return self._Query(_GetQueryFields(self.queryargs), None)
1457 """Submits job querying for resources.
1459 @return: Query result, see L{objects.QueryResponse}
1462 body = self.request_body
1464 baserlib.CheckType(body, dict, "Body contents")
1467 fields = body["fields"]
1469 fields = _GetQueryFields(self.queryargs)
1471 qfilter = body.get("qfilter", None)
1472 # TODO: remove this after 2.7
1474 qfilter = body.get("filter", None)
1476 return self._Query(fields, qfilter)
1479 class R_2_query_fields(baserlib.ResourceBase):
1480 """/2/query/[resource]/fields resource.
1483 GET_OPCODE = opcodes.OpQueryFields
1486 """Retrieves list of available fields for a resource.
1488 @return: List of serialized L{objects.QueryFieldDefinition}
1492 raw_fields = self.queryargs["fields"]
1496 fields = _SplitQueryFields(raw_fields[0])
1498 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1501 class _R_Tags(baserlib.OpcodeResource):
1502 """Quasiclass for tagging resources.
1504 Manages tags. When inheriting this class you must define the
1509 GET_OPCODE = opcodes.OpTagsGet
1510 PUT_OPCODE = opcodes.OpTagsSet
1511 DELETE_OPCODE = opcodes.OpTagsDel
1513 def __init__(self, items, queryargs, req, **kwargs):
1514 """A tag resource constructor.
1516 We have to override the default to sort out cluster naming case.
1519 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1521 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1524 self.name = items[0]
1527 """Returns a list of tags.
1529 Example: ["tag1", "tag2", "tag3"]
1532 kind = self.TAG_LEVEL
1534 if kind in (constants.TAG_INSTANCE,
1535 constants.TAG_NODEGROUP,
1537 constants.TAG_NETWORK):
1539 raise http.HttpBadRequest("Missing name on tag request")
1541 cl = self.GetClient(query=True)
1542 tags = list(cl.QueryTags(kind, self.name))
1544 elif kind == constants.TAG_CLUSTER:
1545 assert not self.name
1546 # TODO: Use query API?
1547 ssc = ssconf.SimpleStore()
1548 tags = ssc.GetClusterTags()
1551 raise http.HttpBadRequest("Unhandled tag type!")
1555 def GetPutOpInput(self):
1556 """Add a set of tags.
1558 The request as a list of strings should be PUT to this URI. And
1559 you'll have back a job id.
1563 "kind": self.TAG_LEVEL,
1565 "tags": self.queryargs.get("tag", []),
1566 "dry_run": self.dryRun(),
1569 def GetDeleteOpInput(self):
1572 In order to delete a set of tags, the DELETE
1573 request should be addressed to URI like:
1574 /tags?tag=[tag]&tag=[tag]
1578 return self.GetPutOpInput()
1581 class R_2_instances_name_tags(_R_Tags):
1582 """ /2/instances/[instance_name]/tags resource.
1584 Manages per-instance tags.
1587 TAG_LEVEL = constants.TAG_INSTANCE
1590 class R_2_nodes_name_tags(_R_Tags):
1591 """ /2/nodes/[node_name]/tags resource.
1593 Manages per-node tags.
1596 TAG_LEVEL = constants.TAG_NODE
1599 class R_2_groups_name_tags(_R_Tags):
1600 """ /2/groups/[group_name]/tags resource.
1602 Manages per-nodegroup tags.
1605 TAG_LEVEL = constants.TAG_NODEGROUP
1608 class R_2_networks_name_tags(_R_Tags):
1609 """ /2/networks/[network_name]/tags resource.
1611 Manages per-network tags.
1614 TAG_LEVEL = constants.TAG_NETWORK
1617 class R_2_tags(_R_Tags):
1618 """ /2/tags resource.
1620 Manages cluster tags.
1623 TAG_LEVEL = constants.TAG_CLUSTER