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", "nic.links", "nic.bridges",
75 "disk.sizes", "disk_usage",
76 "beparams", "hvparams",
77 "oper_state", "oper_ram", "oper_vcpus", "status",
78 "custom_hvparams", "custom_beparams", "custom_nicparams",
81 N_FIELDS = ["name", "offline", "master_candidate", "drained",
83 "mtotal", "mnode", "mfree",
84 "pinst_cnt", "sinst_cnt",
85 "ctotal", "cnodes", "csockets",
87 "pinst_list", "sinst_list",
88 "master_capable", "vm_capable",
107 "id", "ops", "status", "summary",
109 "received_ts", "start_ts", "end_ts",
112 J_FIELDS = J_FIELDS_BULK + [
117 _NR_DRAINED = "drained"
118 _NR_MASTER_CANDIDATE = "master-candidate"
119 _NR_MASTER = "master"
120 _NR_OFFLINE = "offline"
121 _NR_REGULAR = "regular"
124 constants.NR_MASTER: _NR_MASTER,
125 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
126 constants.NR_DRAINED: _NR_DRAINED,
127 constants.NR_OFFLINE: _NR_OFFLINE,
128 constants.NR_REGULAR: _NR_REGULAR,
131 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
133 # Request data version field
134 _REQ_DATA_VERSION = "__version__"
136 # Feature string for instance creation request data version 1
137 _INST_CREATE_REQV1 = "instance-create-reqv1"
139 # Feature string for instance reinstall request version 1
140 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
142 # Feature string for node migration version 1
143 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
145 # Feature string for node evacuation with LU-generated jobs
146 _NODE_EVAC_RES1 = "node-evac-res1"
148 ALL_FEATURES = frozenset([
150 _INST_REINSTALL_REQV1,
155 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
159 # FIXME: For compatibility we update the beparams/memory field. Needs to be
160 # removed in Ganeti 2.7
161 def _UpdateBeparams(inst):
162 """Updates the beparams dict of inst to support the memory field.
164 @param inst: Inst dict
165 @return: Updated inst dict
168 beparams = inst["beparams"]
169 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
174 class R_root(baserlib.ResourceBase):
180 """Supported for legacy reasons.
192 class R_version(baserlib.ResourceBase):
193 """/version resource.
195 This resource should be used to determine the remote API version and
196 to adapt clients accordingly.
201 """Returns the remote API version.
204 return constants.RAPI_VERSION
207 class R_2_info(baserlib.OpcodeResource):
211 GET_OPCODE = opcodes.OpClusterQuery
214 """Returns cluster information.
217 client = self.GetClient()
218 return client.QueryClusterInfo()
221 class R_2_features(baserlib.ResourceBase):
222 """/2/features resource.
227 """Returns list of optional RAPI features implemented.
230 return list(ALL_FEATURES)
233 class R_2_os(baserlib.OpcodeResource):
237 GET_OPCODE = opcodes.OpOsDiagnose
240 """Return a list of all OSes.
242 Can return error 500 in case of a problem.
244 Example: ["debian-etch"]
247 cl = self.GetClient()
248 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
249 job_id = self.SubmitJob([op], cl=cl)
250 # we use custom feedback function, instead of print we log the status
251 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
252 diagnose_data = result[0]
254 if not isinstance(diagnose_data, list):
255 raise http.HttpBadGateway(message="Can't get OS list")
258 for (name, variants) in diagnose_data:
259 os_names.extend(cli.CalculateOSNames(name, variants))
264 class R_2_redist_config(baserlib.OpcodeResource):
265 """/2/redistribute-config resource.
268 PUT_OPCODE = opcodes.OpClusterRedistConf
271 class R_2_cluster_modify(baserlib.OpcodeResource):
272 """/2/modify resource.
275 PUT_OPCODE = opcodes.OpClusterSetParams
278 class R_2_jobs(baserlib.ResourceBase):
283 """Returns a dictionary of jobs.
285 @return: a dictionary with jobs id and uri.
288 client = self.GetClient()
291 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
292 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
294 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
295 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
296 uri_fields=("id", "uri"))
299 class R_2_jobs_id(baserlib.ResourceBase):
300 """/2/jobs/[job_id] resource.
304 """Returns a job status.
306 @return: a dictionary with job parameters.
308 - id: job ID as a number
309 - status: current job status as a string
310 - ops: involved OpCodes as a list of dictionaries for each
312 - opstatus: OpCodes status as a list
313 - opresult: OpCodes results as a list of lists
316 job_id = self.items[0]
317 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
319 raise http.HttpNotFound()
320 return baserlib.MapFields(J_FIELDS, result)
323 """Cancel not-yet-started job.
326 job_id = self.items[0]
327 result = self.GetClient().CancelJob(job_id)
331 class R_2_jobs_id_wait(baserlib.ResourceBase):
332 """/2/jobs/[job_id]/wait resource.
335 # WaitForJobChange provides access to sensitive information and blocks
336 # machine resources (it's a blocking RAPI call), hence restricting access.
337 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
340 """Waits for job changes.
343 job_id = self.items[0]
345 fields = self.getBodyParameter("fields")
346 prev_job_info = self.getBodyParameter("previous_job_info", None)
347 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
349 if not isinstance(fields, list):
350 raise http.HttpBadRequest("The 'fields' parameter should be a list")
352 if not (prev_job_info is None or isinstance(prev_job_info, list)):
353 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
356 if not (prev_log_serial is None or
357 isinstance(prev_log_serial, (int, long))):
358 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
361 client = self.GetClient()
362 result = client.WaitForJobChangeOnce(job_id, fields,
363 prev_job_info, prev_log_serial,
364 timeout=_WFJC_TIMEOUT)
366 raise http.HttpNotFound()
368 if result == constants.JOB_NOTCHANGED:
372 (job_info, log_entries) = result
375 "job_info": job_info,
376 "log_entries": log_entries,
380 class R_2_nodes(baserlib.OpcodeResource):
381 """/2/nodes resource.
384 GET_OPCODE = opcodes.OpNodeQuery
387 """Returns a list of all nodes.
390 client = self.GetClient()
393 bulkdata = client.QueryNodes([], N_FIELDS, False)
394 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
396 nodesdata = client.QueryNodes([], ["name"], False)
397 nodeslist = [row[0] for row in nodesdata]
398 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
399 uri_fields=("id", "uri"))
402 class R_2_nodes_name(baserlib.OpcodeResource):
403 """/2/nodes/[node_name] resource.
406 GET_OPCODE = opcodes.OpNodeQuery
409 """Send information about a node.
412 node_name = self.items[0]
413 client = self.GetClient()
415 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
416 names=[node_name], fields=N_FIELDS,
417 use_locking=self.useLocking())
419 return baserlib.MapFields(N_FIELDS, result[0])
422 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
423 """/2/nodes/[node_name]/powercycle resource.
426 POST_OPCODE = opcodes.OpNodePowercycle
428 def GetPostOpInput(self):
429 """Tries to powercycle a node.
432 return (self.request_body, {
433 "node_name": self.items[0],
434 "force": self.useForce(),
438 class R_2_nodes_name_role(baserlib.OpcodeResource):
439 """/2/nodes/[node_name]/role resource.
442 PUT_OPCODE = opcodes.OpNodeSetParams
445 """Returns the current node role.
450 node_name = self.items[0]
451 client = self.GetClient()
452 result = client.QueryNodes(names=[node_name], fields=["role"],
453 use_locking=self.useLocking())
455 return _NR_MAP[result[0][0]]
457 def GetPutOpInput(self):
458 """Sets the node role.
461 baserlib.CheckType(self.request_body, basestring, "Body contents")
463 role = self.request_body
465 if role == _NR_REGULAR:
470 elif role == _NR_MASTER_CANDIDATE:
472 offline = drained = None
474 elif role == _NR_DRAINED:
476 candidate = offline = None
478 elif role == _NR_OFFLINE:
480 candidate = drained = None
483 raise http.HttpBadRequest("Can't set '%s' role" % role)
485 assert len(self.items) == 1
488 "node_name": self.items[0],
489 "master_candidate": candidate,
492 "force": self.useForce(),
493 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
497 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
498 """/2/nodes/[node_name]/evacuate resource.
501 POST_OPCODE = opcodes.OpNodeEvacuate
503 def GetPostOpInput(self):
504 """Evacuate all instances off a node.
507 return (self.request_body, {
508 "node_name": self.items[0],
509 "dry_run": self.dryRun(),
513 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
514 """/2/nodes/[node_name]/migrate resource.
517 POST_OPCODE = opcodes.OpNodeMigrate
519 def GetPostOpInput(self):
520 """Migrate all primary instances from a node.
524 # Support old-style requests
525 if "live" in self.queryargs and "mode" in self.queryargs:
526 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
529 if "live" in self.queryargs:
530 if self._checkIntVariable("live", default=1):
531 mode = constants.HT_MIGRATION_LIVE
533 mode = constants.HT_MIGRATION_NONLIVE
535 mode = self._checkStringVariable("mode", default=None)
541 data = self.request_body
544 "node_name": self.items[0],
548 class R_2_nodes_name_modify(baserlib.OpcodeResource):
549 """/2/nodes/[node_name]/modify resource.
552 POST_OPCODE = opcodes.OpNodeSetParams
554 def GetPostOpInput(self):
555 """Changes parameters of a node.
558 assert len(self.items) == 1
560 return (self.request_body, {
561 "node_name": self.items[0],
565 class R_2_nodes_name_storage(baserlib.OpcodeResource):
566 """/2/nodes/[node_name]/storage resource.
569 # LUNodeQueryStorage acquires locks, hence restricting access to GET
570 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
571 GET_OPCODE = opcodes.OpNodeQueryStorage
573 def GetGetOpInput(self):
574 """List storage available on a node.
577 storage_type = self._checkStringVariable("storage_type", None)
578 output_fields = self._checkStringVariable("output_fields", None)
580 if not output_fields:
581 raise http.HttpBadRequest("Missing the required 'output_fields'"
585 "nodes": [self.items[0]],
586 "storage_type": storage_type,
587 "output_fields": output_fields.split(","),
591 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
592 """/2/nodes/[node_name]/storage/modify resource.
595 PUT_OPCODE = opcodes.OpNodeModifyStorage
597 def GetPutOpInput(self):
598 """Modifies a storage volume on a node.
601 storage_type = self._checkStringVariable("storage_type", None)
602 name = self._checkStringVariable("name", None)
605 raise http.HttpBadRequest("Missing the required 'name'"
610 if "allocatable" in self.queryargs:
611 changes[constants.SF_ALLOCATABLE] = \
612 bool(self._checkIntVariable("allocatable", default=1))
615 "node_name": self.items[0],
616 "storage_type": storage_type,
622 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
623 """/2/nodes/[node_name]/storage/repair resource.
626 PUT_OPCODE = opcodes.OpRepairNodeStorage
628 def GetPutOpInput(self):
629 """Repairs a storage volume on a node.
632 storage_type = self._checkStringVariable("storage_type", None)
633 name = self._checkStringVariable("name", None)
635 raise http.HttpBadRequest("Missing the required 'name'"
639 "node_name": self.items[0],
640 "storage_type": storage_type,
645 class R_2_groups(baserlib.OpcodeResource):
646 """/2/groups resource.
649 GET_OPCODE = opcodes.OpGroupQuery
650 POST_OPCODE = opcodes.OpGroupAdd
652 "name": "group_name",
655 def GetPostOpInput(self):
656 """Create a node group.
659 assert not self.items
660 return (self.request_body, {
661 "dry_run": self.dryRun(),
665 """Returns a list of all node groups.
668 client = self.GetClient()
671 bulkdata = client.QueryGroups([], G_FIELDS, False)
672 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
674 data = client.QueryGroups([], ["name"], False)
675 groupnames = [row[0] for row in data]
676 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
677 uri_fields=("name", "uri"))
680 class R_2_groups_name(baserlib.OpcodeResource):
681 """/2/groups/[group_name] resource.
684 DELETE_OPCODE = opcodes.OpGroupRemove
687 """Send information about a node group.
690 group_name = self.items[0]
691 client = self.GetClient()
693 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
694 names=[group_name], fields=G_FIELDS,
695 use_locking=self.useLocking())
697 return baserlib.MapFields(G_FIELDS, result[0])
699 def GetDeleteOpInput(self):
700 """Delete a node group.
703 assert len(self.items) == 1
705 "group_name": self.items[0],
706 "dry_run": self.dryRun(),
710 class R_2_groups_name_modify(baserlib.OpcodeResource):
711 """/2/groups/[group_name]/modify resource.
714 PUT_OPCODE = opcodes.OpGroupSetParams
716 def GetPutOpInput(self):
717 """Changes some parameters of node group.
721 return (self.request_body, {
722 "group_name": self.items[0],
726 class R_2_groups_name_rename(baserlib.OpcodeResource):
727 """/2/groups/[group_name]/rename resource.
730 PUT_OPCODE = opcodes.OpGroupRename
732 def GetPutOpInput(self):
733 """Changes the name of a node group.
736 assert len(self.items) == 1
737 return (self.request_body, {
738 "group_name": self.items[0],
739 "dry_run": self.dryRun(),
743 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
744 """/2/groups/[group_name]/assign-nodes resource.
747 PUT_OPCODE = opcodes.OpGroupAssignNodes
749 def GetPutOpInput(self):
750 """Assigns nodes to a group.
753 assert len(self.items) == 1
754 return (self.request_body, {
755 "group_name": self.items[0],
756 "dry_run": self.dryRun(),
757 "force": self.useForce(),
761 class R_2_instances(baserlib.OpcodeResource):
762 """/2/instances resource.
765 GET_OPCODE = opcodes.OpInstanceQuery
766 POST_OPCODE = opcodes.OpInstanceCreate
769 "name": "instance_name",
773 """Returns a list of all available instances.
776 client = self.GetClient()
778 use_locking = self.useLocking()
780 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
781 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
783 instancesdata = client.QueryInstances([], ["name"], use_locking)
784 instanceslist = [row[0] for row in instancesdata]
785 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
786 uri_fields=("id", "uri"))
788 def GetPostOpInput(self):
789 """Create an instance.
794 baserlib.CheckType(self.request_body, dict, "Body contents")
796 # Default to request data version 0
797 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
799 if data_version == 0:
800 raise http.HttpBadRequest("Instance creation request version 0 is no"
802 elif data_version != 1:
803 raise http.HttpBadRequest("Unsupported request data version %s" %
806 data = self.request_body.copy()
807 # Remove "__version__"
808 data.pop(_REQ_DATA_VERSION, None)
811 "dry_run": self.dryRun(),
815 class R_2_instances_name(baserlib.OpcodeResource):
816 """/2/instances/[instance_name] resource.
819 GET_OPCODE = opcodes.OpInstanceQuery
820 DELETE_OPCODE = opcodes.OpInstanceRemove
823 """Send information about an instance.
826 client = self.GetClient()
827 instance_name = self.items[0]
829 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
830 names=[instance_name],
832 use_locking=self.useLocking())
834 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
836 def GetDeleteOpInput(self):
837 """Delete an instance.
840 assert len(self.items) == 1
842 "instance_name": self.items[0],
843 "ignore_failures": False,
844 "dry_run": self.dryRun(),
848 class R_2_instances_name_info(baserlib.OpcodeResource):
849 """/2/instances/[instance_name]/info resource.
852 GET_OPCODE = opcodes.OpInstanceQueryData
854 def GetGetOpInput(self):
855 """Request detailed instance information.
858 assert len(self.items) == 1
860 "instances": [self.items[0]],
861 "static": bool(self._checkIntVariable("static", default=0)),
865 class R_2_instances_name_reboot(baserlib.OpcodeResource):
866 """/2/instances/[instance_name]/reboot resource.
868 Implements an instance reboot.
871 POST_OPCODE = opcodes.OpInstanceReboot
873 def GetPostOpInput(self):
874 """Reboot an instance.
876 The URI takes type=[hard|soft|full] and
877 ignore_secondaries=[False|True] parameters.
881 "instance_name": self.items[0],
883 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
884 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
885 "dry_run": self.dryRun(),
889 class R_2_instances_name_startup(baserlib.OpcodeResource):
890 """/2/instances/[instance_name]/startup resource.
892 Implements an instance startup.
895 PUT_OPCODE = opcodes.OpInstanceStartup
897 def GetPutOpInput(self):
898 """Startup an instance.
900 The URI takes force=[False|True] parameter to start the instance
901 if even if secondary disks are failing.
905 "instance_name": self.items[0],
906 "force": self.useForce(),
907 "dry_run": self.dryRun(),
908 "no_remember": bool(self._checkIntVariable("no_remember")),
912 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
913 """/2/instances/[instance_name]/shutdown resource.
915 Implements an instance shutdown.
918 PUT_OPCODE = opcodes.OpInstanceShutdown
920 def GetPutOpInput(self):
921 """Shutdown an instance.
924 return (self.request_body, {
925 "instance_name": self.items[0],
926 "no_remember": bool(self._checkIntVariable("no_remember")),
927 "dry_run": self.dryRun(),
931 def _ParseInstanceReinstallRequest(name, data):
932 """Parses a request for reinstalling an instance.
935 if not isinstance(data, dict):
936 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
938 ostype = baserlib.CheckParameter(data, "os", default=None)
939 start = baserlib.CheckParameter(data, "start", exptype=bool,
941 osparams = baserlib.CheckParameter(data, "osparams", default=None)
944 opcodes.OpInstanceShutdown(instance_name=name),
945 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
950 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
955 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
956 """/2/instances/[instance_name]/reinstall resource.
958 Implements an instance reinstall.
961 POST_OPCODE = opcodes.OpInstanceReinstall
964 """Reinstall an instance.
966 The URI takes os=name and nostartup=[0|1] optional
967 parameters. By default, the instance will be started
971 if self.request_body:
973 raise http.HttpBadRequest("Can't combine query and body parameters")
975 body = self.request_body
977 # Legacy interface, do not modify/extend
979 "os": self._checkStringVariable("os"),
980 "start": not self._checkIntVariable("nostartup"),
985 ops = _ParseInstanceReinstallRequest(self.items[0], body)
987 return self.SubmitJob(ops)
990 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
991 """/2/instances/[instance_name]/replace-disks resource.
994 POST_OPCODE = opcodes.OpInstanceReplaceDisks
996 def GetPostOpInput(self):
997 """Replaces disks on an instance.
1001 "instance_name": self.items[0],
1004 if self.request_body:
1005 data = self.request_body
1006 elif self.queryargs:
1007 # Legacy interface, do not modify/extend
1009 "remote_node": self._checkStringVariable("remote_node", default=None),
1010 "mode": self._checkStringVariable("mode", default=None),
1011 "disks": self._checkStringVariable("disks", default=None),
1012 "iallocator": self._checkStringVariable("iallocator", default=None),
1019 raw_disks = data.pop("disks")
1024 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1025 data["disks"] = raw_disks
1027 # Backwards compatibility for strings of the format "1, 2, 3"
1029 data["disks"] = [int(part) for part in raw_disks.split(",")]
1030 except (TypeError, ValueError), err:
1031 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1033 return (data, static)
1036 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1037 """/2/instances/[instance_name]/activate-disks resource.
1040 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1042 def GetPutOpInput(self):
1043 """Activate disks for an instance.
1045 The URI might contain ignore_size to ignore current recorded size.
1049 "instance_name": self.items[0],
1050 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1054 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1055 """/2/instances/[instance_name]/deactivate-disks resource.
1058 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1060 def GetPutOpInput(self):
1061 """Deactivate disks for an instance.
1065 "instance_name": self.items[0],
1069 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1070 """/2/instances/[instance_name]/recreate-disks resource.
1073 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1075 def GetPostOpInput(self):
1076 """Recreate disks for an instance.
1080 "instance_name": self.items[0],
1084 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1085 """/2/instances/[instance_name]/prepare-export resource.
1088 PUT_OPCODE = opcodes.OpBackupPrepare
1090 def GetPutOpInput(self):
1091 """Prepares an export for an instance.
1095 "instance_name": self.items[0],
1096 "mode": self._checkStringVariable("mode"),
1100 class R_2_instances_name_export(baserlib.OpcodeResource):
1101 """/2/instances/[instance_name]/export resource.
1104 PUT_OPCODE = opcodes.OpBackupExport
1106 "destination": "target_node",
1109 def GetPutOpInput(self):
1110 """Exports an instance.
1113 return (self.request_body, {
1114 "instance_name": self.items[0],
1118 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1119 """/2/instances/[instance_name]/migrate resource.
1122 PUT_OPCODE = opcodes.OpInstanceMigrate
1124 def GetPutOpInput(self):
1125 """Migrates an instance.
1128 return (self.request_body, {
1129 "instance_name": self.items[0],
1133 class R_2_instances_name_failover(baserlib.OpcodeResource):
1134 """/2/instances/[instance_name]/failover resource.
1137 PUT_OPCODE = opcodes.OpInstanceFailover
1139 def GetPutOpInput(self):
1140 """Does a failover of an instance.
1143 return (self.request_body, {
1144 "instance_name": self.items[0],
1148 class R_2_instances_name_rename(baserlib.OpcodeResource):
1149 """/2/instances/[instance_name]/rename resource.
1152 PUT_OPCODE = opcodes.OpInstanceRename
1154 def GetPutOpInput(self):
1155 """Changes the name of an instance.
1158 return (self.request_body, {
1159 "instance_name": self.items[0],
1163 class R_2_instances_name_modify(baserlib.OpcodeResource):
1164 """/2/instances/[instance_name]/modify resource.
1167 PUT_OPCODE = opcodes.OpInstanceSetParams
1169 def GetPutOpInput(self):
1170 """Changes parameters of an instance.
1173 return (self.request_body, {
1174 "instance_name": self.items[0],
1178 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1179 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1182 POST_OPCODE = opcodes.OpInstanceGrowDisk
1184 def GetPostOpInput(self):
1185 """Increases the size of an instance disk.
1188 return (self.request_body, {
1189 "instance_name": self.items[0],
1190 "disk": int(self.items[1]),
1194 class R_2_instances_name_console(baserlib.ResourceBase):
1195 """/2/instances/[instance_name]/console resource.
1198 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1199 GET_OPCODE = opcodes.OpInstanceConsole
1202 """Request information for connecting to instance's console.
1204 @return: Serialized instance console description, see
1205 L{objects.InstanceConsole}
1208 client = self.GetClient()
1210 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1213 raise http.HttpServiceUnavailable("Instance console unavailable")
1215 assert isinstance(console, dict)
1219 def _GetQueryFields(args):
1224 fields = args["fields"]
1226 raise http.HttpBadRequest("Missing 'fields' query argument")
1228 return _SplitQueryFields(fields[0])
1231 def _SplitQueryFields(fields):
1235 return [i.strip() for i in fields.split(",")]
1238 class R_2_query(baserlib.ResourceBase):
1239 """/2/query/[resource] resource.
1242 # Results might contain sensitive information
1243 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1244 GET_OPCODE = opcodes.OpQuery
1245 PUT_OPCODE = opcodes.OpQuery
1247 def _Query(self, fields, qfilter):
1248 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1251 """Returns resource information.
1253 @return: Query result, see L{objects.QueryResponse}
1256 return self._Query(_GetQueryFields(self.queryargs), None)
1259 """Submits job querying for resources.
1261 @return: Query result, see L{objects.QueryResponse}
1264 body = self.request_body
1266 baserlib.CheckType(body, dict, "Body contents")
1269 fields = body["fields"]
1271 fields = _GetQueryFields(self.queryargs)
1273 qfilter = body.get("qfilter", None)
1274 # TODO: remove this after 2.7
1276 qfilter = body.get("filter", None)
1278 return self._Query(fields, qfilter)
1281 class R_2_query_fields(baserlib.ResourceBase):
1282 """/2/query/[resource]/fields resource.
1285 GET_OPCODE = opcodes.OpQueryFields
1288 """Retrieves list of available fields for a resource.
1290 @return: List of serialized L{objects.QueryFieldDefinition}
1294 raw_fields = self.queryargs["fields"]
1298 fields = _SplitQueryFields(raw_fields[0])
1300 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1303 class _R_Tags(baserlib.OpcodeResource):
1304 """Quasiclass for tagging resources.
1306 Manages tags. When inheriting this class you must define the
1311 GET_OPCODE = opcodes.OpTagsGet
1312 PUT_OPCODE = opcodes.OpTagsSet
1313 DELETE_OPCODE = opcodes.OpTagsDel
1315 def __init__(self, items, queryargs, req, **kwargs):
1316 """A tag resource constructor.
1318 We have to override the default to sort out cluster naming case.
1321 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1323 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1326 self.name = items[0]
1329 """Returns a list of tags.
1331 Example: ["tag1", "tag2", "tag3"]
1334 kind = self.TAG_LEVEL
1336 if kind in (constants.TAG_INSTANCE,
1337 constants.TAG_NODEGROUP,
1338 constants.TAG_NODE):
1340 raise http.HttpBadRequest("Missing name on tag request")
1342 cl = self.GetClient()
1343 if kind == constants.TAG_INSTANCE:
1344 fn = cl.QueryInstances
1345 elif kind == constants.TAG_NODEGROUP:
1349 result = fn(names=[self.name], fields=["tags"], use_locking=False)
1350 if not result or not result[0]:
1351 raise http.HttpBadGateway("Invalid response from tag query")
1354 elif kind == constants.TAG_CLUSTER:
1355 assert not self.name
1356 # TODO: Use query API?
1357 ssc = ssconf.SimpleStore()
1358 tags = ssc.GetClusterTags()
1362 def GetPutOpInput(self):
1363 """Add a set of tags.
1365 The request as a list of strings should be PUT to this URI. And
1366 you'll have back a job id.
1370 "kind": self.TAG_LEVEL,
1372 "tags": self.queryargs.get("tag", []),
1373 "dry_run": self.dryRun(),
1376 def GetDeleteOpInput(self):
1379 In order to delete a set of tags, the DELETE
1380 request should be addressed to URI like:
1381 /tags?tag=[tag]&tag=[tag]
1385 return self.GetPutOpInput()
1388 class R_2_instances_name_tags(_R_Tags):
1389 """ /2/instances/[instance_name]/tags resource.
1391 Manages per-instance tags.
1394 TAG_LEVEL = constants.TAG_INSTANCE
1397 class R_2_nodes_name_tags(_R_Tags):
1398 """ /2/nodes/[node_name]/tags resource.
1400 Manages per-node tags.
1403 TAG_LEVEL = constants.TAG_NODE
1406 class R_2_groups_name_tags(_R_Tags):
1407 """ /2/groups/[group_name]/tags resource.
1409 Manages per-nodegroup tags.
1412 TAG_LEVEL = constants.TAG_NODEGROUP
1415 class R_2_tags(_R_Tags):
1416 """ /2/tags resource.
1418 Manages cluster tags.
1421 TAG_LEVEL = constants.TAG_CLUSTER